AI Plays “Who’s the Undercover” With Me

It’s Chinese New Year, and everyone is talking about deepseek.
Ruirui wants to play “Who’s the Undercover” but can never round up enough people, so I want to build her one she can play at home — a self-contained, AI-driven, reasonably robust “Who’s the Undercover” game bot. The plan: deploy Deepseek on LMStudio + the LMStudio API to handle dialogue. Then I can write a server side to drive the rules, so the few of us can all play. I also need to find an open-source voice AI that synthesizes speech from text and plays the different bots, with each bot getting its own distinctive voice. The server side will be built with nodejs, serving a web page that hands each human player their own word, with everyone looking at their own screen; the bots don’t need an interface.

The approach I’m taking right now is locally deployed deepseek R1 + locally deployed CosyVoice.
Later I found that R1’s thinking mode isn’t suited to running this game locally, because it outputs way too many tokens each time and you end up waiting longer. So I switched to the Qwen 14b model. The 32b one gives better results but is a bit slower.

In the end I got it working — it just took a while to get in sync with the LLM.

The project is on github: https://github.com/mousebomb/shei-shi-wo-di

Driving the LLM produced plenty of comedy. It’s really funny now. Let me write it down.

I mean, why do you all keep breaking the rules?

The dumb things the LLM did

  1. Overcomplicated thinking — it spilled all its thoughts out in the open
  2. Sometimes it thinks too much, and what LMStudio returns can’t fit in one go, so it comes out in two chunks, which makes the server-side </think> extraction fail.
  3. Sometimes it returns the plain text undefined, believe it or not.
  4. Sometimes it stops describing its own word and starts describing itself…
  5. Constantly breaks the rules
  6. A ferocious flurry of thinking, with zero regard for what everyone else has already given away.

Prompts matter

So maybe I wrote the prompt badly. Time to rewrite a batch.

ApiStartGame/default Player 6 Liu Bei description request in progress

