diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py index 3d60563ce..fc897474e 100644 --- a/examples/werewolf_game/actions/moderator_actions.py +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -13,8 +13,9 @@ STEP_INSTRUCTIONS = { 1: {"content": "Guard, please open your eyes!", "send_to": "Moderator", # for moderator to continuen speaking "restricted_to": ""}, - 2: {"content": """Guard, now tell me who you protect tonight? - You only choose one from the following living options please: {living_players}. Or you can pass. For example: I protect ...""", + 2: {"content": """Guard, now tell me who you protect tonight? + You only choose one from the following living options please: {living_players}. + Or you can pass. For example: Protect ...""", "send_to": "Guard", "restricted_to": "Moderator,Guard"}, 3: {"content": "Guard, close your eyes", @@ -26,7 +27,7 @@ STEP_INSTRUCTIONS = { 5: {"content": """Werewolves, I secretly tell you that {werewolf_players} are all of the 2 werewolves! Keep in mind you are teammates. The rest players are not werewolves. choose one from the following living options please: - {living_players}. For example: I kill ...""", + {living_players}. For example: Kill ...""", "send_to": "Werewolf", "restricted_to": "Moderator,Werewolf"}, 6: {"content": "Werewolves, close your eyes", @@ -35,12 +36,13 @@ STEP_INSTRUCTIONS = { 7: {"content": "Witch, please open your eyes!", "send_to": "Moderator", "restricted_to": ""}, - 8: {"content": """Witch, tonight {player_hunted} has been killed by the werewolves. - You have a bottle of antidote, would you like to save him/her? If so, say "Save", else, say "Pass".""", + 8: {"content": """Witch, tonight {player_hunted} has been killed by the werewolves. + You have a bottle of antidote, would you like to save him/her? If so, say "Save", else, say "Pass".""", "send_to": "Witch", "restricted_to": "Moderator,Witch"}, # 要先判断女巫是否有解药,再去询问女巫是否使用解药救人 - 9: {"content": """Witch, you also have a bottle of poison, would you like to use it to kill one of the living players? - Choose one from the following living options: {living_players}. If so, say "Poison PlayerX", where X is the player index, else, say "Pass".""", + 9: {"content": """Witch, you also have a bottle of poison, would you like to use it to kill one of the living players? + Choose one from the following living options: {living_players}. + If so, say "Poison PlayerX", where X is the player index, else, say "Pass".""", "send_to": "Witch", "restricted_to": "Moderator,Witch"}, # 10: {"content": "Witch, close your eyes", @@ -49,8 +51,8 @@ STEP_INSTRUCTIONS = { 11: {"content": "Seer, please open your eyes!", "send_to": "Moderator", "restricted_to": ""}, - 12: {"content": """Seer, you can check one player's identity. Who are you going to verify its identity tonight? - Choose only one from the following living options:{living_players}.""", + 12: {"content": """Seer, you can check one player's identity. Who are you going to verify its identity tonight? + Choose only one from the following living options:{living_players}.""", "send_to": "Seer", "restricted_to": "Moderator,Seer"}, 13: {"content": "Seer, close your eyes", @@ -77,33 +79,6 @@ STEP_INSTRUCTIONS = { "restricted_to": ""}, } -VOTE_PROMPT = """ - Welcome to the daytime discussion phase in the Werewolf game. - - During the day, players discuss and share information about who they suspect might be a werewolf. - Players can also cast their votes to eliminate a player they believe is a werewolf. - - Here are the conversations from the daytime: - - {vote_message} - - Now it's time to cast your votes. - - You can vote for a player by typing their name. - Example: "Vote for Player2" - - Here are the voting options: -""" - -PARSE_INSTRUCTIONS = { - 0: "Now it's time to vote", - 1: "The {winner} have won! They successfully eliminated all the {loser}." - "The game has ended. Thank you for playing Werewolf!", - 2: "The night has ended, and it's time to reveal the casualties." - "During the night, the Werewolves made their move. Unfortunately, they targeted {PlayerName}, who is now dead." -} - - class InstructSpeak(Action): def __init__(self, name="InstructSpeak", context=None, llm=None): super().__init__(name, context, llm) @@ -123,9 +98,9 @@ class InstructSpeak(Action): if "{werewolf_players}" in content: content = content.format(werewolf_players=",".join(werewolf_players)) if "{player_hunted}" in content: - player_hunted = "No one" if not player_hunted else player_hunted content = content.format(player_hunted=player_hunted) if "{player_current_dead}" in content: + player_current_dead = "No one" if not player_current_dead else player_current_dead content = content.format(player_current_dead=player_current_dead) return content, instruction_info["send_to"], instruction_info["restricted_to"] @@ -137,43 +112,6 @@ class ParseSpeak(Action): async def run(self): pass -class SummarizeNight(Action): - """consider all events at night, conclude which player dies (can be a peaceful night)""" - - def __init__(self, name="SummarizeNight", context=None, llm=None): - super().__init__(name, context, llm) - - async def run(self, events): - # 假设events是一个字典,代表夜晚发生的多个事件,key是事件类型,value是该事件对应的玩家 - # 例如被狼人杀的玩家:{"killed_by_werewolves": "Player1"} - # 被守卫守护的玩家:{"protected_by_guard": "Player2"} - # 被女巫救的玩家:{"saved_by_witch": "Player3"} - # 被女巫毒的玩家:{"poisoned_by_witch": "Player4"} - # 被预言家查验的玩家:{"verified_by_seer": "Player5"} - # 若没有事件发生,则events为空字典 - killed_by_werewolves = events.get("killed_by_werewolves", "") - protected_by_guard = events.get("protected_by_guard", "") - saved_by_witch = events.get("saved_by_witch", "") - poisoned_by_witch = events.get("poisoned_by_witch", "") - - # 若狼人杀的人和守卫守的人是同一个人,那么该人就会活着; - if protected_by_guard and killed_by_werewolves and protected_by_guard == killed_by_werewolves: - return "It was a peaceful night. No one was killed." - - # 若守卫和女巫都救了同一个人,那么该人就会死 - if protected_by_guard and saved_by_witch and protected_by_guard == saved_by_witch: - return f"{protected_by_guard} was killed by the werewolves." - - if saved_by_witch: - return f"{saved_by_witch} was saved by the witch." - - if poisoned_by_witch: - return f"{poisoned_by_witch} was poisoned by the witch." - - if killed_by_werewolves: - return f"{killed_by_werewolves} was killed by the werewolves." - - class SummarizeDay(Action): """consider all votes at day, conclude which player dies""" @@ -205,12 +143,9 @@ class AnnounceGameResult(Action): async def run(self, winner: str): return f"Game over! The winner is the {winner}" - async def main(): rst1 = await SummarizeDay().run({"Player1": 0, "Player2": 0, "Player3": 0, "Player4": 0}) - rst2 = await SummarizeNight().run({"killed_by_werewolves": "Player1"}) print(rst1) - print(rst2) if __name__ == '__main__': asyncio.run(main()) diff --git a/examples/werewolf_game/roles/base_player.py b/examples/werewolf_game/roles/base_player.py index 38e6b2374..df4cf48bf 100644 --- a/examples/werewolf_game/roles/base_player.py +++ b/examples/werewolf_game/roles/base_player.py @@ -1,3 +1,5 @@ +import re + from metagpt.roles import Role from metagpt.schema import Message from metagpt.logs import logger @@ -9,67 +11,55 @@ class BasePlayer(Role): self, name: str = "PlayerXYZ", profile: str = "BasePlayer", - team: str = "good guys", special_action_names: list[str] = [], **kwargs, ): super().__init__(name, profile, **kwargs) self._init_actions([Speak]) self._watch([InstructSpeak]) - self.team = team - # 调用 get_status() 来检查存活状态,并通过 set_status() 更新状态。 + # 通过 set_status() 更新状态。 self.status = 0 # 0代表活着,1代表死亡 - + # 技能和监听配置 self._watch([InstructSpeak]) # 监听Moderator的指令以做行动 special_actions = [ACTIONS[action_name] for action_name in special_action_names] capable_actions = [Speak] + special_actions self._init_actions(capable_actions) # 给角色赋予行动技能 self.special_actions = special_actions - + async def _observe(self) -> int: if self.status == 1: # 死者不再参与游戏 return 0 - + await super()._observe() # 只有发给全体的("")或发给自己的(self.profile)消息需要走下面的_react流程, # 其他的收听到即可,不用做动作 self._rc.news = [msg for msg in self._rc.news if msg.send_to in ["", self.profile]] return len(self._rc.news) - + async def _think(self): news = self._rc.news[0] assert news.cause_by == InstructSpeak # 消息为来自Moderator的指令时,才去做动作 if not news.restricted_to: # 消息接收范围为全体角色的,做公开发言(发表投票观点也算发言) self._rc.todo = Speak() - elif self.profile in news.restricted_to.split(","): # FIXME: hard code to split, restricted为"Moderator"或"Moderator,角色profile" + elif self.profile in news.restricted_to.split(","): + # FIXME: hard code to split, restricted为"Moderator"或"Moderator,角色profile" # Moderator加密发给自己的,意味着要执行角色的特殊动作 self._rc.todo = self.special_actions[0]() async def _act(self): """每个角色要改写此函数以实现该角色的动作""" raise NotImplementedError - + def get_all_memories(self) -> str: memories = self._rc.memory.get() + time_stamp_pattern = r'[0-9]+ \| ' # NOTE: 除Moderator外,其他角色使用memory,只能用m.sent_from(玩家名)不能用m.role(玩家角色),因为他们不知道说话者的身份 - memories = [f"{m.sent_from}: {m.content}" for m in memories] + memories = [f"{m.sent_from}: {re.sub(time_stamp_pattern, '', m.content)}" for m in memories] # regex去掉时间戳 memories = "\n".join(memories) return memories - - def get_name(self): - return self.name - - def get_profile(self): - return self.profile - - def get_team(self): - return self.team - - def get_status(self): - return self.status def set_status(self, new_status): self.status = new_status diff --git a/examples/werewolf_game/roles/guard.py b/examples/werewolf_game/roles/guard.py index 89d22c153..1f53dd795 100644 --- a/examples/werewolf_game/roles/guard.py +++ b/examples/werewolf_game/roles/guard.py @@ -8,12 +8,11 @@ class Guard(BasePlayer): self, name: str = "", profile: str = "Guard", - team: str = "good guys", special_action_names: list[str] = ["Protect"], **kwargs, ): - super().__init__(name, profile, team, special_action_names, **kwargs) - + super().__init__(name, profile, special_action_names, **kwargs) + async def _act(self): # todo为_think时确定的,有两种情况,Speak或Protect todo = self._rc.todo @@ -30,7 +29,7 @@ class Guard(BasePlayer): content=rsp, role=self.profile, sent_from=self.name, cause_by=Speak, send_to="", restricted_to="", ) - + elif isinstance(todo, Protect): rsp = await todo.run(context=memories) msg = Message( @@ -38,9 +37,7 @@ class Guard(BasePlayer): cause_by=Protect, send_to="", restricted_to=f"Moderator,{self.profile}", # 给Moderator发送守卫要保护的人加密消息 ) - + logger.info(f"{self._setting}: {rsp}") return msg - - diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index 9d0323221..c02400918 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -1,4 +1,3 @@ -import asyncio import re from collections import Counter @@ -13,9 +12,6 @@ from metagpt.actions import BossRequirement as UserRequirement class Moderator(Role): - # 游戏状态属性 - is_game_over = False - winner = None def __init__( self, @@ -35,7 +31,7 @@ class Moderator(Role): self.winner = None self.witch_poison_left = 1 self.witch_antidote_left = 1 - + # player states of current night self.player_hunted = None self.player_protected = None @@ -48,7 +44,7 @@ class Moderator(Role): self.werewolf_players = re.findall(r"Player[0-9]+: Werewolf", game_setup) self.werewolf_players = [p.replace(": Werewolf", "") for p in self.werewolf_players] self.good_guys = [p for p in self.living_players if p not in self.werewolf_players] - + def update_player_status(self, player_names: list[str]): if not player_names: return @@ -72,15 +68,15 @@ class Moderator(Role): logger.info(self.step_idx) latest_msg = memories[-1] + latest_msg_content = latest_msg.content - match = re.search(r"Player[0-9]+", latest_msg.content[-10:]) + match = re.search(r"Player[0-9]+", latest_msg_content[-10:]) # FIXME: hard code truncation target = match.group(0) if match else "" # default return msg_content = "Understood" restricted_to = "" - source_role = latest_msg.role msg_cause_by = latest_msg.cause_by if msg_cause_by == Hunt: self.player_hunted = target @@ -94,14 +90,18 @@ class Moderator(Role): msg_content = f"{target} is a good guy" restricted_to = "Moderator,Seer" elif msg_cause_by == Save: - if not self.witch_antidote_left and latest_msg.content != "Pass": + if "pass" in latest_msg_content.lower(): + pass + elif not self.witch_antidote_left: msg_content = "You have no antidote left and thus can not save the player" restricted_to = "Moderator,Witch" else: self.witch_antidote_left -= 1 - self.is_hunted_player_saved = latest_msg.content == "Save" + self.is_hunted_player_saved = True elif msg_cause_by == Poison: - if not self.witch_poison_left and latest_msg.content != "Pass": + if "pass" in latest_msg_content.lower(): + pass + elif not self.witch_poison_left: msg_content = "You have no poison left and thus can not poison the player" restricted_to = "Moderator,Witch" else: @@ -140,7 +140,7 @@ class Moderator(Role): self.living_players = [p for p in self.living_players if p not in self.player_current_dead] self.update_player_status(self.player_current_dead) msg_content = "Voting done" - + # game's termination condition living_werewolf = [p for p in self.werewolf_players if p in self.living_players] living_good_guys = [p for p in self.good_guys if p in self.living_players] @@ -156,7 +156,7 @@ class Moderator(Role): if self.winner is not None: self._rc.todo = AnnounceGameResult() return - + latest_msg = self._rc.memory.get()[-1] if latest_msg.role in ["User"]: # 上一轮消息是用户指令,解析用户指令,开始游戏 @@ -181,18 +181,19 @@ class Moderator(Role): # print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) if self.step_idx % len(STEP_INSTRUCTIONS) == 0 or self.winner is not None: # 进行完一夜一日的循环,打印一次完整发言历史 + logger.info("a night and day cycle completed, examine all history") print(self.get_all_memories()) # 根据_think的结果,执行InstructSpeak还是ParseSpeak, 并将结果返回 if isinstance(todo, InstructSpeak): msg_content, msg_to_send_to, msg_restriced_to = await self._instruct_speak() - msg_content = f"Step {self.step_idx}: {msg_content}" # HACK: 加一个unique的step_idx避免记忆的自动去重 + # msg_content = f"Step {self.step_idx}: {msg_content}" # HACK: 加一个unique的step_idx避免记忆的自动去重 msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=InstructSpeak, send_to=msg_to_send_to, restricted_to=msg_restriced_to) elif isinstance(todo, ParseSpeak): msg_content, msg_restriced_to = await self._parse_speak(memories) - msg_content = f"Step {self.step_idx}: {msg_content}" # HACK: 加一个unique的step_idx避免记忆的自动去重 + # msg_content = f"Step {self.step_idx}: {msg_content}" # HACK: 加一个unique的step_idx避免记忆的自动去重 msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=ParseSpeak, send_to="", restricted_to=msg_restriced_to) diff --git a/examples/werewolf_game/roles/seer.py b/examples/werewolf_game/roles/seer.py index 7003bb2ad..3d9bdf92e 100644 --- a/examples/werewolf_game/roles/seer.py +++ b/examples/werewolf_game/roles/seer.py @@ -10,11 +10,10 @@ class Seer(BasePlayer): self, name: str = "", profile: str = "Seer", - team: str = "good guys", special_action_names: list[str] = ["Verify"], **kwargs, ): - super().__init__(name, profile, team, special_action_names, **kwargs) + super().__init__(name, profile, special_action_names, **kwargs) async def _act(self): todo = self._rc.todo diff --git a/examples/werewolf_game/roles/villager.py b/examples/werewolf_game/roles/villager.py index 6b1c6e50c..ececa96ea 100644 --- a/examples/werewolf_game/roles/villager.py +++ b/examples/werewolf_game/roles/villager.py @@ -8,11 +8,10 @@ class Villager(BasePlayer): self, name: str = "", profile: str = "Villager", - team: str = "good guys", special_action_names: list[str] = [], **kwargs, ): - super().__init__(name, profile, team, special_action_names, **kwargs) + super().__init__(name, profile, special_action_names, **kwargs) async def _act(self): @@ -34,5 +33,5 @@ class Villager(BasePlayer): ) logger.info(f"{self._setting}: {rsp}") - + return msg diff --git a/examples/werewolf_game/roles/werewolf.py b/examples/werewolf_game/roles/werewolf.py index a4187d38c..4a7393d77 100644 --- a/examples/werewolf_game/roles/werewolf.py +++ b/examples/werewolf_game/roles/werewolf.py @@ -8,12 +8,11 @@ class Werewolf(BasePlayer): self, name: str = "", profile: str = "Werewolf", - team: str = "werewolves", special_action_names: list[str] = ["Hunt"], **kwargs, ): - super().__init__(name, profile, team, special_action_names, **kwargs) - + super().__init__(name, profile, special_action_names, **kwargs) + async def _act(self): # todo为_think时确定的,有两种情况,Speak或Hunt todo = self._rc.todo @@ -30,7 +29,7 @@ class Werewolf(BasePlayer): content=rsp, role=self.profile, sent_from=self.name, cause_by=Speak, send_to="", restricted_to="", ) - + elif isinstance(todo, Hunt): rsp = await todo.run(context=memories) msg = Message( @@ -38,7 +37,7 @@ class Werewolf(BasePlayer): cause_by=Hunt, send_to="", restricted_to=f"Moderator,{self.profile}", # 给Moderator及狼阵营发送要杀的人的加密消息 ) - + logger.info(f"{self._setting}: {rsp}") return msg diff --git a/examples/werewolf_game/roles/witch.py b/examples/werewolf_game/roles/witch.py index 2111bd01d..13b677d7e 100644 --- a/examples/werewolf_game/roles/witch.py +++ b/examples/werewolf_game/roles/witch.py @@ -8,12 +8,11 @@ class Witch(BasePlayer): self, name: str = "", profile: str = "Witch", - team: str = "good guys", special_action_names: list[str] = ["Save", "Poison"], **kwargs, ): - super().__init__(name, profile, team, special_action_names, **kwargs) - + super().__init__(name, profile, special_action_names, **kwargs) + async def _think(self): # 女巫涉及两个特殊技能,因此在此需要改写_think进行路由 news = self._rc.news[0] @@ -21,7 +20,8 @@ class Witch(BasePlayer): if not news.restricted_to: # 消息接收范围为全体角色的,做公开发言(发表投票观点也算发言) self._rc.todo = Speak() - elif self.profile in news.restricted_to.split(","): # FIXME: hard code to split, restricted为"Moderator"或"Moderator,角色profile" + elif self.profile in news.restricted_to.split(","): + # FIXME: hard code to split, restricted为"Moderator"或"Moderator,角色profile" # Moderator加密发给自己的,意味着要执行角色的特殊动作 # 这里用关键词进行动作的选择,需要Moderator侧的指令进行配合 if "save" in news.content.lower(): diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index 83d3b3385..541066117 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -1,46 +1,45 @@ import asyncio import platform import fire +import random from examples.werewolf_game.werewolf_game import WerewolfGame from examples.werewolf_game.roles import Moderator, Villager, Werewolf, Guard, Seer, Witch -DEFAULT_PLAYER_SETUP = """ -Game setup: -Player1: Villager, -Player2: Villager, -Player3: Werewolf, -Player4: Werewolf, -Player5: Guard, -Player6: Seer, -Player7: Witch. -""" +def init_game_setup(shuffle=False): + roles = [ + Villager, + Villager, + Werewolf, + Werewolf, + Guard, + Seer, + Witch + ] + if shuffle: + random.seed(2023) + random.shuffle(roles) + players = [role(name=f"Player{i+1}") for i, role in enumerate(roles)] + game_setup = ["Game setup:"] + [f"{player.name}: {player.profile}," for player in players] + game_setup = "\n".join(game_setup) + return game_setup, players -async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 5): +async def start_game(investment: float = 3.0, n_round: int = 5): game = WerewolfGame() - game.hire([ - Moderator(), - Villager(name="Player1"), - Villager(name="Player2"), - Werewolf(name="Player3"), - Werewolf(name="Player4"), - Guard(name="Player5"), - Seer(name="Player6"), - Witch(name="Player7"), - ]) + game_setup, players = init_game_setup(shuffle=True) + players = [Moderator()] + players + game.hire(players) game.invest(investment) - game.start_project(idea) + game.start_project(game_setup) await game.run(n_round=n_round) - -def main(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 100): +def main(investment: float = 3.0, n_round: int = 100): """ - :param idea: game config instructions :param investment: contribute a certain dollar amount to watch the debate :param n_round: maximum rounds of the debate :return: """ - asyncio.run(start_game(idea, investment, n_round)) + asyncio.run(start_game(investment, n_round)) if __name__ == '__main__': diff --git a/examples/werewolf_game/werewolf_game.py b/examples/werewolf_game/werewolf_game.py index 7e743772c..feb2dca77 100644 --- a/examples/werewolf_game/werewolf_game.py +++ b/examples/werewolf_game/werewolf_game.py @@ -5,6 +5,20 @@ from metagpt.schema import Message class WerewolfEnvironment(Environment): + timestamp: int = 0 + + def publish_message(self, message: Message, add_timestamp: bool = True): + """向当前环境发布信息 + Post information to the current environment + """ + # self.message_queue.put(message) + if add_timestamp: + # 因消息内容可能重复,例如,连续两晚杀同一个人, + # 因此需要加一个unique的time_stamp以使得相同的message在加入记忆时不被自动去重 + message.content = f"{self.timestamp} | " + message.content + self.memory.add(message) + self.history += f"\n{message}" + async def run(self, k=1): """处理一次所有信息的运行,各角色顺序执行 Process all Role runs at once @@ -12,6 +26,7 @@ class WerewolfEnvironment(Environment): for _ in range(k): for role in self.roles.values(): await role.run() + self.timestamp += 1 class WerewolfGame(SoftwareCompany): """Use the "software company paradigm" to hold a werewolf game""" @@ -24,4 +39,3 @@ class WerewolfGame(SoftwareCompany): self.environment.publish_message( Message(role="User", content=idea, cause_by=UserRequirement, restricted_to="Moderator") ) - print("a")