From c6a5b46ddd6d9a465acbdec6052928718bf97bd6 Mon Sep 17 00:00:00 2001 From: mannaandpoem <1580466765@qq.com> Date: Tue, 26 Sep 2023 09:45:28 +0800 Subject: [PATCH] add seer role and action, and update moderator role --- examples/werewolf_game/actions/__init__.py | 2 + .../actions/moderator_actions.py | 127 +++++++++++++----- .../werewolf_game/actions/seer_actions.py | 22 +++ examples/werewolf_game/roles/__init__.py | 1 + examples/werewolf_game/roles/moderator.py | 53 +++++--- examples/werewolf_game/roles/seer.py | 44 ++++++ examples/werewolf_game/start_game.py | 6 +- 7 files changed, 195 insertions(+), 60 deletions(-) create mode 100644 examples/werewolf_game/actions/seer_actions.py create mode 100644 examples/werewolf_game/roles/seer.py diff --git a/examples/werewolf_game/actions/__init__.py b/examples/werewolf_game/actions/__init__.py index 7c5af46df..5568f50f8 100644 --- a/examples/werewolf_game/actions/__init__.py +++ b/examples/werewolf_game/actions/__init__.py @@ -1,8 +1,10 @@ from examples.werewolf_game.actions.moderator_actions import InstructSpeak from examples.werewolf_game.actions.common_actions import Speak from examples.werewolf_game.actions.werewolf_actions import Hunt +from examples.werewolf_game.actions.seer_actions import Verify ACTIONS = { "Speak": Speak, "Hunt": Hunt, + "Verify": Verify, } \ No newline at end of file diff --git a/examples/werewolf_game/actions/moderator_actions.py b/examples/werewolf_game/actions/moderator_actions.py index 9e51a9106..3ca67c5c1 100644 --- a/examples/werewolf_game/actions/moderator_actions.py +++ b/examples/werewolf_game/actions/moderator_actions.py @@ -1,6 +1,9 @@ +import asyncio +from random import random + from metagpt.actions import Action -STAGE_INSTRUCTIONS = { +STEP_INSTRUCTIONS = { # 上帝需要介入的全部步骤和对应指令 # The 1-st night 0: {"content": "It’s dark, everyone close your eyes. I will talk with you/your team secretly at night.", @@ -19,7 +22,7 @@ STAGE_INSTRUCTIONS = { 4: {"content": "Werewolves, please open your eyes!", "send_to": "Moderator", "restricted_to": ""}, - 5: {"content": """Werewolves, I secretly tell you that xxx and xxx are + 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}. """, # send to werewolf restrictedly for a response @@ -57,7 +60,7 @@ STAGE_INSTRUCTIONS = { 14: {"content": """It's daytime. Everyone woke up except those who had been killed.""", "send_to": "Moderator", "restricted_to": ""}, - 15: {"content": "xxxx was killed last night. Or, it was a peaceful night and no one died!", + 15: {"content": "{killed_player} was killed last night. Or, it was a peaceful night and no one died!", "send_to": "Moderator", "restricted_to": ""}, 16: {"content": """Now freely talk about roles of other players with each other based on your observation and @@ -66,49 +69,39 @@ STAGE_INSTRUCTIONS = { "restricted_to": ""}, 17: {"content": """Now vote and tell me who you think is the werewolf. Don’t mention your role. You only choose one from the following living options please: - {living_players}. Or you can pass. For example: I vote to kill xxxx""", + {living_players}. Or you can pass. For example: I vote to kill ...""", "send_to": "Moderator", "restricted_to": ""}, - 18: {"content": """xxxx was eliminated.""", + 18: {"content": """{voted_out_player} was eliminated.""", "send_to": "Moderator", "restricted_to": ""}, } -INSTRUCT_SPEAK_TEMPLATE = """ -## BACKGROUND -It's a Werewolf game, you are moderator. -## STAGE -The current stage of the game is: -{} - -## CONTEXT -Here's the current context: -{} - -What would you like to instruct? -""" class InstructSpeak(Action): def __init__(self, name="InstructSpeak", context=None, llm=None): super().__init__(name, context, llm) - async def run(self, context,living_players, stage_idx): + async def run(self, step_idx, living_players, werewolf_players, killed_player, voted_out_player): + instruction_info = STEP_INSTRUCTIONS.get(step_idx, { + "content": "Unknown instruction.", + "send_to": "", + "restricted_to": "" + }) + content = instruction_info["content"] + if "{living_players}" in content and "{werewolf_players}" in content: + content = content.format(living_players=",".join(living_players), + werewolf_players=",".join(werewolf_players)) + if "{living_players}" in content: + content = content.format(living_players=",".join(living_players)) + if "{werewolf_players}" in content: + content = content.format(werewolf_players=",".join(werewolf_players)) + if "{killed_player}" in content: + content = content.format(killed_player=killed_player) + if "{voted_out_player}" in content: + content = content.format(voted_out_player=voted_out_player) - instruction_info = STAGE_INSTRUCTIONS.get(stage_idx, "Unknown instruction.") - # 利用上下文信息context和所处阶段的信息 - if "{living_players}" in instruction_info["content"]: - content = instruction_info["content"].format(living_players) - else: - content = instruction_info["content"] - - prompt = INSTRUCT_SPEAK_TEMPLATE.format(content, context) - - rsp = await self._aask(prompt) - - send_to = instruction_info["send_to"] - restricted_to = instruction_info["restricted_to"] - - return rsp, send_to, restricted_to + return content, instruction_info["send_to"], instruction_info["restricted_to"] class ParseSpeak(Action): @@ -118,15 +111,77 @@ class ParseSpeak(Action): class SummarizeNight(Action): """consider all events at night, conclude which player dies (can be a peaceful night)""" - pass + + 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"} + 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""" - pass + + def __init__(self, name="SummarizeDay", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, votes): + # 假设votes是一个字典,代表白天投票的结果,key是被投票的玩家,value是得票数 + # 例如:{"Player1": 2, "Player2": 1, "Player3": 1, "Player4": 0} + # 表示Player1得到2票,Player2和Player3各得到1票,Player4得到0票 + # 若平票,则无人死亡 + if not votes: + return "No votes were cast. No one was killed." + + max_votes = max(votes.values()) + players_with_max_votes = [player for player, vote_count in votes.items() if vote_count == max_votes] + + if len(players_with_max_votes) == 1: + eliminated_player = players_with_max_votes[0] + return f"{eliminated_player} was voted out and eliminated." + else: + # 若平票,则随机选一个人出局 + eliminated_player = players_with_max_votes[int(random() * len(players_with_max_votes))] + return f"There was a tie in the votes. {eliminated_player} was randomly chosen and eliminated." class AnnounceGameResult(Action): async def run(self, winner: str): return f"Game over! The winner is {winner}" + + +async def main(): + rst1 = await SummarizeDay().run({"Player1": 0, "Player2": 0, "Player3": 0, "Player4": 0}) + rst2 = await SummarizeNight().run({"killed_by_werewolves": "Player1"}) + # 表示第2个步骤的指令,living_players是所有活着的玩家,werewolf_players是所有狼人,killed_player是被杀的玩家,voted_out_player是被投票出局的玩家 + rst3 = await InstructSpeak().run(2, ["Player1", "Player2", "Player3", "Player4"], + ["Player3", "Player4"], "Player4", "Player3") + print(rst1) + print(rst2) + print(rst3) + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/examples/werewolf_game/actions/seer_actions.py b/examples/werewolf_game/actions/seer_actions.py new file mode 100644 index 000000000..b4cb55b15 --- /dev/null +++ b/examples/werewolf_game/actions/seer_actions.py @@ -0,0 +1,22 @@ +from metagpt.actions import Action + + +class Verify(Action): + """Action: Seer verifies a player's identity at night""" + + PROMPT_TEMPLATE = """ + It's a werewolf game and you are a seer. + You can choose to verify the identity of a player. + Here's the game history: + {context}. + Now, choose one player to verify: + """ + + def __init__(self, name="Verify", context=None, llm=None): + super().__init__(name, context, llm) + + async def run(self, context: str): + prompt = self.PROMPT_TEMPLATE.format(context=context) + rsp = await self._aask(prompt) + + return rsp diff --git a/examples/werewolf_game/roles/__init__.py b/examples/werewolf_game/roles/__init__.py index 464563344..ac79f07d7 100644 --- a/examples/werewolf_game/roles/__init__.py +++ b/examples/werewolf_game/roles/__init__.py @@ -2,3 +2,4 @@ from examples.werewolf_game.roles.base_player import BasePlayer from examples.werewolf_game.roles.moderator import Moderator from examples.werewolf_game.roles.villager import Villager from examples.werewolf_game.roles.werewolf import Werewolf +from examples.werewolf_game.roles.seer import Seer diff --git a/examples/werewolf_game/roles/moderator.py b/examples/werewolf_game/roles/moderator.py index 9b59300ef..7cb85c50b 100644 --- a/examples/werewolf_game/roles/moderator.py +++ b/examples/werewolf_game/roles/moderator.py @@ -4,37 +4,46 @@ from metagpt.roles import Role from metagpt.schema import Message from metagpt.logs import logger from examples.werewolf_game.actions.moderator_actions import ( - InstructSpeak, ParseSpeak, AnnounceGameResult, STAGE_INSTRUCTIONS + InstructSpeak, ParseSpeak, AnnounceGameResult, STEP_INSTRUCTIONS ) from metagpt.actions import BossRequirement as UserRequirement -class Moderator(Role): +class Moderator(Role): # 游戏状态属性 is_game_over = False winner = None def __init__( - self, - name: str = "Moderator", - profile: str = "Moderator", - **kwargs, + self, + name: str = "Moderator", + profile: str = "Moderator", + **kwargs, ): super().__init__(name, profile, **kwargs) self._watch([UserRequirement, InstructSpeak, ParseSpeak]) self._init_actions([InstructSpeak, ParseSpeak, AnnounceGameResult]) - self.stage_idx = 0 - self.living_players = "[Player 1, Player 2, Player 3, Player 4]" - - async def _instruct_speak(self,context): - stage_idx = self.stage_idx % len(STAGE_INSTRUCTIONS) - self.stage_idx += 1 - return await InstructSpeak().run(context=context, stage_idx=stage_idx, living_players=self.living_players) + self.step_idx = 0 + self.living_players = ["Player1", "Player2", "Player3", "Player4", "Player5"] + self.werewolf_players = ["Player1", "Player2"] + self.killed_player = "Player 4" # 夜晚阶段,死掉的玩家 + self.voted_out_player = "Player 3" # 白天阶段,被投票出局的玩家 + # 假设votes代表白天投票的结果,key是被投票的玩家,value是得票数 + self.votes = {"Player1": 1, "Player2": 2, "Player3": 1, "Player4": 0, "Player5": 0} + + async def _instruct_speak(self, context): + step_idx = self.step_idx % len(STEP_INSTRUCTIONS) + self.step_idx += 1 + return await InstructSpeak().run(step_idx, + living_players=self.living_players, + werewolf_players=self.werewolf_players, + killed_player=self.killed_player, + voted_out_player=self.voted_out_player) async def _parse_speak(self): # 解析玩家消息并返回结果 parse_result = await ParseSpeak().run() - + # 理解结果,更新各角色状态、游戏状态 return "Player message processed" @@ -44,7 +53,7 @@ class Moderator(Role): if self.is_game_over: self._rc.todo = AnnounceGameResult() return - + # 确定当前是需要InstructSpeak还是ParseSpeak. 通过判断当前流程状态变量,以及消息的cause_by属性 # 0: InstructSpeak, 1: ParseSpeak,且需要判断消息的cause_by属性 if self._rc.memory.get()[-1].role in ["User", self.profile]: @@ -52,7 +61,7 @@ class Moderator(Role): # 2. 上一轮消息是Moderator自己的指令,继续发出指令,一个事情可以分几条消息来说 # 3. 上一轮消息是Moderator自己的解析消息,一个阶段结束,发出新一个阶段的指令 self._rc.todo = InstructSpeak() - + else: # 上一轮消息是游戏角色的发言,解析角色的发言 self._rc.todo = ParseSpeak() @@ -68,20 +77,20 @@ class Moderator(Role): if isinstance(todo, InstructSpeak): msg_content, msg_to_send_to, msg_restriced_to = await self._instruct_speak(memories) 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) - + cause_by=InstructSpeak, send_to=msg_to_send_to, restricted_to=msg_restriced_to) + elif isinstance(todo, ParseSpeak): msg_content = await self._parse_speak() msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=ParseSpeak) - + elif isinstance(todo, AnnounceGameResult): msg_content = await AnnounceGameResult().run(winner=self.winner) msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=AnnounceGameResult) - + logger.info(f"{self._setting}: {msg_content}") - + return msg - + def get_all_memories(self) -> str: memories = self._rc.memory.get() memories = [str(m) for m in memories] diff --git a/examples/werewolf_game/roles/seer.py b/examples/werewolf_game/roles/seer.py new file mode 100644 index 000000000..b4ff41c8c --- /dev/null +++ b/examples/werewolf_game/roles/seer.py @@ -0,0 +1,44 @@ +from examples.werewolf_game.actions.seer_actions import Verify +from examples.werewolf_game.roles.base_player import BasePlayer +from examples.werewolf_game.actions import Speak +from metagpt.schema import Message +from metagpt.logs import logger + + +class Seer(BasePlayer): + def __init__( + 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) + + async def _act(self): + todo = self._rc.todo + logger.info(f"{self._setting}: ready to {str(todo)}") + + memories = self.get_all_memories() + print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10) + + # 基于todo的类型,调用不同的action + if isinstance(todo, Speak): + rsp = await todo.run(profile=self.profile, context=memories) + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=Speak, send_to="", restricted_to="", + ) + + elif isinstance(todo, Verify): + rsp = await todo.run(context=memories) + msg = Message( + content=rsp, role=self.profile, sent_from=self.name, + cause_by=Verify, send_to="", + restricted_to="Moderator", + ) + + logger.info(f"{self._setting}: {rsp}") + + return msg diff --git a/examples/werewolf_game/start_game.py b/examples/werewolf_game/start_game.py index 98d76aa9d..b272dc13b 100644 --- a/examples/werewolf_game/start_game.py +++ b/examples/werewolf_game/start_game.py @@ -3,14 +3,15 @@ import platform import fire from examples.werewolf_game.werewolf_game import WerewolfGame -from examples.werewolf_game.roles import Moderator, Villager, Werewolf +from examples.werewolf_game.roles import Moderator, Villager, Werewolf, Seer DEFAULT_PLAYER_SETUP = """ Game setup: Player1: Villager, Player2: Villager, Player3: Werewolf, -Player4: Werewolf. +Player4: Werewolf, +Player5: Seer. """ async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, n_round: int = 5): @@ -21,6 +22,7 @@ async def start_game(idea: str = DEFAULT_PLAYER_SETUP, investment: float = 3.0, Villager(name="Player2"), Werewolf(name="Player3"), Werewolf(name="Player4"), + Seer(name="Player5"), ]) game.invest(investment) game.start_project(idea)