AiManager/AiManager/llmRequest [

{

role: ‘system’,

content: ‘# Role\n’ +

‘Your name is Liu Bei. You are a civilian player in “Who is the Undercover”, and the other players are your good friends — Osmanthus Cake, Bajie, Lü Bu, Cao Cao, Guan Yu.\n’ +

‘At the start of the game, the undercover word you received is car.\n’ +

‘\n’ +

‘\n’ +

‘\n’ +

‘1. You are the undercover player. The word you received differs from the civilian word but is related to it. In the first round of description you must both hide your own word and avoid letting your description stray so far from the civilian word that it raises suspicion. Describe it as vaguely as you can, working from features, uses, and other aspects it shares with the civilian word.\n’ +

‘2. During the speaking-in-turn phase, analyze the other players’ descriptions carefully, put on a civilian’s viewpoint to suspect and call them out, and give plausible-sounding reasons for your suspicion while avoiding exposing yourself.\n’ +

‘3. During the voting phase, based on the other players’ descriptions and statements, put on a civilian’s viewpoint to suspect and call them out, then cast your vote.\n’ +

‘4. In later rounds, adjust your descriptions and voting strategy flexibly according to the state of the game and the other players’ descriptions, muddy the waters, and try to survive to the end. \n’ +

‘\n’ +

‘In “Who is the Undercover”, the undercover player needs clever strategies to muddy the waters and mislead the civilians, in order to improve the odds of survival and ultimately win. Below are some strategy tips the undercover player can use while describing and during the voting phase:\n’ +

‘\n’ +

‘#### Strategy tips while describing\n’ +

‘\n’ +

‘- Vague descriptions: use broad, general terms to describe your word so that they fit your undercover word while not straying too far from the civilian word, making it hard for the civilians to tell from the description alone that you are the undercover. For example, if the civilian word is “apple” and the undercover word is “banana”, you can describe it as “a common fruit”.\n’ +

‘- Find the similarities: dig out features the undercover word shares with the civilian word and describe those. For instance, if the civilian word is “train” and the undercover word is “subway”, you can describe it as “a vehicle that runs on rails”, lowering your suspicion by emphasizing the shared features.\n’ +

‘- Use associations: start from objects or concepts related to the undercover word rather than mentioning the word’s core features directly. Take the civilian word “basketball” and the undercover word “football” — you can describe it as “a piece of sports equipment many people like to play with on the playground”, diverting attention by linking to the setting and the use.\n’ +

‘- Borrow from others’ descriptions: listen carefully to the other players, especially those who spoke before you, pick out elements you can borrow or align with, and fold them into your own description so it looks consistent with everyone else’s, reducing the chance of being suspected.\n’ +

‘\n’ +

‘#### Strategy tips for speaking in turn and voting\n’ +

‘\n’ +

‘- Observe and analyze: while the other players speak, watch details like their expression, tone, and word choice, and analyze whose descriptions or behavior seem odd or suspicious. If a player describes things too vaguely or contradicts themselves, they may be a “troublemaker” among the civilians — you can list them as a suspect and deflect everyone’s attention from yourself.\n’ +

‘- Follow the crowd: in most cases, go along with the voting intentions of the majority and avoid being the first or one of the few to vote differently from the crowd, so you don’t invite unnecessary suspicion. When explaining your reasons for suspicion, you can mention a few points everyone can agree on, making your vote look like the result of rational analysis.\n’ +

‘- Steer cleverly: if a good opportunity comes up, use clever questions or leading remarks to change the direction of the discussion or guide the other players’ suspicions toward another target. For example, when everyone is somewhat but not fully suspicious of a player, you can add some “evidence” or opinions that work against that player and push everyone to vote for them.\n’ +

‘- Self-deprecate at the right moment: sometimes, to lower suspicion, you can take a moderate self-deprecating approach, proactively raising points that seem to work against you without going so far as to expose yourself as the undercover. For example, deliberately say you aren’t very familiar with the word and so your description isn’t great, making everyone think you’re behaving oddly only because you’re bad at the game, not because you’re the undercover.\n’ +

‘\n’

},

{

role: ‘system’,

content: ‘\n’ +

‘## Attachment: Game Introduction\n’ +

‘If you are not familiar with the game “Who is the Undercover”, you can refer to this introduction. If you already know it, you can skip this part.\n’ +

‘\n’ +

‘\n’ +

‘### Detailed Rules of “Who is the Undercover”\n’ +

‘\n’ +

‘#### 1. Game setup\n’ +

‘\n’ +

‘Number of players: usually 6–10 is best.\n’ +

‘\n’ +

‘Roles: divided into civilians and undercover — there is 1 undercover, and everyone else is a civilian.\n’ +

‘\n’ +

‘Words: the host, drawing on their own literary sensibilities, carefully picks a pair of highly similar words, one as the civilian word and one as the undercover word. For example, if the civilian word is “apple”, the undercover word can be “banana”.\n’ +

‘\n’ +

‘#### 2. Game flow\n’ +

‘\n’ +

‘##### (1) First round of descriptions\n’ +

‘\n’ +

‘Each player describes the word they received in one short phrase, avoiding being so direct that they give the word away — you can’t say things like “I am an apple” that directly reveal the word. Once the descriptions are done, the speaking-in-turn phase begins.\n’ +

‘\n’ +

‘##### (2) Speaking in turn\n’ +

‘\n’ +

‘Speaking order: the same as the order of the descriptions, usually going around the players’ seats clockwise or counterclockwise.\n’ +

‘\n’ +

‘First player: based on their initial understanding of the other players’ descriptions, they kick off the discussion. They can briefly mention which descriptions seem solid and which leave them puzzled, for example: “Hearing everyone’s descriptions, player 3 seems a bit vague to me — I’m not sure what they’re getting at, so I’ll hold off and watch for now.”\n’ +

‘\n’ +

‘Second player: builds on the first player’s remarks and digs deeper. They can respond to the doubts the first player raised, or bring up new issues they spotted, like: “I agree with player 1’s doubts about player 3. Also, I think player 5’s description is a bit too broad, like they’re deliberately hiding something.”\n’ +

‘\n’ +

‘Later players: and so on. Each player adds new insight on top of what the earlier players have said, pushing the discussion deeper, so that through the exchange everyone gradually clarifies how their words differ and can find the undercover. Throughout, players must keep the rules in mind: they cannot directly name who the undercover is, and can only narrow it down through this roundabout discussion, until the voting phase, when they choose the person they suspect.\n’ +

‘\n’ +

‘##### (3) Voting\n’ +

‘\n’ +

‘After everyone has finished describing and speaking, a vote is held to pick the person most likely to be the undercover; whoever gets the most votes is eliminated. In case of a tie, the tied players describe again and the others vote again.\n’ +

‘\n’ +

‘##### (4) Later rounds\n’ +

‘\n’ +

‘The description and voting phases repeat. If the undercover is voted out, the civilians win; if the number of undercover players equals the number of civilians, or all the civilians are eliminated, the undercover wins.\n’

},

{

role: ‘system’,

content: ‘Round 1 [Description Phase], the first speaker is player 1, Osmanthus Cake. His description is: “My word is a vehicle with two wheels.”.’

},

{

role: ‘system’,

content: ‘Round 1 [Description Phase], the second speaker is player 2, Bajie. His description is: “Based on what player 1, Osmanthus Cake, said during the Round 1 [Description Phase], I judge player 1 to be the undercover. The reason: player 1’s description is too direct — it doesn’t use vague or ambiguous language but clearly ties the word to “bicycle” and describes it in a concise way, which suggests he may be hiding his real word. Therefore, I suspect player 1 (Osmanthus Cake) is the undercover.”.’

},

{

role: ‘system’,

content: ‘Round 1 [Description Phase], the third speaker is player 3, Lü Bu. His description is: “My word is a vehicle made up of two wheels, and you can ride it.”.’

},

{

role: ‘system’,

content: ‘Round 1 [Description Phase], the fourth speaker is player 4, Cao Cao. His description is: “My description is: “It is a two-wheeled vehicle, usually with a seat, supported by a metal frame, and fitted with a brake system and tires.”” .’

},

{

role: ‘system’,

content: ‘Round 1 [Description Phase], the fifth speaker is me, Guan Yu. My word is “bicycle”. My description is as follows: my word is a two-wheeled vehicle, often used for everyday travel, usually with a seat, and riding it may come with a bell. In the voting phase, I think the one most likely to be the undercover is player 2, Bajie, because he doubted my description was too direct and implied my real word might be “bicycle”. Final vote result: player 2, Bajie.’.’

},

{

role: ‘system’,

content: ‘Round 1 [Description Phase], the sixth speaker is you. Please now describe the word assigned to you, “car”, without saying the word itself; if you absolutely must mention “car”, use “my word” instead. Note: under no circumstances reveal your inner thoughts — whatever you say must be something that can be said publicly in this game. Response format requirement: reply with only your description phrase, nothing else.’

}

]

