rm redundant & add random game setup

This commit is contained in:
yzlin 2023-09-30 15:02:19 +08:00
parent 4d39bb2815
commit fa88e44521
10 changed files with 95 additions and 162 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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__':

View file

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