first commit

This commit is contained in:
feie9456 2025-06-27 08:44:35 +08:00
commit 1cbfe2005e
7 changed files with 492 additions and 0 deletions

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# dependencies (bun install)
node_modules
# output
out
dist
audio
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# backend-test
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.5. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

58
events.jsonl Normal file
View File

@ -0,0 +1,58 @@
{"type": "gameStartRequest", "data": {"players": [{"id": 1, "name": "Alexander", "skin": 4, "observer": false, "role": "wolf"}]}}
{"type": "gameStartRequest", "data": {"players": [{"id": 1, "name": "Alexander", "skin": 4, "observer": false, "role": "wolf"},{"id": 2, "name": "Bella", "skin": 6, "observer": false, "role": "village"}]}}
{"type": "gameStartRequest", "data": {"players": [{"id": 1, "name": "Alexander", "skin": 4, "observer": false, "role": "wolf"},{"id": 2, "name": "Bella", "skin": 6, "observer": false, "role": "village"},{"id": 3, "name": "Charlie", "skin": 9, "observer": false, "role": "seer"}]}}
{"type": "gameStartRequest", "data": {"players": [{"id": 1, "name": "Alexander", "skin": 4, "observer": false, "role": "wolf"},{"id": 2, "name": "Bella", "skin": 6, "observer": false, "role": "village"},{"id": 3, "name": "Charlie", "skin": 9, "observer": false, "role": "seer"},{"id": 4, "name": "Daisy", "skin": 3, "observer": false, "role": "village"}]}}
{"type": "gameStartRequest", "data": {"players": [{"id": 1, "name": "Alexander", "skin": 4, "observer": false, "role": "wolf"},{"id": 2, "name": "Bella", "skin": 6, "observer": false, "role": "village"},{"id": 3, "name": "Charlie", "skin": 9, "observer": false, "role": "seer"},{"id": 4, "name": "Daisy", "skin": 3, "observer": false, "role": "village"},{"id": 5, "name": "Emma", "skin": 8, "observer": false, "role": "witch"}]}}
{"type": "gameStart", "data": {"players": [{"id": 1, "name": "Alexander", "skin": 4, "observer": false, "role": "wolf"},{"id": 2, "name": "Bella", "skin": 6, "observer": false, "role": "village"},{"id": 3, "name": "Charlie", "skin": 9, "observer": false, "role": "seer"},{"id": 4, "name": "Daisy", "skin": 3, "observer": false, "role": "village"},{"id": 5, "name": "Emma", "skin": 8, "observer": false, "role": "witch"},{"id": 6, "name": "Felix", "skin": 2, "observer": false, "role": "wolf"}],"role": "wolf","scene": 3}}
{"type": "phaseChange", "data": {"day": 1, "phase": "night"}}
{"type": "wolfTurn", "data": {"target": 1}}
{"type": "wolfAction", "data": {"player": 1, "action": "kill", "target": 2}}
{"type": "phaseChange", "data": {"day": 1, "phase": "day"}}
{"type": "globalNotice", "data": {"message": "天亮了,大家请睁眼。昨晚平安夜,没有人死亡。"}}
{"type": "turnToSpeak", "data": {"target": 1}}
{"type": "speak", "data": {"player": 1, "message": "<audio style=\"serious\">平安夜|可能|是|狼人|战略性|地|选择|了|观望||也|可能|是|女巫|救|了|人|。</audio><audio style=\"calm\">如果|是|后者||那么|女巫|的|解药|已经|用|掉|了||这|对|我们|接下来|的|夜晚|是|个|不利|因素|。</audio><audio style=\"newscast\">不过|现在|讨论|谁|是|狼人|还|为时过早||我们|需要|更多|信息|。</audio>"}}
{"type": "speak", "data": {"player": 2, "message": "<action>咧嘴一笑,向后靠在椅子上</action><audio style=\"humorous\">哇||平安夜||狼人|是|不是|昨晚|偷懒|了|</audio><action>挑眉看向四周</action><audio style=\"sarcastic\">或者|有人|运气|好||被|女巫|大人|青睐|了|呢|</audio><action>耸耸肩</action><audio style=\"casual\">不管|怎样||我|觉得|1号|分析|得|挺|全面|的||不过|平安夜|确实|让|我们|少|了|很多|线索||只能|靠|观察|发言|来|推测|了|。</audio>"}}
{"type": "speak", "data": {"player": 3, "message": "<action>身体微微前倾,表情谨慎</action><audio style=\"calm\">平安夜|确实|让|我们|少|了|一些|信息||但|也|给|了|我们|思考|的|空间|。</audio><action>若有所思地看着其他人</action><audio style=\"narration-professional\">我|认为|我们|应该|考虑|两种|可能性||如果|狼人|没有|行动||说明|他们|比较|谨慎||如果|有人|被|救|了||说明|狼人|已经|暴露|了|杀人|意图|。</audio><action>轻声补充</action><audio style=\"serious\">无论|是|哪种|情况||我们|都|应该|仔细|观察|每个|人|的|发言|逻辑||这|往往|能|揭示|更多|信息|。</audio>"}}
{"type": "speak", "data": {"player": 4, "message": "<action>直视前方,表情平静</action><audio style=\"newscast\">我|认为|狼人|昨晚|行动|了||只是|被|女巫|破坏|了|计划|。</audio><action>环顾四周</action><audio style=\"disgruntled\">第一晚|不|杀人|对|狼人|没有|好处||他们|需要|尽快|减少|村民|数量|。</audio><action>略微点头</action><audio style=\"newscast\">所以|我们|可以|假设|女巫|已经|用|掉|解药|了||这点|在|后面|的|讨论|中|很|重要|。</audio>"}}
{"type": "speak", "data": {"player": 5, "message": "<action>双手轻放桌面,表情略显犹豫</action><audio style=\"fearful\">4号|的|推理|很|有|道理|...|狼人|第一晚|通常|会|行动|。</audio><action>目光短暂停留在2号身上</action><audio style=\"whispering\">我|觉得|1号|和|6号|的|表现|可能|值得|关注||他们|似乎|都|很|克制||不像|是|急于|表达|观点|的|样子|。</audio><action>轻声补充</action><audio style=\"sad\">不过|这|只是|我|的|初步|观察||我们|还|需要|更多|线索|。</audio>"}}
{"type": "speak", "data": {"player": 6, "message": "<action>坐姿端正,表情平静</action><audio style=\"friendly\">我|觉得|大家|都|提出|了|很|好|的|观点|。</audio><action>微微向前倾身</action><audio style=\"gentle\">不过|我|想|指出||5号|似乎|很|快|就|把|注意力|引向|了|我|和|1号||这种|没有|实质|证据|的|怀疑|让|我|有些|困惑|。</audio><action>环顾四周</action><audio style=\"newscast\">我|认为|3号|的|分析|更为|客观||我们|确实|应该|关注|发言|逻辑|而|不是|主观|印象|。|比如|4号|的|发言|简短|但|逻辑|严密||而|5号|的|发言|则|更多|基于|感觉|。</audio>"}}
{"type": "noteAppear", "data": {"message": "一张神秘的小纸条突然从天而降,飘到了桌子中央...3号玩家捡到了这张纸条。"}}
{"type": "speak", "data": {"player": 3, "message": "<action>表情略显困惑</action><audio style=\"calm\">我|刚刚|看到|了|一张|纸条||上面|写着|'月夜|幽灵|双|影|现||一|静|一|动|皆|可疑|'|。</audio><action>环视众人</action><audio style=\"narration-professional\">这|似乎|在|暗示|两个|人|可能|有|问题||一个|行事|沉稳||一个|比较|活跃|。</audio><action>轻轻摇头</action><audio style=\"serious\">当然||这种|提示|相当|模糊||可以|有|多种|解读|。|我们|不能|完全|依赖|它||但|可以|作为|参考|。</audio>"}}
{"type": "speak", "data": {"player": 2, "message": "<action>挑眉,表情困惑</action><audio style=\"humorous\">等等||这|很|有趣|。</audio><action>高举纸条</action><audio style=\"humorous\">这张|纸条|上|写着|'三五|密语|暗|相通||真伪|难辨|需|警惕|'||但|3号|刚才|说|的|是|'月夜|幽灵|双|影|现||一|静|一|动|皆|可疑|'|。</audio><action>环顾四周,眼神锐利</action><audio style=\"sarcastic\">看来|这张|纸条|在|途中|变|了|内容|啊|。|6号||作为|中间|的|传递者||你|能|解释|一下|吗|</audio>"}}
{"type": "speak", "data": {"player": 6, "message": "<action>表情略显惊讶,随后恢复冷静</action><audio style=\"embarrassed\">我|承认|我|确实|修改|了|纸条|内容|。</audio><action>环视众人</action><audio style=\"friendly\">但|我|这么|做|是|为了|测试|大家|的|反应|。|注意|到|3号|拿到|纸条|后||立即|提到|'一|静|一|动|皆|可疑|'||这|很|容易|被|解读|为|针对|某些|人|的|暗示|。</audio><action>看向5号</action><audio style=\"gentle\">而且|5号|之前|也|无端|指责|我|和|1号||我|怀疑|3号|和|5号|可能|在|合作|引导|舆论|。|我|修改|的|内容|恰恰|点出|了|这|一点||'三五|密语|暗|相通|'|。</audio>"}}
{"type": "turnToVote", "data": {"target": 1}}
{"type": "vote", "data": {"player": 1, "target": 3}}
{"type": "vote", "data": {"player": 2, "target": 6}}
{"type": "vote", "data": {"player": 3, "target": 6}}
{"type": "vote", "data": {"player": 4, "target": 6}}
{"type": "vote", "data": {"player": 5, "target": 6}}
{"type": "vote", "data": {"player": 6, "target": 3}}
{"type": "voteEnd", "data": {"target": 6}}
{"type": "playerKilled", "data": {"targets": [6]}}
{"type": "phaseChange", "data": {"day": 2, "phase": "night"}}
{"type": "wolfTurn", "data": {"target": 1}}
{"type": "wolfAction", "data": {"player": 1, "action": "kill", "target": 3}}
{"type": "phaseChange", "data": {"day": 2, "phase": "day"}}
{"type": "globalNotice", "data": {"message": "天亮了大家请睁眼。昨晚3号死亡。"}}
{"type": "playerKilled", "data": {"targets": [3]}}
{"type": "speak", "data": {"player": 2, "message": "<action>表情严肃,不再嬉笑</action><audio style=\"serious\">3号|被|杀||这|很|可能|不|是|巧合|。</audio><action>环视众人</action><audio style=\"serious\">如果|6号|是|狼人||那么|1号|很|可能|是|他|的|同伴|。|昨天|6号|修改|纸条|的|行为|已经|很|可疑||而且|1号|和|6号|的|投票|也|完全|一致|。</audio><action>看向1号</action><audio style=\"disgruntled\">1号||你|有|什么|要|说|的|吗|</audio>"}}
{"type": "turnToSpeak", "data": {"target": 1}}
{"type": "speak", "data": {"player": 1, "message": "<audio style=\"serious\">我|理解|大家|的|怀疑||但|请|听|我|解释|。</audio><audio style=\"newscast\">事实上||我|是|预言家|。|昨晚|我|查验|了|5号||结果|是|狼人|。</audio><audio style=\"calm\">你们|投|出|6号|是|正确|的||但|别|忘|了|那张|被|篡改|的|纸条|上|写|的|是|'三五|密语|暗|相通|'|。|3号|被|杀|并|不能|证明|他|是|好人||也|可能|是|狼人|之间|的|内讧|。</audio>"}}
{"type": "speak", "data": {"player": 5, "message": "<action>表情先是震惊,然后坚定</action><audio style=\"fearful\">1号||你|的|预言家|身份|宣称|来|得|太|突然|了|。</audio><action>环顾四周</action><audio style=\"sad\">如果|你|真|的|是|预言家||为什么|不|早点|站|出来||而|是|等到|被|怀疑|时|才|突然|跳|预言家|</audio><action>看向2号</action><audio style=\"whispering\">而且||我|想|分享|一个|重要|信息||第一晚|是|我|救|了|2号|。|这|意味着|2号|是|被|狼人|袭击|的|目标||所以|他|一定|是|好人|。|我们|现在|至少|可以|确定|2号|的|身份|是|清白|的|。</audio>"}}
{"type": "speak", "data": {"player": 4, "message": "<action>表情严肃,语气坚定</action><audio style=\"newscast\">1号|的|预言家|身份|声明|不|可信|。</audio><action>直视1号</action><audio style=\"disgruntled\">跳|预言家|的|时机|太|过|刻意||明显|是|狼人|的|自救|手段|。</audio><action>看向5号</action><audio style=\"newscast\">5号|自称|是|女巫|并|说|她|救|了|2号||这个|信息|很|关键|。|如果|属实||那么|2号|确实|是|好人||而|1号|作为|最后|的|狼人|在|尝试|转移|视线|。</audio>"}}
{"type": "noteAppear", "data": {"message": "最后一张小纸条飘落到桌子中央...1号玩家捡到了这张纸条。"}}
{"type": "notePick", "data": {"player": 1, "content": "一人假面具正义,临危跳职保自身"}}
{"type": "turnToNote", "data": {"target": 1}}
{"type": "noteAction", "data": {"player": 1, "action": "modify", "originalContent": "一人假面具正义,临危跳职保自身", "newContent": "五人药水有玄机,解毒两瓶皆在握"}}
{"type": "noteAction", "data": {"player": 1, "action": "drop"}}
{"type": "speak", "data": {"player": 4, "message": "<action>皱眉,表情怀疑</action><audio style=\"disgruntled\">这张|纸条|写着|'五人|药水|有|玄机||解毒|两瓶|皆|在|握|'|。</audio><action>看向5号</action><audio style=\"newscast\">这|似乎|在|暗示|5号|拥有|两瓶|解药||但|我们|都|知道|女巫|只有|一瓶|解药|和|一瓶|毒药|。</audio><action>转向1号</action><audio style=\"disgruntled\">1号||我|怀疑|你|篡改|了|纸条|内容||就|像|6号|曾经|做|的|那样|。|这|再次|证明|你|不|可信|。</audio>"}}
{"type": "turnToSpeak", "data": {"target": 1}}
{"type": "speak", "data": {"player": 1, "message": "<audio style=\"serious\">我|没有|篡改|任何|纸条||这张|纸条|恰恰|证明|了|5号|在|说谎|。</audio><audio style=\"calm\">女巫|只有|一瓶|解药||但|纸条|暗示|5号|声称|拥有|两瓶|。|这|证明|她|不|是|真正|的|女巫||而|是|在|试图|建立|可信度|。</audio><audio style=\"newscast\">我|作为|预言家|已经|验证|了|她|是|狼人||现在|这张|纸条|更加|证实|了|这|一点|。</audio>"}}
{"type": "vote", "data": {"player": 2, "target": 1}}
{"type": "turnToVote", "data": {"target": 1}}
{"type": "vote", "data": {"player": 1, "target": 5}}
{"type": "vote", "data": {"player": 5, "target": 1}}
{"type": "vote", "data": {"player": 4, "target": 1}}
{"type": "voteEnd", "data": {"target": 1}}
{"type": "playerKilled", "data": {"targets": [1]}}
{"type": "gameOver", "data": {"result": "defeat", "winningSide": "village", "reason": "好人阵营获胜,你失败了!"}}