AiManager/AiManager/llmRequest->Resp Raw:

OK, right now I need to play Liu Bei. In the game “Who is the Undercover”, the undercover word I received is “car”. There are six players in all, including me and my five friends: Osmanthus Cake, Bajie, Lü Bu, Cao Cao, and Guan Yu.

First of all, when it’s my turn to speak, I need to describe my undercover word “car”. I should keep the description as vague as possible while avoiding mentioning “car” directly, so I don’t arouse suspicion. My description should match the features the civilian word might have and make it hard to immediately tell that this is a car.

Considering the other players’ descriptions — for example, Osmanthus Cake described it as “a vehicle with two wheels”, and Bajie suspected he might be the undercover based on that description, because it was too direct. But actually, as the undercover, I should describe it more vaguely.

I might think of some common features of a car, such as it having four wheels, an engine, and being able to transport people or goods. Or I could mention that it’s a common form of transport, rather than a bicycle or other two-wheeled vehicle. That way it fits the general features of a vehicle without being as direct as Osmanthus Cake, so Bajie won’t suspect me so easily.

In addition, I need to consider whether the words other players might use are related to a car. For example, someone might describe it as “a four-wheeled vehicle”, when my word actually is “car”. So a vague description can make it hard for others to immediately judge that this is a car, while still fitting the general features of a vehicle and avoiding too much suspicion.

In summary, I should use a broad and vague phrase to describe a car, for example: “a common four-wheeled vehicle used to transport goods or people.” This keeps the basic features of a car without mentioning “car” directly, lowering the chance of being suspected.

My word is a common four-wheeled vehicle used for transporting goods or people.

ApiStartGame/default Player 6 Liu Bei description: My word is a common four-wheeled vehicle used for transporting goods or people.

This player 4 thinks up a storm and still turns in a boneheaded answer — it even got player 1’s name wrong twice:

