update werewolf to meet WerewolfEnv

This commit is contained in:
better629 2024-04-10 14:04:37 +08:00
parent d692d9fb7b
commit b568b8f4a0
18 changed files with 451 additions and 348 deletions

View file

@ -29,7 +29,7 @@ async def start_game(
use_memory_selection=use_memory_selection,
new_experience_version=new_experience_version,
)
logger.info(f"game_setup\n{game_setup}")
logger.info(f"{game_setup}")
players = [Moderator()] + players
game.hire(players)

View file

@ -0,0 +1,106 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Desc :
from enum import Enum
from metagpt.const import MESSAGE_ROUTE_TO_ALL
class RoleType(Enum):
VILLAGER = "Villager"
WEREWOLF = "Werewolf"
GUARD = "Guard"
SEER = "Seer"
WITCH = "Witch"
MODERATOR = "Moderator"
class RoleState(Enum):
ALIVE = "alive" # the role is alive
DEAD = "dead" # killed or poisoned
KILLED = "killed" # killed by werewolf or voting
POISONED = "poisoned" # killed by poison
SAVED = "saved" # saved by antidote
PROTECTED = "projected" # projected by guard
class RoleActionRes(Enum):
SAVE = "save"
PASS = "pass" # ignore current action output
# the ordered rules by the moderator to announce to everyone each step
STEP_INSTRUCTIONS = {
0: {
"content": "Its dark, everyone close your eyes. I will talk with you/your team secretly at night.",
"send_to": {"Moderator"}, # for moderator to continue speaking
"restricted_to": {},
},
1: {
"content": "Guard, please open your eyes!",
"send_to": {"Moderator"}, # for moderator to continue 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: Protect ...""",
"send_to": {"Guard"},
"restricted_to": {"Moderator", "Guard"},
},
3: {"content": "Guard, close your eyes", "send_to": {"Moderator"}, "restricted_to": {}},
4: {"content": "Werewolves, please open your eyes!", "send_to": {"Moderator"}, "restricted_to": {}},
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: Kill ...""",
"send_to": {"Werewolf"},
"restricted_to": {"Moderator", "Werewolf"},
},
6: {"content": "Werewolves, close your eyes", "send_to": {"Moderator"}, "restricted_to": {}},
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".""",
"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 ONLY "Poison PlayerX", replace PlayerX with the actual player name, else, say "Pass".""",
"send_to": {"Witch"},
"restricted_to": {"Moderator", "Witch"},
}, #
10: {"content": "Witch, close your eyes", "send_to": {"Moderator"}, "restricted_to": {}},
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}.""",
"send_to": {"Seer"},
"restricted_to": {"Moderator", "Seer"},
},
13: {"content": "Seer, close your eyes", "send_to": {"Moderator"}, "restricted_to": {}},
# The 1-st daytime
14: {
"content": """It's daytime. Everyone woke up except those who had been killed.""",
"send_to": {"Moderator"},
"restricted_to": {},
},
15: {"content": "{player_current_dead} was killed last night!", "send_to": {"Moderator"}, "restricted_to": {}},
16: {
"content": """Living players: {living_players}, now freely talk about the current situation based on your observation and
reflection with a few sentences. Decide whether to reveal your identity based on your reflection.""",
"send_to": {MESSAGE_ROUTE_TO_ALL}, # send to all to speak in daytime
"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}. Say ONLY: I vote to eliminate ...""",
"send_to": {MESSAGE_ROUTE_TO_ALL},
"restricted_to": {},
},
18: {"content": """{player_current_dead} was eliminated.""", "send_to": {"Moderator"}, "restricted_to": {}},
}

View file

@ -0,0 +1,49 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Desc : werewolf observation/action space and its action definition
from pydantic import ConfigDict, Field
from gymnasium import spaces
from metagpt.environment.base_env_space import BaseEnvAction, BaseEnvActionType
from metagpt.environment.werewolf.const import STEP_INSTRUCTIONS, RoleState
class EnvActionType(BaseEnvActionType):
NONE = 0 # no action to run, just get observation
WOLF_KILL = 1 # wolf kill someone
VOTE_KILL = 2 # vote kill someone
WITCH_POISON = 3 # witch poison someone
WITCH_SAVE = 4 # witch save someone
GUARD_PROTECT = 5 # guard protect someone
PROGRESS_STEP = 6 # step increment
class EnvAction(BaseEnvAction):
model_config = ConfigDict(arbitrary_types_allowed=True)
action_type: int = Field(default=EnvActionType.NONE, description="action type")
player_name: str = Field(default="", description="the name of the player to do the action")
target_player_name: str = Field(default="", description="the name of the player who take the action")
def get_observation_space(player_num: int) -> spaces.Dict:
space = spaces.Dict({
"step_idx": spaces.Discrete(len(STEP_INSTRUCTIONS)),
"player_states": spaces.MultiDiscrete([len(RoleState)] * player_num),
"vote_counts": spaces.MultiDiscrete([player_num - 1] * player_num),
"player_current_dead": None, # TODO
"winner": spaces.Text(16),
"win_reason": spaces.Text(64)
})
return space
def get_action_space() -> spaces.Dict:
space = spaces.Dict({
"action_type": spaces.Discrete(len(EnvActionType)),
"player_name": spaces.Text(16), # the player to do the action
"target_player_name": spaces.Text(16) # the target player who take the action
})
return space

View file

@ -6,7 +6,6 @@ from pydantic import Field
from metagpt.environment.base_env import Environment
from metagpt.environment.werewolf.werewolf_ext_env import WerewolfExtEnv
from metagpt.logs import logger
from metagpt.schema import Message
@ -15,13 +14,11 @@ class WerewolfEnv(Environment, WerewolfExtEnv):
def publish_message(self, message: Message, add_timestamp: bool = True):
"""Post information to the current environment"""
logger.debug(f"publish_message: {message.dump()}")
if add_timestamp:
# Because the content of the message may be repeated, for example, killing the same person in two nights
# Therefore, a unique timestamp prefix needs to be added so that the same message will not be automatically deduplicated when added to the memory.
message.content = f"{self.timestamp} | " + message.content
self.memory.add(message)
self.history += f"\n{message}"
super().publish_message(message)
async def run(self, k=1):
"""Process all Role runs by order"""