92
events_0.jsonl Normal file
View File

@ -0,0 +1,92 @@
{"type": "gameStartRequest","data": {"players": [{"id": 1,"name": "Alice","skin": 0, "role": "wolf"}],"role": "observer","scene": 1}}
{"type": "gameStartRequest","data": {"players": [{"id": 1,"name": "Alice","skin": 0, "role": "wolf"},{"id": 2,"name": "Bob","skin": 2, "role": "wolf"}],"role": "observer","scene": 1}}
{"type": "gameStartRequest","data": {"players": [{"id": 1,"name": "Alice","skin": 0, "role": "wolf"},{"id": 2,"name": "Bob","skin": 2, "role": "wolf"},{"id": 5,"name": "Eve","skin": 5, "role": "witch"}],"role": "observer","scene": 1}}
{"type": "gameStartRequest","data": {"players": [{"id": 1,"name": "Alice","skin": 0, "role": "wolf"},{"id": 2,"name": "Bob","skin": 2, "role": "wolf"},{"id": 3,"name": "Charlie","skin": 3, "role": "village"},{"id": 5,"name": "Eve","skin": 5, "role": "witch"}],"role": "observer","scene": 1}}
{"type": "gameStartRequest","data": {"players": [{"id": 1,"name": "Alice","skin": 0, "role": "wolf"},{"id": 2,"name": "Bob","skin": 3, "role": "wolf"},{"id": 3,"name": "Charlie","skin": 2, "role": "village"},{"id": 4,"name": "Diana","skin": 3, "role": "village"},{"id": 5,"name": "Eve","skin": 5, "role": "witch"},{"id": 6,"name": "Frank","skin": 8, "role": "seer"}],"role": "observer","scene": 1}}
{"type": "gameStart","data": {"players": [{"id": 1,"name": "Alice","skin": 0, "role": "wolf"},{"id": 2,"name": "Bob","skin": 11, "role": "wolf"},{"id": 3,"name": "Charlie","skin": 6, "role": "village"},{"id": 4,"name": "Diana","skin": 3, "role": "village"},{"id": 5,"name": "Eve","skin": 5, "role": "witch"},{"id": 6,"name": "Frank","skin": 8, "role": "seer"}],"role": "observer","scene": 1}}
{"type": "phaseChange", "data": {"day": 1, "phase": "night"}}
{"type": "reasoning", "data": {"player": 1, "message": "第一晚|没有|信息|可供|分析||随机|选|一个|玩家"}}
{"type": "wolfAction", "data": {"player": 1, "action": "kill", "target": 4}}
{"type": "reasoning", "data": {"player": 6, "message": "第一晚|没有|信息|可供|分析||随机|选|一个|玩家"}}
{"type": "seerAction", "data": {"target": 2}}
{"type": "seerResult", "data": {"target": 2, "role": "wolf"}}
{"type": "reasoning", "data": {"player": 5, "message": "第一晚|救人|很|重要||能够|保留|更多|的|好人|力量|。|虽然|不知道|被杀|的|是|谁||但|应该|是|个|对|好人|阵营|有|价值|的|角色|。"}}
{"type": "witchAction", "data": {"usedAntidote": true, "antidoteTarget": 4, "usedPoison": false}}
{"type": "phaseChange", "data": {"day": 1, "phase": "day"}}
{"type": "globalNotice", "data": {"message": "天亮了,大家请睁眼。昨晚平安夜,没有人死亡。"}}
{"type": "reasoning", "data": {"player": 1, "message": "平安夜|意味|着|女巫|用了|解药|。|现在|不要|急|着|跳|预言家||先|观察|局势||看看|真|预言家|会|不会|跳出来|。|我|应该|先引导|大家|讨论|女巫|的|身份||分散|注意力|。"}}
{"type": "speak", "data": {"player": 1, "message": "<action>双手交叉放在桌上,微微前倾</action><audio style=\"calm\">女巫|第一晚|就|用了|解药||说明|女巫|很|谨慎|。|女巫|能|看到|被|刀|的|人|是|谁||这|对|我们|很|有利|。|不过|这|也|意味着|女巫|的|解药|已经|用完了|。</audio><action>环顾四周</action><audio style=\"friendly\">大家|有|什么|线索|可以|分享|吗|</audio>"}}
{"type": "reasoning", "data": {"player": 2, "message": "我|需要|制造|信息|干扰||拖延|时间||同时|试探|预言家|是否|会|跳出来|。|表现|得|激动|一些||掩盖|我|是|狼人|的|事实|。"}}
{"type": "speak", "data": {"player": 2, "message": "<action>双手夸张地挥舞</action><audio style=\"excited\">哇||第一晚|就|用|解药||这|太|紧张|了||我|对|女巫|的|决定|表示|敬佩||但|也|有点|担心||因为|现在|解药|已经|没了|。</audio><action>身体前倾,目光扫视每个人</action><audio style=\"shouting\">我|想|知道|预言家|有|没有|什么|发现||如果|查到|了|狼人||请|一定|要|说出来|</audio><action>做出思考状</action><audio style=\"cheerful\">不过|也|可能|预言家|现在|不想|暴露|自己||这|也|可以|理解|。|大家|觉得|谁|最|可疑||我|个人|觉得|游戏|刚|开始||发言|比较|少||很|难|判断|...</audio>"}}
{"type": "reasoning", "data": {"player": 3, "message": "伪装|成|新手|可以|降低|狼人|对|我|的|警惕性||同时|通过|提问|来|观察|其他|玩家|的|反应||特别|是|关于|被|救者|身份|的|问题||狼人|可能|会|有|特别|的|反应|。"}}
{"type": "speak", "data": {"player": 3, "message": "<action>挠头,表情困惑</action><audio style=\"embarrassed\">我|有|个|问题|...|既然|有人|被|救|了||那|被|救|的|人|知道|自己|被|救|了|吗|</audio><action>眼睛微微睁大,恍然大悟</action><audio style=\"gentle\">他|能|不能|告诉|我们|他|是|谁||这样|我们|就|能|知道|谁|是|好人|了||对|吧|</audio>"}}
{"type": "reasoning", "data": {"player": 4, "message": "仔细|分析|前面|玩家|的|发言||尤其|是|1号|和|2号||他们|的|发言|模式|很|可疑|。"}}
{"type": "speak", "data": {"player": 4, "message": "<action>皱眉,目光锐利</action><audio style=\"serious\">我们|不|知道|谁|被|刀|了||被|救|的|人|也|不会|收到|通知|。</audio><action>指向2号</action><audio style=\"angry\">不过|我|对|2号|的|长篇大论|有些|怀疑||说|了|那么|多|但|没有|实质|内容||这|是|狼人|常用|的|话术|。</audio><action>转向1号</action><audio style=\"disgruntled\">还有||1号|的|发言|有意|引导|我们|关注|女巫||似乎|想|转移|视线|。</audio>"}}
{"type": "reasoning", "data": {"player": 5, "message": "作为|女巫||我|已经|用了|解药||剩下|毒药|需要|谨慎|使用|。|现在|不要|暴露|自己||先|同意|4号|对|2号|的|怀疑||但|语气|保持|犹豫|。"}}
{"type": "speak", "data": {"player": 5, "message": "<action>身体微微后缩,目光游移</action><audio style=\"whispering\">我|...|我|觉得|现在|下|结论|还|太|早|。</audio><action>小心翼翼地看了看4号</action><audio style=\"fearful\">不过|4号|的|分析|有点|道理||2号|确实|话|很|多|但|没|说|什么|有用|的||预言家|给|一些|线索|吧|。</audio>"}}
{"type": "reasoning", "data": {"player": 6, "message": "我|已经|确认|2号|是|狼人||考虑到|女巫|已经|用了|解药||如果|今晚|我|被|杀|就|没有|人|能|救|我|了|。|现在|是|跳出来|的|最佳|时机||必须|尽快|告诉|大家|我|的|发现|。"}}
{"type": "speak", "data": {"player": 6, "message": "<action>坐直身体,表情严肃</action><audio style=\"newscast\">我|是|预言家|。|昨晚|我|查|了|2号||是|狼人|。</audio><action>目光坚定地扫视全场</action><audio style=\"documentary-narration\">我|知道|这样|跳出来|很|危险||但|第一晚|有|人|被|救||说明|女巫|已经|用了|解药||如果|今晚|我|被|杀|就|没人|能|救|了||所以|我|出来|说|一下|。</audio>"}}
{"type": "noteAppear", "data": {"message": "在投票前,一张神秘的小纸条从天而降,落到了场中..."}}
{"type": "notePick", "data": {"player": 3, "content": "二人饭后常散步,月下窃窃私语甚是亲密"}}
{"type": "reasoning", "data": {"player": 3, "message": "这条|信息|似乎|在|暗示|2号|的|身份||与|预言家|的|判断|吻合|。|但|如果|我|直接|表明||可能|会|暴露|信息|来源|。|我|可以|利用|这个|机会|试探|其他|人|的|反应|。"}}
{"type": "noteAction", "data": {"player": 3, "action": "modify", "originalContent": "二人饭后常散步,月下窃窃私语甚是亲密", "newContent": "四人独处时常皱眉,似有心事难言"}}
{"type": "noteAction", "data": {"player": 3, "action": "drop"}}
{"type": "notePick", "data": {"player": 4, "content": "四人独处时常皱眉,似有心事难言"}}
{"type": "reasoning", "data": {"player": 4, "message": "这张|纸条|内容|很|奇怪||似乎|在|暗示|4号|有|问题||但|这|与|我|的|直觉|不符|。|考虑到|我|刚刚|才|质疑|了|1号|和|2号||这|可能|是|狼人|阵营|的|诡计|。|我|应该|在|投票|前|提出来|讨论|。"}}
{"type": "noteAction", "data": {"player": 4, "action": "drop"}}
{"type": "speak", "data": {"player": 4, "message": "<action>举起纸条,表情困惑</action><audio style=\"disgruntled\">等等||在|我们|投票|前||我|刚刚|捡到|一张|纸条|。</audio><action>念出内容</action><audio style=\"serious\">上面|写着|'四人|独处|时|常|皱眉||似|有|心事|难言|'|。|不|知道|这|是|什么|意思||但|似乎|有人|想|暗示|我|有|问题|</audio><action>眼神锐利地扫视全场</action><audio style=\"angry\">我|觉得|这|更|像|是|狼人|的|陷阱|。</audio>"}}
{"type": "reasoning", "data": {"player": 3, "message": "4号|的|困惑|是|真实|的||这|是|我|的|机会||可以|继续|扮演|新手|角色|。"}}
{"type": "speak", "data": {"player": 3, "message": "<action>尴尬地举手</action><audio style=\"embarrassed\">哎呀||抱歉||那|是|我|刚才|掉|的|。</audio><action>低头看着自己的手指</action><audio style=\"chat\">我|...|其实|我|收到|了|另|一张|纸条||但|上面|的|内容|我|不|确定|是|什么|意思||所以|我|就|...|改|了|一下|。</audio><action>抬头,天真</action><audio style=\"gentle\">原来|的|纸条|是|关于|2号|的||说|什么|'二人|饭后|常|散步||月下|窃窃私语|甚是|亲密|'|。|我|觉得|可能|是|在|暗示|2号|和|谁|是|一伙|的|</audio>"}}
{"type": "reasoning", "data": {"player": 2, "message": "小|纸条|暴露|了|信息||需要|立即|反击|混淆|局面|。"}}
{"type": "speak", "data": {"player": 2, "message": "<action>猛地站起来,表情夸张</action><audio style=\"shouting\">什么||这|太|荒谬|了|</audio><action>指着3号</action><audio style=\"angry\">3号|你|为什么|要|篡改|纸条|内容||你|是|想|陷害|4号|吗||这种|行为|本身|就|很|可疑|。</audio><action>双手叉腰</action><audio style=\"excited\">而且|你|说|的|原始|内容|明显|是|在|暗示|我|是|狼人||配合|6号|的|预言家|身份||这|很|可能|是|好人|阵营|的|联合|策略|</audio>"}}
{"type": "reasoning", "data": {"player": 1, "message": "必须|乘胜追击||引导|大家|怀疑|3号|和|6号|的|联合|。"}}
{"type": "speak", "data": {"player": 1, "message": "<action>保持冷静,但语气坚定</action><audio style=\"serious\">我|同意|2号|的|看法|。</audio><action>轻轻指向3号</action><audio style=\"calm\">3号|的|行为|非常|可疑||篡改|纸条|是|在|误导|大家|。</audio><action>转向6号眉头微皱</action><audio style=\"friendly\">再|联系|6号|刚好|查验|了|2号|是|狼人||会|不会|这|是|你们|预先|串通|好|的|</audio>"}}
{"type": "reasoning", "data": {"player": 6, "message": "狼人|的|联合|反击|很|强势||但|小|纸条|内容|恰恰|佐证|了|我|的|预言|结果||这|是|个|机会|。"}}
{"type": "speak", "data": {"player": 6, "message": "<action>双手平放在桌上,表情镇定</action><audio style=\"calm\">各位|冷静|。</audio><action>用专业的语气分析</action><audio style=\"newscast\">小|纸条|的|内容|确实|与|我|的|预言|结果|一致||这|不|是|巧合|。|3号|篡改|纸条|的|行为|虽然|不当||但|他|最终|还是|说|出|了|真相|。</audio><action>目光坚定扫视全场</action><audio style=\"documentary-narration\">我们|现在|应该|集中|精力|投|出|2号|这个|确认|的|狼人||不要|被|狼人|的|混淆|视听|所|迷惑|。</audio>"}}
{"type": "reasoning", "data": {"player": 5, "message": "现在|情况|更加|复杂|了||但|预言家|和|小|纸条|都|指向|2号|是|狼人||这个|信息|很|可能|是|可靠|的|。"}}
{"type": "speak", "data": {"player": 5, "message": "<action>身体微微颤抖,声音轻柔</action><audio style=\"whispering\">我|...|我|觉得|6号|说|得|有|道理|。</audio><action>偷瞄了一眼2号又迅速移开视线</action><audio style=\"fearful\">无论|3号|为什么|要|篡改|纸条||最|重要|的|是|我们|现在|有|两个|线索|都|指向|2号|有|问题|。</audio>"}}
{"type": "reasoning", "data": {"player": 1, "message": "预言家|已经|跳出来|指认|2号||必须|投|预言家|来|制造|混乱||为|后续|跳|预言家|做|准备|。"}}
{"type": "vote", "data": {"player": 1, "target": 6}}
{"type": "reasoning", "data": {"player": 2, "message": "被|预言家|指认|了||但|现在|跟随|狼|队友|的|策略||一起|投|预言家||争取|混淆|局面|。"}}
{"type": "vote", "data": {"player": 2, "target": 6}}
{"type": "reasoning", "data": {"player": 3, "message": "预言家|的|信息|很|有|价值||2号|表现|也|确实|可疑||我|的|小|纸条|策略|虽然|有些|冒险||但|最终|帮助|确认|了|这|一点|。|我|相信|预言家|的|判断|。"}}
{"type": "vote", "data": {"player": 3, "target": 2}}
{"type": "reasoning", "data": {"player": 4, "message": "预言家|给|出|了|明确|的|狼人|信息||而且|与|小|纸条|内容|相符||应该|投|出|2号|。"}}
{"type": "vote", "data": {"player": 4, "target": 2}}
{"type": "reasoning", "data": {"player": 5, "message": "预言家|的|信息|非常|关键||我|应该|支持|他|的|判断||投|出|2号|。"}}
{"type": "vote", "data": {"player": 5, "target": 2}}
{"type": "reasoning", "data": {"player": 6, "message": "我|已经|确认|2号|是|狼人||必须|投|出|他|。"}}
{"type": "vote", "data": {"player": 6, "target": 2}}
{"type": "voteEnd", "data": {"target": 2}}
{"type": "playerKilled", "data": {"targets": [2]}}
{"type": "phaseChange", "data": {"day": 2, "phase": "night"}}
{"type": "reasoning", "data": {"player": 1, "message": "现在|只|剩|我|一个|狼人|了||预言家|已经|暴露||必须|先|清除|他||否则|明天|我|就|会|被|投|出局|。"}}
{"type": "wolfAction", "data": {"player": 1, "action": "kill", "target": 6}}
{"type": "reasoning", "data": {"player": 5, "message": "我|已经|没有|解药|了||不能|救|预言家|。|是否|要|使用|毒药||现在|还|不能|确定|1号|就|是|狼人||如果|误|杀|好人|会|很|危险||先|保留|毒药|。"}}
{"type": "phaseChange", "data": {"day": 2, "phase": "day"}}
{"type": "globalNotice", "data": {"message": "天亮了大家请睁眼。昨晚6号被刀。"}}
{"type": "playerKilled", "data": {"targets": [6]}}
{"type": "noteAppear", "data": {"message": "又一张神秘小纸条落入场中这次是5号玩家捡到了。"}}
{"type": "notePick", "data": {"player": 5, "content": "一人故作镇定,夜间却形迹可疑"}}
{"type": "reasoning", "data": {"player": 5, "message": "这|很|可能|在|暗示|1号|是|狼人|。|结合|6号|预言家|的|死亡||以及|1号|之前|的|可疑|行为||我|应该|告诉|大家|这个|信息||但|要|小心|不要|暴露|自己|的|身份|。"}}
{"type": "noteAction", "data": {"player": 5, "action": "drop"}}
{"type": "speak", "data": {"player": 5, "message": "<action>声音轻柔,有些颤抖</action><audio style=\"whispering\">我|...|我|捡到|了|一张|纸条|。</audio><action>缓缓展示纸条</action><audio style=\"fearful\">上面|写着|'一人|故作|镇定||夜间|却|形迹|可疑|'|。</audio><action>偷偷瞄了一眼1号</action><audio style=\"sad\">我|觉得|这|可能|在|暗示|1号||因为|他|总是|表现|得|很|冷静|...</audio>"}}
{"type": "reasoning", "data": {"player": 1, "message": "预言家|被|杀||是|我|自救|的|最佳|时机|。|我|应该|马上|跳|预言家||声称|自己|查到|了|3号|是|好人||然后|把|怀疑|引向|4号|和|5号|。|同时|我|需要|对抗|小|纸条|带来|的|怀疑|。"}}
{"type": "speak", "data": {"player": 1, "message": "<action>从容不迫地站起身,轻轻整理衣领</action><audio style=\"newscast\">其实|我|才|是|预言家||但|昨天|没有|立即|跳|是|因为|担心|被|狼人|刀|。</audio><action>竖起一根手指</action><audio style=\"calm\">昨晚|我|查|了|3号||是|好人|。|昨天|6号|说|2号|是|狼人||结果|证明|他|是|对|的|。</audio><action>目光锐利地扫向4号和5号</action><audio style=\"serious\">所以|剩下|的|狼人|应该|在|4号|和|5号|之间|。</audio><action>嘲讽地扬起纸条</action><audio style=\"disgruntled\">至于|那张|小|纸条||明显|是|某人|想要|陷害|我||就|像|昨天|3号|试图|篡改|纸条|一样||这些|小|纸条|根本|不|可信|</audio>"}}
{"type": "reasoning", "data": {"player": 3, "message": "1号|的|行为|非常|可疑||真|预言家|已经|跳|了||他|为什么|第一天|不|跳||很|可能|是|狼人|在|自救|。|加上|小|纸条|的|暗示||我|更加|确定|他|有|问题|。"}}
{"type": "speak", "data": {"player": 3, "message": "<action>挠头,表情从困惑转为怀疑</action><audio style=\"chat\">等等||你|说|你|是|预言家|</audio><action>歪着头,装作天真</action><audio style=\"embarrassed\">那|为什么|第一天|不|跳|呢|</audio><action>眼睛忽然变得锐利</action><audio style=\"gentle\">虽然|你|给|我|发|了|金水||但|我|对|你|的|身份|依然|存疑|。|两|天|的|小|纸条|指向|都|很|清晰||2号|和|1号|是|狼人|搭档|。</audio>"}}
{"type": "reasoning", "data": {"player": 4, "message": "1号|明显|是|在|伪装|预言家||试图|混淆|视听|。|真|预言家|已经|确认|了|2号|是|狼人||而|1号|现在|突然|跳|预言家||这|是|狼人|惯用|的|自救|手段|。|小|纸条|再次|证实|了|我|的|判断|。"}}
{"type": "speak", "data": {"player": 4, "message": "<action>拍桌而起,眼神凌厉</action><audio style=\"angry\">1号|突然|跳|预言家|很|可疑|</audio><action>用手指着1号</action><audio style=\"disgruntled\">真|预言家|已经|确认|了|2号|是|狼人||而|1号|昨天|的|表现|就|很|奇怪||现在|又|在|死亡|的|预言家|之后|跳|预言家|。</audio><action>冷笑一声</action><audio style=\"serious\">这|明显|是|狼人|想要|混淆|视听|。|再|加上|今天|的|小|纸条||我|认为|1号|就|是|最后|一|只|狼|。</audio>"}}
{"type": "reasoning", "data": {"player": 5, "message": "局势|变得|更加|复杂|了||但|1号|的|行为|确实|很|可疑||我|同意|4号|的|看法||但|还是|保持|一定|的|谨慎|态度|。|考虑到|我|拿到|了|小|纸条||应该|更|坚定|一些|。"}}
{"type": "speak", "data": {"player": 5, "message": "<action>小心地向前倾身,声音比平时稍大</action><audio style=\"fearful\">我|...|我|也|觉得|1号|很|可疑|。</audio><action>举起手中的纸条</action><audio style=\"whispering\">小|纸条|的|描述|和|1号|的|行为|太|符合|了|。</audio><action>鼓起勇气直视1号</action><audio style=\"sad\">真|预言家|已经|确认|了|2号|是|狼人||而|1号|现在|才|跳|预言家||这|很|不|合理|。</audio>"}}
{"type": "reasoning", "data": {"player": 1, "message": "我|必须|转移|怀疑||5号|的|发言|提到|了|小|纸条||是|引起|争议|的|关键|。|必须|把|票|投|给|她||同时|争取|3号|的|信任|。"}}
{"type": "vote", "data": {"player": 1, "target": 5, "message": "<action>镇定自若地指向5号</action><audio style=\"calm\">我|投|5号|。|她|明显|在|利用|小|纸条|操控|局势||这|是|狼人|的|惯用|伎俩|。</audio>"}}
{"type": "reasoning", "data": {"player": 3, "message": "情况|很|复杂||1号|说|我|是|好人||但|他|可能|是|狼人|在|混淆|视听|...|两|天|的|小|纸条|都|指向|了|狼人||我|应该|相信|这些|线索|。"}}
{"type": "vote", "data": {"player": 3, "target": 1, "message": "<action>犹豫了一下最终坚定地指向1号</action><audio style=\"gentle\">我|投|1号|。|这|两|天|的|线索|都|指向|他|和|2号|是|狼人|搭档|。</audio>"}}
{"type": "reasoning", "data": {"player": 4, "message": "我|非常|确信|1号|是|最后|的|狼人||必须|投|他|出局|。"}}
{"type": "vote", "data": {"player": 4, "target": 1, "message": "<action>毫不犹豫地指向1号</action><audio style=\"angry\">我|投|1号||狼人|的|把戏|已经|被|识破|了|。</audio>"}}
{"type": "reasoning", "data": {"player": 5, "message": "1号|的|行为|很|可疑||我|应该|投|他|。"}}
{"type": "vote", "data": {"player": 5, "target": 1, "message": "<action>战战兢兢地举起手指向1号</action><audio style=\"whispering\">我|...|我|投|1号|。|小|纸条|不会|说谎|。</audio>"}}
{"type": "voteEnd", "data": {"target": 1}}
{"type": "playerKilled", "data": {"targets": [1]}}
{"type": "gameOver", "data": {"result": "victory/defeat", "winningSide": "village", "reason": "好人阵营获胜,你失败了!"}}