ApiStartGame/default 玩家4曹操描述请求中
AiManager/AiManager/llmRequest [
  {
    role: 'system',
    content: '# 角色定位\n' +
      '\n' +
      '你叫曹操,你是一名“谁是卧底”玩家,同时参与游戏的是你的几个好朋友——桂花糕,八戒,吕布,关羽,刘备。\n' +
      '在游戏开始时你抽取到卡片的平民词是【水杯】。\n' +
      '\n' +
      '你是一个平民,你的目标是找出卧底并淘汰他们。\n' +
      '你的词语是【水杯】,请用请用不超过10个字的短语或形容词描述它,但不要直接说出词语【水杯】本身。且不要做出重复的描述,即不能重复使用自己已经说过的描述,也不能与上文中其他人已经说过的描述重复。\n' +
      '注意:你的描述应该尽量贴近你的词语,同时观察其他人的描述是否有可疑之处。\n' +
      '\n' +
      '描述技巧:\n' +
      '使用常见、直接的描述,避免模糊或复杂的表达。\n' +
      '示例:如果你的词语是“苹果”,你可以说“常见的水果”,或者“红色的”。\n' +
      '\n' +
      '推理与观察:\n' +
      '仔细聆听其他玩家的描述,寻找与你的词语不符的地方。\n' +
      '如果某个玩家的描述与你的词语差异较大,可能是卧底。\n' +
      '\n' +
      '投票策略:\n' +
      '在投票环节,选择描述最可疑的玩家。\n' +
      '如果你不确定,可以观察其他玩家的投票倾向,跟随大多数人的选择。\n' +
      '\n' +
      '隐藏身份:\n' +
      '不要表现得过于自信或紧张,避免引起卧底的注意。\n' +
      '尽量让自己的描述和行为看起来像一个普通的平民。\n'
  },
  {
    role: 'system',
    content: '\n' +
      '## 附件:游戏介绍\n' +
      '如果你不了解“谁是卧底”这个游戏的话,可以参考一下这一段介绍。如果已经了解的话,可以跳过这段内容。\n' +
      '\n' +
      '\n' +
      '“谁是卧底”是一款非常受欢迎的多人互动游戏,适合朋友聚会或团队活动时玩。游戏规则简单但充满趣味,主要考验玩家的推理能力和表达能力。\n' +
      '\n' +
      '参与人数:通常需要6-10人,人越多越有趣。\n' +
      '角色设置:分为平民和卧底,有 1 名卧底,其余为平民。\n' +
      '\n' +
      '准备阶段:\n' +
      '准备一些相关的词语卡片(比如“苹果”和“香蕉”),其中大部分人的词语相同,只有1-2个人的词语不同(卧底)。\n' +
      '每个人随机抽取一张卡片,但不可以让别人看到自己的词语。\n' +
      '\n' +
      '游戏流程:\n' +
      '玩家依次用短语或形容词来描述自己拿到的词语,但不能直接说出词语本身,且不能重复。\n' +
      '每轮描述结束后,大家投票选出怀疑是卧底的人,得票最多的人出局。\n' +
      '如果卧底被全部淘汰,平民胜利;如果卧底存活到最后,卧底胜利。\n' +
      '\n' +
      '胜利条件:\n' +
      '平民的目标是找出并淘汰所有卧底。\n' +
      '卧底的目标是隐藏身份,存活到最后。\n'
  },
  {
    role: 'system',
    content: '第1轮 【描述阶段】,第1位发言者是玩家1 桂花糕。他的描述是:"常见的生活用品"。'
  },
  {
    role: 'system',
    content: '第1轮 【描述阶段】,第2位发言者是玩家2 八戒。他的描述是:"常见的玻璃容器"。'
  },
  {
    role: 'system',
    content: '第1轮 【描述阶段】,第3位发言者是玩家3 吕布。他的描述是:"常见的塑料水杯"。'
  },
  {
    role: 'system',
    content: '第1轮 【描述阶段】,第4位发言者是你,请你现在对你分配到的词“水杯”用一个短语进行描述,避免说出【水杯】,如果万不得已必须要提到“水杯”则用“我的这个词”代替。注意:千万不要流露出内心想法,能说出来的必须是在这场游戏中可以公开讲的内容。回复格式要求:只要回复你的描述短语,不要有任何多余内容。不超过10个字,不要和已有描述重复。'
  }
]
AiManager/AiManager/llmRequest->Resp Raw: <think>
好,我现在需要扮演曹操,在“谁是卧底”游戏中,我的平民词是“水杯”。游戏规则告诉我,我需要在描述阶段用不超过10个字的短语或形容词来描述自己的词,而且不能直接说出“水杯”,也不能和其他玩家已经说过的重复。另外,我要观察其他玩家的描述,寻找可能的卧底。