View file

@ -4,98 +4,15 @@
import random
from collections import Counter
from enum import Enum
from typing import Any, Callable, Optional
from pydantic import ConfigDict, Field
from metagpt.environment.base_env import ExtEnv, mark_as_readable, mark_as_writeable
from metagpt.environment.base_env_space import BaseEnvAction, BaseEnvObsParams
from metagpt.environment.base_env_space import BaseEnvObsParams
from metagpt.environment.werewolf.env_space import EnvAction, EnvActionType
from metagpt.logs import logger
class RoleState(Enum):
ALIVE = "alive" # the role is alive
KILLED = "killed" # the role is killed by werewolf or voting
POISONED = "poisoned" # the role is killed by posion
SAVED = "saved" # the role is saved by antidote
# the ordered rules by the moderator to announce to everyone each step
STEP_INSTRUCTIONS = {
0: {
"content": "Its dark, everyone close your eyes. I will talk with you/your team secretly at night.",
"send_to": "Moderator", # for moderator to continuen speaking
"restricted_to": "",
},
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: Protect ...""",
"send_to": "Guard",
"restricted_to": "Moderator,Guard",
},
3: {"content": "Guard, close your eyes", "send_to": "Moderator", "restricted_to": ""},
4: {"content": "Werewolves, please open your eyes!", "send_to": "Moderator", "restricted_to": ""},
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: Kill ...""",
"send_to": "Werewolf",
"restricted_to": "Moderator,Werewolf",
},
6: {"content": "Werewolves, close your eyes", "send_to": "Moderator", "restricted_to": ""},
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".""",
"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 ONLY "Poison PlayerX", replace PlayerX with the actual player name, else, say "Pass".""",
"send_to": "Witch",
"restricted_to": "Moderator,Witch",
}, #
10: {"content": "Witch, close your eyes", "send_to": "Moderator", "restricted_to": ""},
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}.""",
"send_to": "Seer",
"restricted_to": "Moderator,Seer",
},
13: {"content": "Seer, close your eyes", "send_to": "Moderator", "restricted_to": ""},
# The 1-st daytime
14: {
"content": """It's daytime. Everyone woke up except those who had been killed.""",
"send_to": "Moderator",
"restricted_to": "",
},
15: {"content": "{player_current_dead} was killed last night!", "send_to": "Moderator", "restricted_to": ""},
16: {
"content": """Living players: {living_players}, now freely talk about the current situation based on your observation and
reflection with a few sentences. Decide whether to reveal your identity based on your reflection.""",
"send_to": "", # send to all to speak in daytime
"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}. Say ONLY: I vote to eliminate ...""",
"send_to": "",
"restricted_to": "",
},
18: {"content": """{player_current_dead} was eliminated.""", "send_to": "Moderator", "restricted_to": ""},
}
from metagpt.environment.werewolf.const import STEP_INSTRUCTIONS, RoleState, RoleType
class WerewolfExtEnv(ExtEnv):
@ -115,13 +32,13 @@ class WerewolfExtEnv(ExtEnv):
special_role_players: list[str] = Field(default=[])
winner: Optional[str] = Field(default=None)
win_reason: Optional[str] = Field(default=None)
witch_poison_left: int = Field(default=1)
witch_antidote_left: int = Field(default=1)
witch_poison_left: int = Field(default=1, description="should be 1 or 0")
witch_antidote_left: int = Field(default=1, description="should be 1 or 0")
# game current round states, a round is from closing your eyes to the next time you close your eyes
round_hunts: dict[str, str] = Field(default=dict(), description="nighttime wolf hunt result")
round_votes: dict[str, str] = Field(
default=dict(), description="daytime all players vote result, key=voteer, value=voted one"
default=dict(), description="daytime all players vote result, key=voter, value=voted one"
)
player_hunted: Optional[str] = Field(default=None)
player_protected: Optional[str] = Field(default=None)
@ -140,8 +57,63 @@ class WerewolfExtEnv(ExtEnv):
def observe(self, obs_params: Optional[BaseEnvObsParams] = None) -> Any:
pass
def step(self, action: BaseEnvAction) -> tuple[dict[str, Any], float, bool, bool, dict[str, Any]]:
pass
def _get_obs(self):
return {
"game_setup": self.game_setup,
"step_idx": self.step_idx,
"living_players": self.living_players,
"werewolf_players": self.werewolf_players,
"player_hunted": self.player_hunted,
"player_current_dead": self.player_current_dead,
"witch_poison_left": self.witch_poison_left,
"witch_antidote_left": self.witch_antidote_left,
"winner": self.winner,
"win_reason": self.win_reason
}
def step(self, action: EnvAction) -> tuple[dict[str, Any], float, bool, bool, dict[str, Any]]:
action_type = action.action_type
player_name = action.player_name
target_player_name = action.target_player_name
if action_type == EnvActionType.WOLF_KILL:
self.wolf_kill_someone(wolf_name=player_name, player_name=target_player_name)
elif action_type == EnvActionType.VOTE_KILL:
self.vote_kill_someone(voter_name=player_name, player_name=target_player_name)
elif action_type == EnvActionType.WITCH_POISON:
self.witch_poison_someone(witch_name=player_name, player_name=target_player_name)
elif action_type == EnvActionType.WITCH_SAVE:
self.witch_save_someone(witch_name=player_name, player_name=target_player_name)
elif action_type == EnvActionType.GUARD_PROTECT:
self.guard_protect_someone(guard_name=player_name, player_name=target_player_name)
elif action_type == EnvActionType.PROGRESS_STEP:
self.progress_step()
elif action_type == EnvActionType.NONE:
pass
else:
raise ValueError(f"not supported action_type: {action_type}")
self.update_game_states()
terminated = self._check_game_finish()
obs = self._get_obs()
return obs, 1.0, terminated, False, {}
def _check_game_finish(self) -> bool:
"""return True if game finished else False"""
# game's termination condition
terminated = False
living_werewolf = [p for p in self.werewolf_players if p in self.living_players]
living_villagers = [p for p in self.villager_players if p in self.living_players]
living_special_roles = [p for p in self.special_role_players if p in self.living_players]
if not living_werewolf:
self.winner = "good guys"
self.win_reason = "werewolves all dead"
terminated = True
elif not living_villagers or not living_special_roles:
self.winner = "werewolf"
self.win_reason = "villagers all dead" if not living_villagers else "special roles all dead"
terminated = True
return terminated
@property
def living_players(self) -> list[str]:
@ -161,12 +133,12 @@ class WerewolfExtEnv(ExtEnv):
@property
def werewolf_players(self) -> list[str]:
player_names = self._role_type_players(role_type="Werewolf")
player_names = self._role_type_players(role_type=RoleType.WEREWOLF.value)
return player_names
@property
def villager_players(self) -> list[str]:
player_names = self._role_type_players(role_type="Villager")
player_names = self._role_type_players(role_type=RoleType.VILLAGER.value)
return player_names
def _init_players_state(self, players: list["Role"]):
@ -193,14 +165,14 @@ class WerewolfExtEnv(ExtEnv):
"""init players using different roles' num"""
role_objs = []
for role_obj in role_uniq_objs:
if str(role_obj) == "Villager":
if "Villager" in str(role_obj):
role_objs.extend([role_obj] * num_villager)
elif str(role_obj) == "Werewolf":
elif "Werewolf" in str(role_obj):
role_objs.extend([role_obj] * num_werewolf)
else:
role_objs.append(role_obj)
if shuffle:
random.shuffle(len(role_objs))
random.shuffle(role_objs)
if add_human:
assigned_role_idx = random.randint(0, len(role_objs) - 1)
assigned_role = role_objs[assigned_role_idx]
@ -233,10 +205,12 @@ class WerewolfExtEnv(ExtEnv):
roletype_state = self.players_state[player_name]
self.players_state[player_name] = (roletype_state[0], state)
def _check_valid_role(self, player: "Role", role_type: str) -> bool:
return True if role_type in str(player) else False
def _check_valid_role(self, player_name: str, role_type: str) -> bool:
roletype_state = self.players_state.get(player_name)
return True if roletype_state and role_type in roletype_state[0] else False
def _check_player_continue(self, player_name: str, particular_step: int = -1) -> bool:
"""to check if can do the operation to the player"""
step_idx = self.step_idx % self.per_round_steps
if particular_step > 0 and step_idx != particular_step: # step no
# particular_step = 18, not daytime vote time, ignore
@ -253,6 +227,10 @@ class WerewolfExtEnv(ExtEnv):
self.step_idx += 1
return instruction
@mark_as_writeable
def progress_step(self):
self.step_idx += 1
@mark_as_readable
def get_players_state(self, player_names: list[str]) -> dict[str, RoleState]:
players_state = {
@ -263,14 +241,14 @@ class WerewolfExtEnv(ExtEnv):
return players_state
@mark_as_writeable
def vote_kill_someone(self, voteer: "Role", player_name: str = None):
def vote_kill_someone(self, voter_name: str, player_name: str = None):
"""player vote result at daytime
player_name: if it's None, regard as abstaining from voting
"""
if not self._check_player_continue(voteer.name, particular_step=18): # 18=step no
if not self._check_player_continue(voter_name, particular_step=18): # 18=step no
return
self.round_votes[voteer.name] = player_name
self.round_votes[voter_name] = player_name
# check if all living players finish voting, then get the dead one
if list(self.round_votes.keys()) == self.living_players:
voted_all = list(self.round_votes.values()) # TODO in case of tie vote, check who was voted first
@ -279,41 +257,55 @@ class WerewolfExtEnv(ExtEnv):
self._update_players_state([self.player_current_dead])
@mark_as_writeable
def wolf_kill_someone(self, wolf: "Role", player_name: str):
if not self._check_valid_role(wolf, "Werewolf"):
def wolf_kill_someone(self, wolf_name: str, player_name: str):
if not self._check_valid_role(wolf_name, RoleType.WEREWOLF.value):
return
if not self._check_player_continue(wolf.name, particular_step=5): # 5=step no
if not self._check_player_continue(wolf_name, particular_step=5): # 5=step no
return
self.round_hunts[wolf.name] = player_name
living_werewolf = [p for p in self.werewolf_players if p in self.living_players]
self.round_hunts[wolf_name] = player_name
# living_werewolf = [p for p in self.werewolf_players if p in self.living_players]
# check if all living wolfs finish hunting, then get the hunted one
if list(self.round_hunts.keys()) == living_werewolf:
hunted_all = list(self.round_hunts.values())
self.player_hunted = Counter(hunted_all).most_common()[0][0]
# if list(self.round_hunts.keys()) == living_werewolf:
# hunted_all = list(self.round_hunts.values())
# self.player_hunted = Counter(hunted_all).most_common()[0][0]
self.player_hunted = player_name
@mark_as_writeable
def witch_poison_someone(self, witch: "Role", player_name: str = None):
if not self._check_valid_role(witch, "Witch"):
def _witch_poison_or_save_someone(self, witch_name: str, player_name: str = None,
state: RoleState = RoleState.POISONED):
if not self._check_valid_role(witch_name, RoleType.WITCH.value):
return
if not self._check_player_continue(player_name):
return
self._update_players_state([player_name], RoleState.POISONED)
self.player_poisoned = player_name
assert state in [RoleState.POISONED, RoleState.SAVED]
self._update_players_state([player_name], state)
if state == RoleState.POISONED:
self.player_poisoned = player_name
self.witch_poison_left -= 1
else:
# self.player_protected = player_name
self.is_hunted_player_saved = True
self.witch_antidote_left -= 1
@mark_as_writeable
def witch_save_someone(self, witch: "Role", player_name: str = None):
if not self._check_valid_role(witch, "Witch"):
def witch_poison_someone(self, witch_name: str, player_name: str = None):
self._witch_poison_or_save_someone(witch_name, player_name, RoleState.POISONED)
@mark_as_writeable
def witch_save_someone(self, witch_name: str, player_name: str = None):
self._witch_poison_or_save_someone(witch_name, player_name, RoleState.SAVED)
@mark_as_writeable
def guard_protect_someone(self, guard_name: str, player_name: str = None):
if not self._check_valid_role(guard_name, RoleType.GUARD.value):
return
if not self._check_player_continue(player_name):
return
self._update_players_state([player_name], RoleState.SAVED)
self.player_protected = player_name
@mark_as_writeable
def update_game_states(self, memories: list):
def update_game_states(self):
step_idx = self.step_idx % self.per_round_steps
if step_idx not in [15, 18] or self.step_idx in self.eval_step_idx:
return
@ -335,16 +327,6 @@ class WerewolfExtEnv(ExtEnv):
self.player_protected = None
self.is_hunted_player_saved = False
self.player_poisoned = None
# game's termination condition
living_werewolf = [p for p in self.werewolf_players if p in self.living_players]
living_villagers = [p for p in self.villager_players if p in self.living_players]
living_special_roles = [p for p in self.special_role_players if p in self.living_players]
if not living_werewolf:
self.winner = "good guys"
self.win_reason = "werewolves all dead"
elif not living_villagers or not living_special_roles:
self.winner = "werewolf"
self.win_reason = "villagers all dead" if not living_villagers else "special roles all dead"
if self.winner is not None:
self._record_all_experiences() # TODO
elif step_idx == 18:
# updated use vote_kill_someone
pass

View file

@ -1,12 +1,13 @@
from metagpt.actions import Action
from metagpt.environment.werewolf.werewolf_ext_env import STEP_INSTRUCTIONS
from metagpt.environment.werewolf.const import STEP_INSTRUCTIONS
class InstructSpeak(Action):
name: str = "InstructSpeak"
async def run(self, step_idx, living_players, werewolf_players, player_hunted, player_current_dead):
instruction_info = STEP_INSTRUCTIONS.get(step_idx, {"content": "Unknown instruction.", "send_to": {}})
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=living_players, werewolf_players=werewolf_players)
@ -20,7 +21,7 @@ class InstructSpeak(Action):
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"]
return content, instruction_info["send_to"], instruction_info["restricted_to"]
class ParseSpeak(Action):