239
index.ts Normal file
View File

@ -0,0 +1,239 @@
import express from 'express';
import cors from 'cors';
import { Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import Readline from 'readline';
import { createHash } from 'crypto';
import ttsSdk from "microsoft-cognitiveservices-speech-sdk"
import { JSDOM } from 'jsdom';
const app = express();
const PORT = process.env.PORT || 3000;
// 启用CORS
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
app.use(express.json());
app.use('/audio', express.static(path.join(__dirname, 'audio')));
// 从文本文件读取游戏事件
const prompts = [
{ voice: "zh-CN-XiaoxiaoNeural" },
{ voice: "zh-CN-YunxiNeural" },
{ voice: "zh-CN-XiaohanNeural" },
{ voice: "zh-CN-YunjianNeural" },
{ voice: "zh-CN-XiaoyiNeural" },
{ voice: "zh-CN-XiaochenNeural" },
]
/* const prompts = [
{ voice: "zh-CN-XiaoxiaoNeural" },
{ voice: "zh-CN-XiaochenNeural" },
{ voice: "zh-CN-YunyangNeural" },
{ voice: "zh-CN-XiaohanNeural" },
{ voice: "zh-CN-XiaomengNeural" },
{ voice: "zh-CN-XiaomoNeural" }
] */
const readEventsFromFile = async (filePath: string): Promise<any[]> => {
let lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(line => line.trim() !== '').map(line => JSON.parse(line));
for (const line of lines) {
if (line.type == "speak" || line.type == "wolfChat") {
let promptIndex = line.data.player - 1;
if (promptIndex < 0 || promptIndex >= prompts.length) {
console.error(`Invalid prompt index ${promptIndex} for line ${JSON.stringify(line)}`);
process.exit(1);
}
let speech = await resolveSpeech(line.data.message, prompts[promptIndex]!.voice)
line.data.voice = speech.voice;
line.data.message = speech.text;
}
}
return lines;
}
function createResolvablePromise() {
let resolve;
const promise = new Promise(r => {
resolve = r;
});
return { promise, resolve };
}
let continueController = createResolvablePromise();
function waitForContinue() {
return continueController.promise;
}
const rl = Readline.createInterface({
input: process.stdin,
output: process.stdout
});
function setupEnterKeyListener() {
console.log("按回车键继续...");
rl.once('line', (input) => {
// 解析等待的 Promise
//@ts-ignore
continueController.resolve(input || 'continued');
continueController = createResolvablePromise();
});
}
app.post('/*', (req: Request, res: Response) => {
res.end('ok')
})
const filePath = path.join(__dirname, 'events_0.jsonl');
const events = await readEventsFromFile(filePath);
app.get('/:gameId/events', async (req: Request, res: Response) => {
const gameId = req.params.gameId;
let disconnected = false;
// 设置SSE响应头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
const filePath = path.join(__dirname, 'events_0.jsonl');
const events = await readEventsFromFile(filePath);
console.log(`客户端已连接到游戏${gameId},准备发送${events.length}个事件`);
let eventIndex = 0;
for (let i = 0; i < events.length; i++) {
if (disconnected) break
const event = events[i];
setupEnterKeyListener(); // 设置监听回车
await waitForContinue();
res.write(`data: ${JSON.stringify(event)}\n\n`);
res.socket?.write('');
console.log(`发送事件 ${eventIndex + 1}/${events.length}:`, event.type);
eventIndex++;
}
// 处理客户端断开连接
req.on('close', () => {
console.log(`客户端已断开连接`);
disconnected = true;
});
});
app.listen(PORT, () => {
console.log(`服务器启动,监听端口 ${PORT}`);
});
async function resolveSpeech(raw: string, voice: string): Promise<{
voice: string,
text: string
}> {
const wrappedXml = `<root>${raw}</root>`;
// 使用JSDOM解析XML
const dom = new JSDOM(wrappedXml, { contentType: 'text/xml' });
const document = dom.window.document;
const scriptElements: ({
type: 'action',
content: string
} | {
type: 'audio',
style: string | null,
content: string,
text: string,
segments: string[]
})[] = [];
document.querySelectorAll('action, audio').forEach(el => {
if (el.tagName.toLowerCase() === 'action') {
scriptElements.push({
type: 'action',
content: el.textContent?.trim() || ""
});
} else if (el.tagName.toLowerCase() === 'audio') {
const style = el.getAttribute('style');
const content = el.textContent?.trim() || "";
scriptElements.push({
type: 'audio',
style,
content: content,
text: content.split('|').join(''),
segments: content.split('|')
});
}
});
let ssml = `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="${voice}">
${scriptElements.filter(el => el.type === 'audio')
.map(el => `<mstts:express-as style="${el.style}" styledegree="1">${el.segments.join('')}</mstts:express-as>`)
.join('<break strength="medium" />')}
</voice>
</speak>`
const fileName = createHash('sha256')
.update(ssml)
.digest('hex');
const filePath = path.join(__dirname, `audio/${fileName}.mp3`);
console.log(`${fileName} -> ${scriptElements.filter(el => el.type === 'audio')
.map(el => `${el.segments.join('')}`)
.join('')} `);
let clientText = scriptElements.map(el => el.type == 'action' ? `<span class="action">(${el.content})</span>` : el.content).join(' ');
if (fs.existsSync(filePath)) {
return {
voice: fileName,
text: clientText
};
}
const speechConfig = ttsSdk.SpeechConfig.fromSubscription(
"6KW9CQeOlrSIHTodWJoiVD3aEtbbek3GwsKIbTfXBmVgngz2te0dJQQJ99BDACYeBjFXJ3w3AAAAACOGWMAe",
"eastus"
);
speechConfig.speechSynthesisVoiceName = voice;
const audioConfig = ttsSdk.AudioConfig.fromAudioFileOutput(filePath);
const synthesizer = new ttsSdk.SpeechSynthesizer(speechConfig, audioConfig);
console.log(`正在合成语音: ${ssml} - ${voice} - ${fileName}`);
return new Promise((resolve, reject) => {
synthesizer.speakSsmlAsync(ssml,
function (result) {
if (result.reason === ttsSdk.ResultReason.SynthesizingAudioCompleted) {
console.log("synthesis finished.");
synthesizer.close();
resolve({
voice: fileName,
text: clientText
});
} else {
console.error("Speech synthesis canceled, " + result.errorDetails);
synthesizer.close();
reject(new Error(result.errorDetails || "Speech synthesis failed"));
}
},
function (err) {
console.trace("err - " + err);
synthesizer.close();
reject(err);
});
});
}

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "backend-test",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"dependencies": {
"@types/jsdom": "^21.1.7",
"@types/xml2js": "^0.4.14",
"cors": "^2.8.5",
"express": "^4.18.2",
"jsdom": "^26.0.0",
"microsoft-cognitiveservices-speech-sdk": "^1.43.1",
"openai": "^4.92.0",
"readline": "^1.3.0",
"xml2js": "^0.6.2"
},
"peerDependencies": {
"typescript": "^5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21"
}
}

28
tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["esnext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": false,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}