add seer role and action, and update moderator role

This commit is contained in:
mannaandpoem 2023-09-26 09:45:28 +08:00
parent 3c01534964
commit c6a5b46ddd
7 changed files with 195 additions and 60 deletions

View file

@ -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,
}

View file

@ -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": "Its 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. Dont 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())

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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)