View file

@ -1,4 +1,5 @@
from metagpt.ext.werewolf.actions.common_actions import NighttimeWhispers
from metagpt.environment.werewolf.const import RoleActionRes
class Save(NighttimeWhispers):
@ -41,6 +42,6 @@ class Poison(NighttimeWhispers):
async def run(self, *args, **kwargs):
rsp = await super().run(*args, **kwargs)
if "pass" in rsp.lower():
if RoleActionRes.PASS.value in rsp.lower():
action_name, rsp = rsp.split() # 带PASS只需回复PASS不需要带上action名否则是Poison PlayerX无需改动
return rsp

View file

@ -1,5 +1,7 @@
import re
from pydantic import SerializeAsAny, Field
from metagpt.ext.werewolf.actions import (
ACTIONS,
AddNewExperiences,
@ -9,65 +11,72 @@ from metagpt.ext.werewolf.actions import (
RetrieveExperiences,
Speak,
)
from metagpt.ext.werewolf.schema import RoleExperience
from metagpt.ext.werewolf.schema import RoleExperience, WwMessage
from metagpt.environment.werewolf.const import RoleType, RoleState
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.actions.action import Action
class BasePlayer(Role):
def __init__(
self,
name: str = "PlayerXYZ",
profile: str = "BasePlayer",
special_action_names: list[str] = [],
use_reflection: bool = True,
use_experience: bool = False,
use_memory_selection: bool = False,
new_experience_version: str = "",
**kwargs,
):
super().__init__(name, profile, **kwargs)
# 通过 set_status() 更新状态。
self.status = 0 # 0代表活着1代表死亡
name: str = "PlayerXYZ"
profile: str = "BasePlayer"
special_action_names: list[str] = []
use_reflection: bool = True
use_experience: bool = False
use_memory_selection: bool = False
new_experience_version: str = ""
status: RoleState = RoleState.ALIVE
special_actions: list[SerializeAsAny[Action]] = Field(default=[], validate_default=True)
experiences: list[RoleExperience] = []
def __init__(self, **kwargs):
super().__init__(**kwargs)
# 技能和监听配置
self._watch([InstructSpeak]) # 监听Moderator的指令以做行动
special_actions = [ACTIONS[action_name] for action_name in special_action_names]
special_actions = [ACTIONS[action_name] for action_name in self.special_action_names]
capable_actions = [Speak] + special_actions
self.set_actions(capable_actions) # 给角色赋予行动技能
self.special_actions = special_actions
self.use_reflection = use_reflection
if not self.use_reflection and use_experience:
if not self.use_reflection and self.use_experience:
logger.warning("You must enable use_reflection before using experience")
self.use_experience = False
else:
self.use_experience = use_experience
self.new_experience_version = new_experience_version
self.use_memory_selection = use_memory_selection
self.experiences = []
async def _observe(self) -> int:
if self.status == 1:
async def _observe(self, ignore_memory=False) -> int:
if self.status != RoleState.ALIVE:
# 死者不再参与游戏
return 0
news = []
if not news:
news = self.rc.msg_buffer.pop_all()
old_messages = [] if ignore_memory else self.rc.memory.get()
for m in news:
if len(m.restricted_to) and self.profile not in m.restricted_to and self.name not in m.restricted_to:
# if the msg is not send to the whole audience ("") nor this role (self.profile or self.name),
# then this role should not be able to receive it and record it into its memory
continue
self.rc.memory.add(m)
self.rc.news = [
n for n in news if (n.cause_by in self.rc.watch or self.profile in n.send_to) and n not in old_messages
]
await super()._observe()
# 只有发给全体的(""或发给自己的self.profile消息需要走下面的_react流程
# 其他的收听到即可,不用做动作
self.rc.news = [msg for msg in self.rc.news if msg.send_to in ["", self.profile]]
# TODO to delete
# 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.send_to:
if not news.restricted_to:
# 消息接收范围为全体角色的,做公开发言(发表投票观点也算发言)
self.rc.todo = Speak()
elif self.profile in news.send_to:
# FIXME: hard code to split, restricted为"Moderator"或"Moderator,角色profile"
elif self.profile in news.restricted_to:
# FIXME: hard code to split, restricted为"Moderator"或"Moderator, 角色profile"
# Moderator加密发给自己的意味着要执行角色的特殊动作
self.rc.todo = self.special_actions[0]()
@ -79,7 +88,6 @@ class BasePlayer(Role):
# 可以用这个函数获取该角色的全部记忆和最新的instruction
memories = self.get_all_memories()
latest_instruction = self.get_latest_instruction()
# print("*" * 10, f"{self._setting}'s current memories: {memories}", "*" * 10)
reflection = (
await Reflect().run(
@ -107,20 +115,21 @@ class BasePlayer(Role):
reflection=reflection,
experiences=experiences,
)
send_to = ""
restricted_to = {}
elif isinstance(todo, NighttimeWhispers):
rsp = await todo.run(
profile=self.profile, name=self.name, context=memories, reflection=reflection, experiences=experiences
)
send_to = {"Moderator", self.profile} # 给Moderator发送使用特殊技能的加密消息
restricted_to = {RoleType.MODERATOR.value, self.profile} # 给Moderator发送使用特殊技能的加密消息
msg = Message(
msg = WwMessage(
content=rsp,
role=self.profile,
sent_from=self.name,
cause_by=type(todo),
send_to=send_to,
send_to={},
restricted_to=restricted_to
)
self.experiences.append(
@ -149,7 +158,7 @@ class BasePlayer(Role):
def get_latest_instruction(self) -> str:
return self.rc.important_memory[-1].content # 角色监听着Moderator的InstructSpeak是其重要记忆直接获取即可
def set_status(self, new_status):
def set_status(self, new_status: RoleState):
self.status = new_status
def record_experiences(self, round_id: str, outcome: str, game_setup: str):

View file

@ -1,7 +1,8 @@
from metagpt.ext.werewolf.roles.base_player import BasePlayer
from metagpt.environment.werewolf.const import RoleType
class Guard(BasePlayer):
name: str = "Guard"
profile: str = "Guard"
name: str = RoleType.GUARD.value
profile: str = RoleType.GUARD.value
special_action_names: list[str] = ["Protect"]

View file

@ -1,7 +1,8 @@
from metagpt.ext.werewolf.actions import Speak
from metagpt.ext.werewolf.roles import BasePlayer
from metagpt.logs import logger
from metagpt.schema import Message
from metagpt.ext.werewolf.schema import WwMessage
from metagpt.environment.werewolf.const import RoleType
async def _act(self):
@ -22,14 +23,15 @@ async def _act(self):
rsp = input(input_instruction) # wait for human input
msg_cause_by = type(todo)
msg_restricted_to = "" if isinstance(todo, Speak) else f"Moderator,{self.profile}"
msg_restricted_to = {} if isinstance(todo, Speak) else {RoleType.MODERATOR.value, self.profile}
msg = Message(
msg = WwMessage(
content=rsp,
role=self.profile,
sent_from=self.name,
cause_by=msg_cause_by,
send_to=msg_restricted_to, # 给Moderator及自身阵营发送加密消息
send_to={},
restricted_to=msg_restricted_to, # 给Moderator及自身阵营发送加密消息
)
logger.info(f"{self._setting}: {rsp}")

View file

@ -1,10 +1,11 @@
import re
from collections import Counter
from typing import Union
from datetime import datetime
from metagpt.actions.add_requirement import UserRequirement
from metagpt.const import DEFAULT_WORKSPACE_ROOT
from metagpt.environment.werewolf.werewolf_ext_env import STEP_INSTRUCTIONS
from metagpt.environment.werewolf.const import STEP_INSTRUCTIONS, RoleType
from metagpt.environment.werewolf.env_space import EnvAction, EnvActionType
from metagpt.ext.werewolf.actions import Hunt, Poison, Protect, Save, Verify
from metagpt.ext.werewolf.actions.moderator_actions import (
AnnounceGameResult,
@ -12,55 +13,29 @@ from metagpt.ext.werewolf.actions.moderator_actions import (
ParseSpeak,
)
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.ext.werewolf.roles.base_player import BasePlayer
from metagpt.ext.werewolf.schema import WwMessage
from metagpt.environment.werewolf.const import RoleState, RoleActionRes
class Moderator(Role):
name: str = "Moderator"
profile: str = "Moderator"
class Moderator(BasePlayer):
name: str = RoleType.MODERATOR.value
profile: str = RoleType.MODERATOR.value
def __init__(
self,
name: str = "Moderator",
profile: str = "Moderator",
**kwargs,
):
super().__init__(name, profile, **kwargs)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._watch([UserRequirement, InstructSpeak, ParseSpeak])
self.set_actions([InstructSpeak, ParseSpeak, AnnounceGameResult])
self.step_idx = 0
self.eval_step_idx = []
# game states
self.step_idx = 0
self.game_setup = ""
self.living_players = []
self.werewolf_players = []
self.villager_players = []
self.special_role_players = []
self.winner = None
self.win_reason = None
self.witch_poison_left = 1
self.witch_antidote_left = 1
# player states of current night
self.player_hunted = None
self.player_protected = None
self.is_hunted_player_saved = False
self.player_poisoned = None
self.player_current_dead = []
def _parse_game_setup(self, game_setup: str):
self.game_setup = game_setup
self.living_players = re.findall(r"Player[0-9]+", game_setup)
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.villager_players = re.findall(r"Player[0-9]+: Villager", game_setup)
self.villager_players = [p.replace(": Villager", "") for p in self.villager_players]
self.special_role_players = [
p for p in self.living_players if p not in self.werewolf_players + self.villager_players
]
def update_player_status(self, player_names: list[str]):
if not player_names:
return
@ -68,33 +43,23 @@ class Moderator(Role):
for role_setting, role in roles_in_env.items():
for player_name in player_names:
if player_name in role_setting:
role.set_status(new_status=1) # 更新为死亡
role.set_status(new_status=RoleState.DEAD) # 更新为死亡
def _record_all_experiences(self):
logger.info(f"The winner of the game: {self.winner}, start to record roles' experiences")
roles_in_env = self.rc.env.get_roles()
timestamp = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
for _, role in roles_in_env.items():
if role == self:
continue
if self.winner == "werewolf":
outcome = "won" if role.name in self.werewolf_players else "lost"
outcome = "won" if role.profile in RoleType.WEREWOLF.value else "lost"
else:
outcome = "won" if role.name not in self.werewolf_players else "lost"
outcome = "won" if role.profile not in RoleType.WEREWOLF.value else "lost"
role.record_experiences(round_id=timestamp, outcome=outcome, game_setup=self.game_setup)
async def _instruct_speak(self):
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,
player_hunted=self.player_hunted,
player_current_dead=self.player_current_dead,
)
async def _parse_speak(self, memories):
logger.info(self.step_idx)
logger.info(f"step_idx: {self.step_idx}")
latest_msg = memories[-1]
latest_msg_content = latest_msg.content
@ -104,157 +69,135 @@ class Moderator(Role):
# default return
msg_content = "Understood"
send_to = set()
restricted_to = set()
msg_cause_by = latest_msg.cause_by
if msg_cause_by == Hunt:
self.player_hunted = target
self.rc.env.step(EnvAction(action_type=EnvActionType.WOLF_KILL, player_name=latest_msg.send_from,
target_player_name=target))
elif msg_cause_by == Protect:
self.player_protected = target
self.rc.env.step(EnvAction(action_type=EnvActionType.GUARD_PROTECT, player_name=latest_msg.send_from,
target_player_name=target))
elif msg_cause_by == Verify:
if target in self.werewolf_players:
msg_content = f"{target} is a werewolf"
else:
msg_content = f"{target} is a good guy"
send_to = {"Moderator", "Seer"}
restricted_to = {RoleType.MODERATOR.value, RoleType.SEER.value}
elif msg_cause_by == Save:
if "pass" in latest_msg_content.lower():
if RoleActionRes.PASS.value in latest_msg_content.lower():
# the role ignore to response, answer `pass`
pass
elif not self.witch_antidote_left:
msg_content = "You have no antidote left and thus can not save the player"
send_to = {"Moderator", "Witch"}
restricted_to = {RoleType.MODERATOR.value, RoleType.WITCH.value}
else:
self.witch_antidote_left -= 1
self.is_hunted_player_saved = True
self.rc.env.step(EnvAction(action_type=EnvActionType.WITCH_SAVE, player_name=latest_msg.send_from,
target_player_name=target))
elif msg_cause_by == Poison:
if "pass" in latest_msg_content.lower():
if RoleActionRes.PASS.value 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"
send_to = {"Moderator", "Witch"}
restricted_to = {RoleType.MODERATOR.value, RoleType.WITCH.value}
else:
self.witch_poison_left -= 1
self.player_poisoned = target # "" if not poisoned and "PlayerX" if poisoned
self.rc.env.step(EnvAction(action_type=EnvActionType.WITCH_POISON, player_name=latest_msg.send_from,
target_player_name=target))
return msg_content, send_to
return msg_content, restricted_to
def _update_game_states(self, memories):
step_idx = self.step_idx % len(STEP_INSTRUCTIONS)
if step_idx not in [15, 18] or self.step_idx in self.eval_step_idx: # FIXME: hard code
return
else:
self.eval_step_idx.append(self.step_idx) # record evaluation, avoid repetitive evaluation at the same step
def _update_player_status(self, step_idx: int, player_current_dead: list[str]):
"""update dead player's status"""
if step_idx in [15, 18]:
self.update_player_status(player_current_dead)
if step_idx == 15: # FIXME: hard code
# night ends: after all special roles acted, process the whole night
self.player_current_dead = [] # reset
if self.player_hunted != self.player_protected and not self.is_hunted_player_saved:
self.player_current_dead.append(self.player_hunted)
if self.player_poisoned:
self.player_current_dead.append(self.player_poisoned)
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)
# reset
self.player_hunted = None
self.player_protected = None
self.is_hunted_player_saved = False
self.player_poisoned = None
elif step_idx == 18: # FIXME: hard code
# day ends: after all roles voted, process all votings
voting_msgs = memories[-len(self.living_players) :]
voted_all = []
for msg in voting_msgs:
voted = re.search(r"Player[0-9]+", msg.content[-10:])
if not voted:
continue
voted_all.append(voted.group(0))
self.player_current_dead = [Counter(voted_all).most_common()[0][0]] # 平票时,杀最先被投的
# print("*" * 10, "dead", self.player_current_dead)
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)
# game's termination condition
living_werewolf = [p for p in self.werewolf_players if p in self.living_players]
living_villagers = [p for p in self.villager_players if p in self.living_players]
living_special_roles = [p for p in self.special_role_players if p in self.living_players]
if not living_werewolf:
self.winner = "good guys"
self.win_reason = "werewolves all dead"
elif not living_villagers or not living_special_roles:
self.winner = "werewolf"
self.win_reason = "villagers all dead" if not living_villagers else "special roles all dead"
if self.winner is not None:
self._record_all_experiences()
def _record_game_history(self):
if self.step_idx % len(STEP_INSTRUCTIONS) == 0 or self.winner is not None:
def _record_game_history(self, step_idx: int):
if step_idx % len(STEP_INSTRUCTIONS) == 0 or self.winner:
logger.info("a night and day cycle completed, examine all history")
print(self.get_all_memories())
logger.debug(f"all_memories: {self.get_all_memories()}")
with open(DEFAULT_WORKSPACE_ROOT / "werewolf_transcript.txt", "w") as f:
f.write(self.get_all_memories())
async def _think(self):
if self.winner is not None:
if self.winner:
self.rc.todo = AnnounceGameResult()
return
latest_msg = self.rc.memory.get()[-1]
if latest_msg.role in ["User"]:
# 上一轮消息是用户指令,解析用户指令,开始游戏
game_setup = latest_msg.content
self._parse_game_setup(game_setup)
if latest_msg.role in ["User", "Human", self.profile]:
# 1. 上一轮消息是用户指令,解析用户指令,开始游戏
# 2.1. 上一轮消息是Moderator自己的指令继续发出指令一个事情可以分几条消息来说
# 2.2. 上一轮消息是Moderator自己的解析消息一个阶段结束发出新一个阶段的指令
self.rc.todo = InstructSpeak()
elif latest_msg.role in [self.profile]:
# 1. 上一轮消息是Moderator自己的指令继续发出指令一个事情可以分几条消息来说
# 2. 上一轮消息是Moderator自己的解析消息一个阶段结束发出新一个阶段的指令
self.rc.todo = InstructSpeak()
else:
# 上一轮消息是游戏角色的发言,解析角色的发言
self.rc.todo = ParseSpeak()
def _init_fields_from_obj(self, obs: dict[str, Union[int, str, list[str]]]):
self.game_setup = obs.get("game_setup", "")
self.winner = obs.get("winner")
self.win_reason = obs.get("win_reason")
self.werewolf_players = obs.get("werewolf_players", [])
self.witch_poison_left = obs.get("witch_poison_left", 0)
self.witch_antidote_left = obs.get("witch_antidote_left", 0)
async def _act(self):
todo = self.rc.todo
logger.info(f"{self._setting} ready to {todo}")
memories = self.get_all_memories(mode="msg")
# 若进行完一夜一日的循环,打印和记录一次完整发言历史
self._record_game_history()
obs, _, _, _, _ = self.rc.env.step(action=EnvAction(action_type=EnvActionType.NONE))
step_idx = obs["step_idx"]
living_players = obs["living_players"]
werewolf_players = obs["werewolf_players"]
player_hunted = obs["player_hunted"]
player_current_dead = obs["player_current_dead"]
self._init_fields_from_obj(obs)
# 若一晚或一日周期结束,对当晚或当日的死者进行总结,并更新游戏状态
self._update_game_states(memories)
# 若进行完一夜一日的循环,打印和记录一次完整发言历史
self._record_game_history(step_idx)
# 若一晚或一日周期结束,对当晚或当日的死者进行总结,并更新玩家状态
self._update_player_status(step_idx, player_current_dead)
if self.winner:
self._record_all_experiences()
# 根据_think的结果执行InstructSpeak还是ParseSpeak, 并将结果返回
if isinstance(todo, InstructSpeak):
msg_content, msg_to_send_to = await self._instruct_speak()
msg_content, msg_to_send_to, msg_restricted_to = await InstructSpeak().run(
step_idx,
living_players=living_players,
werewolf_players=werewolf_players,
player_hunted=player_hunted,
player_current_dead=player_current_dead,
)
# msg_content = f"Step {self.step_idx}: {msg_content}" # HACK: 加一个unique的step_idx避免记忆的自动去重
msg = Message(
msg = WwMessage(
content=msg_content,
role=self.profile,
sent_from=self.name,
cause_by=InstructSpeak,
send_to=msg_to_send_to,
restricted_to=msg_restricted_to
)
self.rc.env.step(EnvAction(action_type=EnvActionType.PROGRESS_STEP)) # to update step_idx
elif isinstance(todo, ParseSpeak):
msg_content, msg_to_send_to = await self._parse_speak(memories)
msg_content, msg_restricted_to = await self._parse_speak(memories)
# msg_content = f"Step {self.step_idx}: {msg_content}" # HACK: 加一个unique的step_idx避免记忆的自动去重
msg = Message(
msg = WwMessage(
content=msg_content,
role=self.profile,
sent_from=self.name,
cause_by=ParseSpeak,
send_to=msg_to_send_to,
send_to={},
restricted_to=msg_restricted_to
)
elif isinstance(todo, AnnounceGameResult):
msg_content = await AnnounceGameResult().run(winner=self.winner, win_reason=self.win_reason)
msg = Message(content=msg_content, role=self.profile, sent_from=self.name, cause_by=AnnounceGameResult)
msg = WwMessage(content=msg_content, role=self.profile, sent_from=self.name, cause_by=AnnounceGameResult)
logger.info(f"{self._setting}: {msg_content}")

View file

@ -1,7 +1,8 @@
from metagpt.ext.werewolf.roles.base_player import BasePlayer
from metagpt.environment.werewolf.const import RoleType
class Seer(BasePlayer):
name: str = "Seer"
profile: str = "Seer"
name: str = RoleType.SEER.value
profile: str = RoleType.SEER.value
special_action_names: list[str] = ["Verify"]

View file

@ -1,7 +1,8 @@
from metagpt.ext.werewolf.roles.base_player import BasePlayer
from metagpt.environment.werewolf.const import RoleType
class Villager(BasePlayer):
name: str = "Villager"
profile: str = "Villager"
name: str = RoleType.VILLAGER.value
profile: str = RoleType.VILLAGER.value
special_action_names: list[str] = []

View file

@ -1,10 +1,11 @@
from metagpt.ext.werewolf.actions import Impersonate, Speak
from metagpt.ext.werewolf.roles.base_player import BasePlayer
from metagpt.environment.werewolf.const import RoleType
class Werewolf(BasePlayer):
name: str = "Werewolf"
profile: str = "Werewolf"
name: str = RoleType.WEREWOLF.value
profile: str = RoleType.WEREWOLF.value
special_action_names: list[str] = ["Hunt"]
async def _think(self):

View file

@ -1,20 +1,21 @@
from metagpt.ext.werewolf.actions import InstructSpeak, Poison, Save, Speak
from metagpt.ext.werewolf.roles.base_player import BasePlayer
from metagpt.environment.werewolf.const import RoleType
class Witch(BasePlayer):
name: str = "Witch"
profile: str = "Witch"
name: str = RoleType.WITCH.value
profile: str = RoleType.WITCH.value
special_action_names: list[str] = ["Save", "Poison"]
async def _think(self):
"""女巫涉及两个特殊技能因此在此需要改写_think进行路由"""
news = self.rc.news[0]
assert news.cause_by == InstructSpeak # 消息为来自Moderator的指令时才去做动作
if not news.send_to:
if not news.restricted_to:
# 消息接收范围为全体角色的,做公开发言(发表投票观点也算发言)
self.rc.todo = Speak()
elif self.profile in news.send_to:
elif self.profile in news.restricted_to:
# FIXME: hard code to split, restricted为"Moderator"或"Moderator,角色profile"
# Moderator加密发给自己的意味着要执行角色的特殊动作
# 这里用关键词进行动作的选择需要Moderator侧的指令进行配合

View file

@ -1,4 +1,6 @@
from pydantic import BaseModel
from pydantic import BaseModel, Field
from metagpt.schema import Message
class RoleExperience(BaseModel):
@ -12,3 +14,8 @@ class RoleExperience(BaseModel):
round_id: str = ""
game_setup: str = ""
version: str = ""
class WwMessage(Message):
# Werewolf Message
restricted_to: set[str] = Field(default={}, validate_default=True)

View file

@ -3,7 +3,7 @@ from typing import Any, Optional
from metagpt.actions.add_requirement import UserRequirement
from metagpt.context import Context
from metagpt.environment.werewolf.werewolf_env import WerewolfEnv
from metagpt.schema import Message
from metagpt.ext.werewolf.schema import WwMessage
from metagpt.team import Team
@ -23,4 +23,4 @@ class WerewolfGame(Team):
def run_project(self, idea):
"""Run a project from user instruction."""
self.idea = idea
self.env.publish_message(Message(role="User", content=idea, cause_by=UserRequirement, send_to={"Moderator"}))
self.env.publish_message(WwMessage(role="User", content=idea, cause_by=UserRequirement, restricted_to={"Moderator"}))

View file

@ -2,7 +2,8 @@
# -*- coding: utf-8 -*-
# @Desc : the unittest of WerewolfExtEnv
from metagpt.environment.werewolf.werewolf_ext_env import RoleState, WerewolfExtEnv
from metagpt.environment.werewolf.werewolf_ext_env import WerewolfExtEnv
from metagpt.environment.werewolf.const import RoleState
from metagpt.roles.role import Role
@ -41,9 +42,9 @@ def test_werewolf_ext_env():
assert "Werewolves, please open your eyes" in curr_instr["content"]
# current step_idx = 5
ext_env.wolf_kill_someone(wolf=Role(name="Player10"), player_name="Player4")
ext_env.wolf_kill_someone(wolf=Werewolf(name="Player0"), player_name="Player4")
ext_env.wolf_kill_someone(wolf=Werewolf(name="Player1"), player_name="Player4")
ext_env.wolf_kill_someone(wolf_name="Player10", player_name="Player4")
ext_env.wolf_kill_someone(wolf_name="Player0", player_name="Player4")
ext_env.wolf_kill_someone(wolf_name="Player1", player_name="Player4")
assert ext_env.player_hunted == "Player4"
assert len(ext_env.living_players) == 5 # hunted but can be saved by witch
@ -52,11 +53,11 @@ def test_werewolf_ext_env():
# current step_idx = 18
assert ext_env.step_idx == 18
ext_env.vote_kill_someone(voteer=Werewolf(name="Player0"), player_name="Player2")
ext_env.vote_kill_someone(voteer=Werewolf(name="Player1"), player_name="Player3")
ext_env.vote_kill_someone(voteer=Villager(name="Player2"), player_name="Player3")
ext_env.vote_kill_someone(voteer=Witch(name="Player3"), player_name="Player4")
ext_env.vote_kill_someone(voteer=Guard(name="Player4"), player_name="Player2")
ext_env.vote_kill_someone(voter_name="Player0", player_name="Player2")
ext_env.vote_kill_someone(voter_name="Player1", player_name="Player3")
ext_env.vote_kill_someone(voter_name="Player2", player_name="Player3")
ext_env.vote_kill_someone(voter_name="Player3", player_name="Player4")
ext_env.vote_kill_someone(voter_name="Player4", player_name="Player2")
assert ext_env.player_current_dead == "Player2"
assert len(ext_env.living_players) == 4