This commit is contained in:
raykkk
2025-10-17 21:40:45 +08:00
commit 7d0451131f
155 changed files with 14873 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
# 🐺⚔️👨‍🌾 Nine-Player Werewolves Game
This is a nine-players werewolves game example built using AgentScope, showcasing **multi-agent interactions**,
**role-based gameplay**, and **structured output handling**.
Specifically, this game is consisted of
- three villagers 👨‍🌾,
- three werewolves 🐺,
- one seer 🔮,
- one witch 🧙‍♀️ and
- one hunter 🏹.
## ✨Changelog
- 2025-10: We update the example to support more features:
- Allow the dead players to leave messages.
- Support Chinese now.
- Support **continuous gaming** by loading and saving session states, so the same agents can play multiple games and continue learning and optimizing their strategies.
## QuickStart
Run the following command to start the game, ensuring you have set up your DashScope API key as an environment variable.
```bash
python main.py
```
> Note:
> - You can adjust the language, model and other parameters in `main.py`.
> - Different models may yield different game experiences.
Running the example with AgentScope Studio provides a more interactive experience.
- Demo Video in Chinese (click to play):
[![Werewolf Game in Chinese](https://img.alicdn.com/imgextra/i3/6000000007235/O1CN011pK6Be23JgcdLWmLX_!!6000000007235-0-tbvideo.jpg)](https://cloud.video.taobao.com/vod/KxyR66_CWaWwu76OPTvOV2Ye1Gas3i5p4molJtzhn_s.mp4)
- Demo Video in English (click to play):
[![Werewolf Game in English](https://img.alicdn.com/imgextra/i3/6000000007389/O1CN011alyGK24SDcFBzHea_!!6000000007389-0-tbvideo.jpg)](https://cloud.video.taobao.com/vod/bMiRTfxPg2vm76wEoaIP2eJfkCi8CUExHRas-1LyK1I.mp4)
## Details
The game is built with the ``ReActAgent`` in AgentScope, utilizing its ability to generate structured outputs to
control the game flow and interactions.
We also use the ``MsgHub`` and pipelines in AgentScope to manage the complex interactions like discussion and voting.
It's very interesting to see how agents play the werewolf game with different roles and objectives.
# Advanced Usage
## Change Language
The game is played in English by default. Just uncomment the following line in `game.py` to switch to Chinese.
```python
# from prompt import ChinesePrompts as Prompts
```
## Play with Agents
You can replace one of the agents with a `UserAgent` to play with AI agents.
## Change Models
Just modify the `model` parameter in `main.py` to try different models. Note you need to change the formatter at the same time to match the model's output format.
## Further Reading
- [Structured Output](https://doc.agentscope.io/tutorial/task_agent.html#structured-output)
- [MsgHub and Pipelines](https://doc.agentscope.io/tutorial/task_pipeline.html)
- [Prompt Formatter](https://doc.agentscope.io/tutorial/task_prompt.html)
- [AgentScope Studio](https://doc.agentscope.io/tutorial/task_studio.html)

View File

@@ -0,0 +1,343 @@
# -*- coding: utf-8 -*-
# pylint: disable=too-many-branches, too-many-statements, no-name-in-module
"""A werewolf game implemented by agentscope."""
import numpy as np
from agentscope.agent import ReActAgent
from agentscope.pipeline import MsgHub, fanout_pipeline, sequential_pipeline
from prompt import EnglishPrompts as Prompts
from utils import (
MAX_DISCUSSION_ROUND,
MAX_GAME_ROUND,
EchoAgent,
Players,
majority_vote,
names_to_str,
)
from .structured_model import (
DiscussionModel,
WitchResurrectModel,
get_hunter_model,
get_poison_model,
get_seer_model,
get_vote_model,
)
# Uncomment the following line to use Chinese prompts
# from prompt import ChinesePrompts as Prompts
moderator = EchoAgent()
async def hunter_stage(
hunter_agent: ReActAgent,
players: Players,
) -> str | None:
"""Because the hunter's stage may happen in two places: killed at night
or voted during the day, we define a function here to avoid duplication."""
global moderator
msg_hunter = await hunter_agent(
await moderator(Prompts.to_hunter.format(name=hunter_agent.name)),
structured_model=get_hunter_model(players.current_alive),
)
if msg_hunter.metadata.get("shoot"):
return msg_hunter.metadata.get("name", None)
return None
async def werewolves_game(agents: list[ReActAgent]) -> None:
"""The main entry of the werewolf game
Args:
agents (`list[ReActAgent]`):
A list of 9 agents.
"""
assert len(agents) == 9, "The werewolf game needs exactly 9 players."
# Init the players' status
players = Players()
# If the witch has healing and poison potion
healing, poison = True, True
# If it's the first day, the dead can leave a message
first_day = True
# Broadcast the game begin message
async with MsgHub(participants=agents) as greeting_hub:
await greeting_hub.broadcast(
await moderator(
Prompts.to_all_new_game.format(names_to_str(agents)),
),
)
# Assign roles to the agents
roles = ["werewolf"] * 3 + ["villager"] * 3 + ["seer", "witch", "hunter"]
np.random.shuffle(agents)
np.random.shuffle(roles)
for agent, role in zip(agents, roles):
# Tell the agent its role
await agent.observe(
await moderator(
f"[{agent.name} ONLY] {agent.name}, your role is {role}.",
),
)
players.add_player(agent, role)
# Printing the roles
players.print_roles()
# GAME BEGIN!
for _ in range(MAX_GAME_ROUND):
# Create a MsgHub for all players to broadcast messages
async with MsgHub(
participants=players.current_alive,
enable_auto_broadcast=False, # manual broadcast only
name="alive_players",
) as alive_players_hub:
# Night phase
await alive_players_hub.broadcast(
await moderator(Prompts.to_all_night),
)
killed_player, poisoned_player, shot_player = None, None, None
# Werewolves discuss
async with MsgHub(
players.werewolves,
enable_auto_broadcast=True,
announcement=await moderator(
Prompts.to_wolves_discussion.format(
names_to_str(players.werewolves),
names_to_str(players.current_alive),
),
),
name="game_werewolves",
) as werewolves_hub:
# Discussion
n_werewolves = len(players.werewolves)
for _ in range(1, MAX_DISCUSSION_ROUND * n_werewolves + 1):
res = await players.werewolves[_ % n_werewolves](
structured_model=DiscussionModel,
)
if _ % n_werewolves == 0 and res.metadata.get(
"reach_agreement",
):
break
# Werewolves vote
# Disable auto broadcast to avoid following other's votes
werewolves_hub.set_auto_broadcast(False)
msgs_vote = await fanout_pipeline(
players.werewolves,
msg=await moderator(content=Prompts.to_wolves_vote),
structured_model=get_vote_model(players.current_alive),
enable_gather=False,
)
killed_player, votes = majority_vote(
[_.metadata.get("vote") for _ in msgs_vote],
)
# Postpone the broadcast of voting
await werewolves_hub.broadcast(
[
*msgs_vote,
await moderator(
Prompts.to_wolves_res.format(votes, killed_player),
),
],
)
# Witch's turn
await alive_players_hub.broadcast(
await moderator(Prompts.to_all_witch_turn),
)
msg_witch_poison = None
for agent in players.witch:
# Cannot heal witch herself
msg_witch_resurrect = None
if healing and killed_player != agent.name:
msg_witch_resurrect = await agent(
await moderator(
Prompts.to_witch_resurrect.format(
witch_name=agent.name,
dead_name=killed_player,
),
),
structured_model=WitchResurrectModel,
)
if msg_witch_resurrect.metadata.get("resurrect"):
killed_player = None
healing = False
# Has poison potion and hasn't used the healing potion
if poison and not (
msg_witch_resurrect
and msg_witch_resurrect.metadata["resurrect"]
):
msg_witch_poison = await agent(
await moderator(
Prompts.to_witch_poison.format(
witch_name=agent.name,
),
),
structured_model=get_poison_model(
players.current_alive,
),
)
if msg_witch_poison.metadata.get("poison"):
poisoned_player = msg_witch_poison.metadata.get("name")
poison = False
# Seer's turn
await alive_players_hub.broadcast(
await moderator(Prompts.to_all_seer_turn),
)
for agent in players.seer:
msg_seer = await agent(
await moderator(
Prompts.to_seer.format(
agent.name,
names_to_str(players.current_alive),
),
),
structured_model=get_seer_model(players.current_alive),
)
if msg_seer.metadata.get("name"):
player = msg_seer.metadata["name"]
await agent.observe(
await moderator(
Prompts.to_seer_result.format(
agent_name=player,
role=players.name_to_role[player],
),
),
)
# Hunter's turn
for agent in players.hunter:
# If killed and not by witch's poison
if (
killed_player == agent.name
and poisoned_player != agent.name
):
shot_player = await hunter_stage(agent, players)
# Update alive players
dead_tonight = [killed_player, poisoned_player, shot_player]
players.update_players(dead_tonight)
# Day phase
if len([_ for _ in dead_tonight if _]) > 0:
await alive_players_hub.broadcast(
await moderator(
Prompts.to_all_day.format(
names_to_str([_ for _ in dead_tonight if _]),
),
),
)
# The killed player leave a last message in first night
if killed_player and first_day:
msg_moderator = await moderator(
Prompts.to_dead_player.format(killed_player),
)
await alive_players_hub.broadcast(msg_moderator)
# Leave a message
last_msg = await players.name_to_agent[killed_player]()
await alive_players_hub.broadcast(last_msg)
else:
await alive_players_hub.broadcast(
await moderator(Prompts.to_all_peace),
)
# Check winning
res = players.check_winning()
if res:
await moderator(res)
break
# Discussion
await alive_players_hub.broadcast(
await moderator(
Prompts.to_all_discuss.format(
names=names_to_str(players.current_alive),
),
),
)
# Open the auto broadcast to enable discussion
alive_players_hub.set_auto_broadcast(True)
await sequential_pipeline(players.current_alive)
# Disable auto broadcast to avoid leaking info
alive_players_hub.set_auto_broadcast(False)
# Voting
msgs_vote = await fanout_pipeline(
players.current_alive,
await moderator(
Prompts.to_all_vote.format(
names_to_str(players.current_alive),
),
),
structured_model=get_vote_model(players.current_alive),
enable_gather=False,
)
voted_player, votes = majority_vote(
[_.metadata.get("vote") for _ in msgs_vote],
)
# Broadcast the voting messages together to avoid influencing
# each other
voting_msgs = [
*msgs_vote,
await moderator(
Prompts.to_all_res.format(votes, voted_player),
),
]
# Leave a message if voted
if voted_player:
prompt_msg = await moderator(
Prompts.to_dead_player.format(voted_player),
)
last_msg = await players.name_to_agent[voted_player](
prompt_msg,
)
voting_msgs.extend([prompt_msg, last_msg])
await alive_players_hub.broadcast(voting_msgs)
# If the voted player is the hunter, he can shoot someone
shot_player = None
for agent in players.hunter:
if voted_player == agent.name:
shot_player = await hunter_stage(agent, players)
if shot_player:
await alive_players_hub.broadcast(
await moderator(
Prompts.to_all_hunter_shoot.format(
shot_player,
),
),
)
# Update alive players
dead_today = [voted_player, shot_player]
players.update_players(dead_today)
# Check winning
res = players.check_winning()
if res:
async with MsgHub(players.all_players) as all_players_hub:
res_msg = await moderator(res)
await all_players_hub.broadcast(res_msg)
break
# The day ends
first_day = False
# Game over, each player reflects
await fanout_pipeline(
agents=agents,
msg=await moderator(Prompts.to_all_reflect),
)

View File

@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
# flake8: noqa: E501
"""The main entry point for the werewolf game."""
import asyncio
import os
from agentscope.agent import ReActAgent
from agentscope.formatter import DashScopeMultiAgentFormatter
from agentscope.model import DashScopeChatModel
from agentscope.session import JSONSession
from game import werewolves_game
def get_official_agents(name: str) -> ReActAgent:
"""Get the official game_werewolves game agents."""
agent = ReActAgent(
name=name,
sys_prompt=f"""You're a werewolf game player named {name}.
# YOUR TARGET
Your target is to win the game with your teammates as much as possible.
# GAME RULES
- In werewolf game, players are divided into three game_werewolves, three villagers, one seer, one hunter and one witch.
- Werewolves: kill one player each night, and must hide identity during the day.
- Villagers: ordinary players without special abilities, try to identify and eliminate game_werewolves.
- Seer: A special villager who can check one player's identity each night.
- Witch: A special villager with two one-time-use potions: a healing potion to save a player from being killed at night, and a poison to eliminate one player at night.
- Hunter: A special villager who can take one player down with them when they are eliminated.
- The game alternates between night and day phases until one side wins:
- Night Phase
- Werewolves choose one victim
- Seer checks one player's identity
- Witch decides whether to use potions
- Moderator announces who died during the night
- Day Phase
- All players discuss and vote to eliminate one suspected player
# GAME GUIDANCE
- Try your best to win the game with your teammates, tricks, lies, and deception are all allowed, e.g. pretending to be a different role.
- During discussion, don't be political, be direct and to the point.
- The day phase voting provides important clues. For example, the game_werewolves may vote together, attack the seer, etc.
## GAME GUIDANCE FOR WEREWOLF
- Seer is your greatest threat, who can check one player's identity each night. Analyze players' speeches, find out the seer and eliminate him/her will greatly increase your chances of winning.
- In the first night, making random choices is common for game_werewolves since no information is available.
- Pretending to be other roles (seer, witch or villager) is a common strategy to hide your identity and mislead other villagers in the day phase.
- The outcome of the night phase provides important clues. For example, if witch uses the healing or poison potion, if the dead player is hunter, etc. Use this information to adjust your strategy.
## GAME GUIDANCE FOR SEER
- Seer is very important to villagers, exposing yourself too early may lead to being targeted by game_werewolves.
- Your ability to check one player's identity is crucial.
- The outcome of the night phase provides important clues. For example, if witch uses the healing or poison potion, if the dead player is hunter, etc. Use this information to adjust your strategy.
## GAME GUIDANCE FOR WITCH
- Witch has two powerful potions, use them wisely to protect key villagers or eliminate suspected game_werewolves.
- The outcome of the night phase provides important clues. For example, if the dead player is hunter, etc. Use this information to adjust your strategy.
## GAME GUIDANCE FOR HUNTER
- Using your ability in day phase will expose your role (since only hunter can take one player down)
- The outcome of the night phase provides important clues. For example, if witch uses the healing or poison potion, etc. Use this information to adjust your strategy.
## GAME GUIDANCE FOR VILLAGER
- Protecting special villagers, especially the seer, is crucial for your team's success.
- Werewolves may pretend to be the seer. Be cautious and don't trust anyone easily.
- The outcome of the night phase provides important clues. For example, if witch uses the healing or poison potion, if the dead player is hunter, etc. Use this information to adjust your strategy.
# NOTE
- [IMPORTANT] DO NOT make up any information that is not provided by the moderator or other players.
- This is a TEXT-based game, so DO NOT use or make up any non-textual information.
- Always critically reflect on whether your evidence exist, and avoid making assumptions.
- Your response should be specific and concise, provide clear reason and avoid unnecessary elaboration.
- Generate your one-line response by using the `generate_response` function.
- Don't repeat the others' speeches.""",
model=DashScopeChatModel(
api_key=os.environ.get("DASHSCOPE_API_KEY"),
model_name="qwen3-max",
),
formatter=DashScopeMultiAgentFormatter(),
)
return agent
async def main() -> None:
"""The main entry point for the werewolf game."""
# Uncomment the following lines if you want to use Agentscope Studio
# to visualize the game process.
# import agentscope
# agentscope.init(
# studio_url="http://localhost:3000",
# project="werewolf_game",
# )
# Prepare 9 players, you can change their names here
players = [get_official_agents(f"Player{_ + 1}") for _ in range(9)]
# Note: You can replace your own agents here, or use all your own agents
# Load states from a previous checkpoint
session = JSONSession(save_dir="./checkpoints")
await session.load_session_state(
session_id="players_checkpoint",
**{player.name: player for player in players},
)
await werewolves_game(players)
# Save the states to a checkpoint
await session.save_session_state(
session_id="players_checkpoint",
**{player.name: player for player in players},
)
asyncio.run(main())

View File

@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
"""Default prompts"""
class EnglishPrompts:
"""English prompts used to guide the werewolf game."""
to_dead_player = (
"{}, you're eliminated now. Now you can make a final statement to "
"all alive players before you leave the game."
)
to_all_new_game = (
"A new game is starting, the players are: {}. Now we randomly "
"reassign the roles to each player and inform them of their roles "
"privately."
)
to_all_night = (
"Night has fallen, everyone close your eyes. Werewolves open your "
"eyes and choose a player to eliminate tonight."
)
to_wolves_discussion = (
"[WEREWOLVES ONLY] {}, you should discuss and "
"decide on a player to eliminate tonight. Current alive players "
"are {}. Remember to set `reach_agreement` to True if you reach an "
"agreement during the discussion."
)
to_wolves_vote = "[WEREWOLVES ONLY] Which player do you vote to kill?"
to_wolves_res = (
"[WEREWOLVES ONLY] The voting result is {}. So you have chosen to "
"eliminate {}."
)
to_all_witch_turn = (
"Witch's turn, witch open your eyes and decide your action tonight..."
)
to_witch_resurrect = (
"[WITCH ONLY] {witch_name}, you're the witch, and tonight {dead_name} "
"is eliminated. You can resurrect him/her by using your healing "
"potion, "
"and note you can only use it once in the whole game. Do you want to "
"resurrect {dead_name}? Give me your reason and decision."
)
to_witch_resurrect_no = (
"[WITCH ONLY] The witch has chosen not to resurrect the player."
)
to_witch_resurrect_yes = (
"[WITCH ONLY] The witch has chosen to resurrect the player."
)
to_witch_poison = (
"[WITCH ONLY] {witch_name}, as a witch, you have a one-time-use "
"poison potion, do you want to use it tonight? Give me your reason "
"and decision."
)
to_all_seer_turn = (
"Seer's turn, seer open your eyes and check one player's identity "
"tonight..."
)
to_seer = (
"[SEER ONLY] {}, as the seer you can check one player's identity "
"tonight. Who do you want to check? Give me your reason and decision."
)
to_seer_result = (
"[SEER ONLY] You've checked {agent_name}, and the result is: {role}."
)
to_hunter = (
"[HUNTER ONLY] {name}, as the hunter you're eliminated tonight. You "
"can choose one player to take down with you. Also, you can choose "
"not to use this ability. Give me your reason and decision."
)
to_all_hunter_shoot = (
"The hunter has chosen to shoot {} down with him/herself."
)
to_all_day = (
"The day is coming, all players open your eyes. Last night, "
"the following player(s) has been eliminated: {}."
)
to_all_peace = (
"The day is coming, all the players open your eyes. Last night is "
"peaceful, no player is eliminated."
)
to_all_discuss = (
"Now the alive players are {names}. The game goes on, it's time to "
"discuss and vote a player to be eliminated. Now you each take turns "
"to speak once in the order of {names}."
)
to_all_vote = (
"Now the discussion is over. Everyone, please vote to eliminate one "
"player from the alive players: {}."
)
to_all_res = "The voting result is {}. So {} has been voted out."
to_all_wolf_win = (
"There are {n_alive} players alive, and {n_werewolves} of them are "
"game_werewolves. "
"The game is over and game_werewolves win🐺🎉!"
"In this game, the true roles of all players are: {true_roles}"
)
to_all_village_win = (
"All the game_werewolves have been eliminated."
"The game is over and villagers win🏘🎉!"
"In this game, the true roles of all players are: {true_roles}"
)
to_all_continue = "The game goes on."
to_all_reflect = (
"The game is over. Now each player can reflect on their performance. "
"Note each player only has one chance to speak and the reflection is "
"only visible to themselves."
)
class ChinesePrompts:
"""Chinese prompts used to guide the werewolf game."""
to_dead_player = "{}, 你已被淘汰。现在你可以向所有存活玩家发表最后的遗言。"
to_all_new_game = "新的一局游戏开始,参与玩家包括:{}。现在为每位玩家重新随机分配身份,并私下告知各自身份。"
to_all_night = "天黑了,请所有人闭眼。狼人请睁眼,选择今晚要淘汰的一名玩家..."
to_wolves_discussion = (
"[仅狼人可见] {}, 你们可以讨论并决定今晚要淘汰的玩家。当前存活玩家有:{}"
"如果达成一致,请将 `reach_agreement` 设为 True。"
)
to_wolves_vote = "[仅狼人可见] 你投票要杀死哪位玩家?"
to_wolves_res = "[仅狼人可见] 投票结果为 {},你们选择淘汰 {}"
to_all_witch_turn = "轮到女巫行动,女巫请睁眼并决定今晚的操作..."
to_witch_resurrect = (
"[仅女巫可见] {witch_name},你是女巫,今晚{dead_name}被淘汰。"
"你可以用解药救他/她,注意解药全局只能用一次。你要救{dead_name}吗?"
"请给出理由和决定。"
)
to_witch_resurrect_no = "[仅女巫可见] 女巫选择不救该玩家。"
to_witch_resurrect_yes = "[仅女巫可见] 女巫选择救活该玩家。"
to_witch_poison = "[仅女巫可见] {witch_name},你有一瓶一次性毒药,今晚要使用吗?请给出理由和决定。"
to_all_seer_turn = "轮到预言家行动,预言家请睁眼并查验一名玩家身份..."
to_seer = "[仅预言家可见] {}, 你是预言家,今晚可以查验一名玩家身份。你要查谁?请给出理由和决定。"
to_seer_result = "[仅预言家可见] 你查验了{agent_name},结果是:{role}"
to_hunter = "[仅猎人可见] {name},你是猎人,今晚被淘汰。你可以选择带走一名玩家,也可以选择不带走。请给出理由和决定。"
to_all_hunter_shoot = "猎人选择带走 {} 一起出局。"
to_all_day = "天亮了,请所有玩家睁眼。昨晚被淘汰的玩家有:{}"
to_all_peace = "天亮了,请所有玩家睁眼。昨晚平安夜,无人被淘汰。"
to_all_discuss = "现在存活玩家有:{names}。游戏继续,大家开始讨论并投票淘汰一名玩家。请按顺序({names})依次发言。"
to_all_vote = "讨论结束。请大家从存活玩家中投票淘汰一人:{}"
to_all_res = "投票结果为 {}{} 被淘汰。"
to_all_wolf_win = (
"当前存活玩家共{n_alive}人,其中{n_werewolves}人为狼人。"
"游戏结束,狼人获胜🐺🎉!"
"本局所有玩家真实身份为:{true_roles}"
)
to_all_village_win = "所有狼人已被淘汰。游戏结束,村民获胜🏘️🎉!本局所有玩家真实身份为:{true_roles}"
to_all_continue = "游戏继续。"
to_all_reflect = "游戏结束。现在每位玩家可以对自己的表现进行反思。注意每位玩家只有一次发言机会,且反思内容仅自己可见。"

View File

@@ -0,0 +1,2 @@
agentscope>=1.0.5
agentscope[full]>=1.0.5

View File

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
"""The structured output models used in the werewolf game."""
from typing import Literal
from agentscope.agent import AgentBase
from pydantic import BaseModel, Field
class DiscussionModel(BaseModel):
"""The output format for discussion."""
reach_agreement: bool = Field(
description="Whether you have reached an agreement or not",
)
def get_vote_model(agents: list[AgentBase]) -> type[BaseModel]:
"""Get the vote model by player names."""
class VoteModel(BaseModel):
"""The vote output format."""
vote: Literal[tuple(_.name for _ in agents)] = Field( # type: ignore
description="The name of the player you want to vote for",
)
return VoteModel
class WitchResurrectModel(BaseModel):
"""The output format for witch resurrect action."""
resurrect: bool = Field(
description="Whether you want to resurrect the player",
)
def get_poison_model(agents: list[AgentBase]) -> type[BaseModel]:
"""Get the poison model by player names."""
class WitchPoisonModel(BaseModel):
"""The output format for witch poison action."""
poison: bool = Field(
description="Do you want to use the poison potion",
)
name: Literal[tuple(_.name for _ in agents)] | None = Field( # type: ignore
description="The name of the player you want to poison, if you "
"don't want to poison anyone, just leave it empty",
default=None,
)
return WitchPoisonModel
def get_seer_model(agents: list[AgentBase]) -> type[BaseModel]:
"""Get the seer model by player names."""
class SeerModel(BaseModel):
"""The output format for seer action."""
name: Literal[tuple(_.name for _ in agents)] = Field( # type: ignore
description="The name of the player you want to check",
)
return SeerModel
def get_hunter_model(agents: list[AgentBase]) -> type[BaseModel]:
"""Get the hunter model by player agents."""
class HunterModel(BaseModel):
"""The output format for hunter action."""
shoot: bool = Field(
description="Whether you want to use the shooting ability or not",
)
name: Literal[tuple(_.name for _ in agents)] | None = Field( # type: ignore
description="The name of the player you want to shoot, if you "
"don't want to the ability, just leave it empty",
default=None,
)
return HunterModel

View File

@@ -0,0 +1,164 @@
# -*- coding: utf-8 -*-
"""Utility functions for the werewolf game."""
from collections import defaultdict
from typing import Any
import numpy as np
from agentscope.agent import AgentBase, ReActAgent
from agentscope.message import Msg
from prompt import EnglishPrompts as Prompts
MAX_GAME_ROUND = 30
MAX_DISCUSSION_ROUND = 3
def majority_vote(votes: list[str]) -> tuple:
"""Return the vote with the most counts."""
result = max(set(votes), key=votes.count)
names, counts = np.unique(votes, return_counts=True)
conditions = ", ".join(
[f"{name}: {count}" for name, count in zip(names, counts)],
)
return result, conditions
def names_to_str(agents: list[str] | list[ReActAgent]) -> str:
"""Return a string of agent names."""
if not agents:
return ""
if len(agents) == 1:
if isinstance(agents[0], ReActAgent):
return agents[0].name
return agents[0]
names = []
for agent in agents:
if isinstance(agent, ReActAgent):
names.append(agent.name)
else:
names.append(agent)
return ", ".join([*names[:-1], "and " + names[-1]])
class EchoAgent(AgentBase):
"""Echo agent that repeats the input message."""
def __init__(self) -> None:
super().__init__()
self.name = "Moderator"
async def reply(self, content: str) -> Msg:
"""Repeat the input content with its name and role."""
msg = Msg(
self.name,
content,
role="assistant",
)
await self.print(msg)
return msg
async def handle_interrupt(
self,
*args: Any,
**kwargs: Any,
) -> Msg:
"""Handle interrupt."""
async def observe(self, msg: Msg | list[Msg] | None) -> None:
"""Observe the user's message."""
class Players:
"""Maintain the players' status."""
def __init__(self) -> None:
"""Initialize the players."""
# The mapping from player name to role
self.name_to_role = {}
self.role_to_names = defaultdict(list)
self.name_to_agent = {}
self.werewolves = []
self.villagers = []
self.seer = []
self.hunter = []
self.witch = []
self.current_alive = []
self.all_players = []
def add_player(self, player: ReActAgent, role: str) -> None:
"""Add a player to the game.
Args:
player (`ReActAgent`):
The player to be added.
role (`str`):
The role of the player.
"""
self.name_to_role[player.name] = role
self.name_to_agent[player.name] = player
self.role_to_names[role].append(player.name)
self.all_players.append(player)
if role == "werewolf":
self.werewolves.append(player)
elif role == "villager":
self.villagers.append(player)
elif role == "seer":
self.seer.append(player)
elif role == "hunter":
self.hunter.append(player)
elif role == "witch":
self.witch.append(player)
else:
raise ValueError(f"Unknown role: {role}")
self.current_alive.append(player)
def update_players(self, dead_players: list[ReActAgent]) -> None:
"""Update the current alive players.
Args:
dead_players (`list[ReActAgent]`):
A list of dead players to be removed.
"""
self.werewolves = [
_ for _ in self.werewolves if _.name not in dead_players
]
self.villagers = [
_ for _ in self.villagers if _.name not in dead_players
]
self.seer = [_ for _ in self.seer if _.name not in dead_players]
self.hunter = [_ for _ in self.hunter if _.name not in dead_players]
self.witch = [_ for _ in self.witch if _.name not in dead_players]
self.current_alive = [
_ for _ in self.current_alive if _.name not in dead_players
]
def print_roles(self) -> None:
"""Print the roles of all players."""
print("Roles:")
for name, role in self.name_to_role.items():
print(f" - {name}: {role}")
def check_winning(self) -> str | None:
"""Check if the game is over and return the winning message."""
# Prepare true roles string
true_roles = (
f'{names_to_str(self.role_to_names["werewolf"])} are werewolves, '
f'{names_to_str(self.role_to_names["villager"])} are villagers, '
f'{names_to_str(self.role_to_names["seer"])} is the seer, '
f'{names_to_str(self.role_to_names["hunter"])} is the hunter, '
f'and {names_to_str(self.role_to_names["witch"])} is the witch.'
)
if len(self.werewolves) * 2 >= len(self.current_alive):
return Prompts.to_all_wolf_win.format(
n_alive=len(self.current_alive),
n_werewolves=len(self.werewolves),
true_roles=true_roles,
)
if self.current_alive and not self.werewolves:
return Prompts.to_all_village_win.format(
true_roles=true_roles,
)
return None