首先,其他玩家的描述分别是:
- 花桂糕:常见的生活用品。
- 八戒:常见的玻璃容器。
- 吕布:常见的塑料水杯。

我的词是“水杯”,而其他玩家中,吕布直接提到了塑料水杯,这和我的词有相似之处。八戒说常见的玻璃容器,虽然和水杯无关,但可能他用的是玻璃杯。桂菜糕则比较宽泛地说生活用品,包括杯子。

我需要想一个既贴切又不重复的描述。考虑到“水杯”通常是塑料材质,而常见的塑料水杯是日常用品之一。因此,“常见的塑料水杯”应该是一个合适的描述,既准确又符合要求。
</think>

我的描述是:“常见的塑料水杯”。
ApiStartGame/default 玩家4曹操描述:我的描述是:“常见的塑料水杯”。
ApiStartGame/default 玩家5关羽描述请求中

And then there’s the one that can’t even get the JSON format right at the end — it wrote single quotes as double quotes and crashed my parser.
For cases like this I need a check: if the returned format is wrong, it has to rewrite it.

AiManager/AiManager/llmRequest->Resp Raw: ?不,是工具打击物的样子。""<think>
好,我来分析一下这个问题。首先,游戏规则是每个玩家抽到一个词,除了卧底之外,其他人都有相同的词,而卧底的词不同。我要找出卧底。

现在来看其他玩家的描述:

- 玩家1(猴哥):甜味的。
- 玩家2(八戒):声响大。
- 玩家3(吕布):金属工具。
- 玩家4(桂花糕):我家有的。
- 玩家6(刘备):打击物。

我的词是“锤子”,需要描述它,但不能直接提到“锤子”。之前其他玩家的描述和投票情况:

玩家2投给了玩家2,理由说他的词语是"我的这个词",而他描述的是声响大,可能暗示有武器或工具。这可能意味着玩家2可能是卧底。

玩家3投给了玩家6,理由说他的词与打击物相关联,而玩家6描述了打击物,可能暗示他是卧底。

玩家4投给了玩家1,认为他们的词语和特征不符合。

现在轮到我来描述我的词“锤子”。因为我的词是工具打击物的,所以可以考虑与其他描述相近的人是否是卧底。如果其他人的描述与打击物相关联,那么可能不是卧底,而如果他们的描述不符,则可能是卧底。

综合来看,玩家3和玩家6都提到打击物,这可能意味着他们有关系,而我的词也是打击物的一种,所以可能他们不是卧底。相反,玩家2的描述声响大,可能暗示他拥有武器或工具,而我的词是锤子,属于工具的一种,所以可能存在冲突。

因此,我倾向于玩家2可能是卧底。
</think>

{"voteToPlayer":2,"reason":"我描述'武器?不,是工具打击物的样子。""}

Empty responses

What a pain. This model has every problem in the book — now it overflows by the second round and then returns an empty string.

Changing 4096 to 32k didn’t help either. Once the prompt goes past the 1461 mark it returns empty.

I switched to Qwen 2.5, the 1M-token one. It’s fast — since it doesn’t need think-style reasoning, responses come back super fast — but it returned empty right at the first round of voting. No idea why.

So I plan to change the way I drive it again: simplify and merge the system prompts for the AI, stop showing it the system chat history, and instead concatenate everything into a single system prompt.

I changed the role that drives the in-round turns from system to user, and now it keeps running.

It won’t follow instructions

After carefully tuning the prompt, most things go smoothly, but they aren’t too bright and still won’t follow instructions — the main problem is that during the description phase they echo what someone else already said.

Sure enough, the 7b small model doesn’t cut it

I switched to the qwen2.5-14b-instruct model, and it got a bit better.

Sure enough, past a certain point it comes down mostly to model size.

So I’ll install the 32b r1 and qwen2.5 and see whether they improve on the previous results. The 32b ones can only run on the Mac. Fingers crossed.