kaggle-environments 1.20.0__py3-none-any.whl → 1.21.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kaggle-environments might be problematic. Click here for more details.
- kaggle_environments/__init__.py +2 -2
- kaggle_environments/envs/cabt/cabt.js +8 -8
- kaggle_environments/envs/cabt/cg/cg.dll +0 -0
- kaggle_environments/envs/cabt/cg/libcg.so +0 -0
- kaggle_environments/envs/{open_spiel → open_spiel_env}/games/universal_poker/universal_poker.js +52 -28
- kaggle_environments/envs/{open_spiel/open_spiel.py → open_spiel_env/open_spiel_env.py} +37 -1
- kaggle_environments/envs/{open_spiel/test_open_spiel.py → open_spiel_env/test_open_spiel_env.py} +65 -1
- kaggle_environments/envs/werewolf/GAME_RULE.md +75 -0
- kaggle_environments/envs/werewolf/__init__.py +0 -0
- kaggle_environments/envs/werewolf/game/__init__.py +0 -0
- kaggle_environments/envs/werewolf/game/actions.py +268 -0
- kaggle_environments/envs/werewolf/game/base.py +115 -0
- kaggle_environments/envs/werewolf/game/consts.py +156 -0
- kaggle_environments/envs/werewolf/game/engine.py +580 -0
- kaggle_environments/envs/werewolf/game/night_elimination_manager.py +101 -0
- kaggle_environments/envs/werewolf/game/protocols/__init__.py +4 -0
- kaggle_environments/envs/werewolf/game/protocols/base.py +242 -0
- kaggle_environments/envs/werewolf/game/protocols/bid.py +248 -0
- kaggle_environments/envs/werewolf/game/protocols/chat.py +467 -0
- kaggle_environments/envs/werewolf/game/protocols/factory.py +59 -0
- kaggle_environments/envs/werewolf/game/protocols/vote.py +471 -0
- kaggle_environments/envs/werewolf/game/records.py +334 -0
- kaggle_environments/envs/werewolf/game/roles.py +326 -0
- kaggle_environments/envs/werewolf/game/states.py +214 -0
- kaggle_environments/envs/werewolf/game/test_actions.py +45 -0
- kaggle_environments/envs/werewolf/test_werewolf.py +161 -0
- kaggle_environments/envs/werewolf/test_werewolf_deterministic.py +211 -0
- kaggle_environments/envs/werewolf/werewolf.js +4377 -0
- kaggle_environments/envs/werewolf/werewolf.json +286 -0
- kaggle_environments/envs/werewolf/werewolf.py +602 -0
- kaggle_environments/static/player.html +19 -1
- {kaggle_environments-1.20.0.dist-info → kaggle_environments-1.21.0.dist-info}/METADATA +9 -4
- {kaggle_environments-1.20.0.dist-info → kaggle_environments-1.21.0.dist-info}/RECORD +55 -36
- kaggle_environments/envs/chess/chess.js +0 -4289
- kaggle_environments/envs/chess/chess.json +0 -60
- kaggle_environments/envs/chess/chess.py +0 -4241
- kaggle_environments/envs/chess/test_chess.py +0 -60
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/__init__.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/__init__.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/chess/chess.js +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/chess/image_config.jsonl +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/chess/openings.jsonl +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/connect_four/__init__.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/connect_four/connect_four.js +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/connect_four/connect_four_proxy.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/go/__init__.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/go/go.js +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/go/go_proxy.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/tic_tac_toe/__init__.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/tic_tac_toe/tic_tac_toe.js +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/tic_tac_toe/tic_tac_toe_proxy.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/universal_poker/__init__.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/universal_poker/universal_poker_proxy.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/html_playthrough_generator.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/observation.py +0 -0
- /kaggle_environments/envs/{open_spiel → open_spiel_env}/proxy.py +0 -0
- {kaggle_environments-1.20.0.dist-info → kaggle_environments-1.21.0.dist-info}/WHEEL +0 -0
- {kaggle_environments-1.20.0.dist-info → kaggle_environments-1.21.0.dist-info}/entry_points.txt +0 -0
- {kaggle_environments-1.20.0.dist-info → kaggle_environments-1.21.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections import defaultdict, deque
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from typing import Any, DefaultDict, Deque, Dict, List, Optional, Sequence, Union
|
|
5
|
+
|
|
6
|
+
from pydantic import ConfigDict, Field, PrivateAttr, computed_field
|
|
7
|
+
|
|
8
|
+
from .base import BaseRole, BaseState, EventHandler, PlayerID
|
|
9
|
+
from .consts import MODERATOR_ID, DetailedPhase, EventName, Phase, PhaseDivider, RevealLevel, RoleConst, Team
|
|
10
|
+
from .records import DataAccessLevel, DataEntry, Event, PhaseDividerDataEntry, PlayerEventView
|
|
11
|
+
from .roles import Player
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EventBus:
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self._subs: DefaultDict[EventName, List[EventHandler]] = defaultdict(list)
|
|
19
|
+
|
|
20
|
+
def register(self, event_name: EventName, handler: EventHandler):
|
|
21
|
+
self._subs[event_name].append(handler)
|
|
22
|
+
|
|
23
|
+
def dispatch(self, entry: Event):
|
|
24
|
+
for handler in self._subs[entry.event_name]:
|
|
25
|
+
handler(entry)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GameState(BaseState):
|
|
29
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
30
|
+
|
|
31
|
+
players: List[Player]
|
|
32
|
+
phase: Phase = Phase.NIGHT
|
|
33
|
+
detailed_phase: DetailedPhase = DetailedPhase.NIGHT_START
|
|
34
|
+
day_count: int = 0
|
|
35
|
+
history: Dict[int, List[Event]] = Field(default_factory=dict)
|
|
36
|
+
wallet: dict[PlayerID, int] = Field(default_factory=dict)
|
|
37
|
+
night_elimination_reveal_level: RevealLevel = RevealLevel.ROLE
|
|
38
|
+
day_exile_reveal_level: RevealLevel = RevealLevel.ROLE
|
|
39
|
+
_id_to_player: Dict[PlayerID, Player] = PrivateAttr(default_factory=dict)
|
|
40
|
+
_event_by_type: Dict[EventName, List[Event]] = PrivateAttr(default_factory=lambda: defaultdict(list))
|
|
41
|
+
_event_queue: Deque[Event] = PrivateAttr(default_factory=deque)
|
|
42
|
+
_night_elimination_player_ids: List[PlayerID] = PrivateAttr(default_factory=list)
|
|
43
|
+
_day_exile_player_ids: List[PlayerID] = PrivateAttr(default_factory=list)
|
|
44
|
+
_event_bus: EventBus = PrivateAttr(default_factory=EventBus)
|
|
45
|
+
|
|
46
|
+
@computed_field
|
|
47
|
+
@cached_property
|
|
48
|
+
def all_player_ids(self) -> List[str]:
|
|
49
|
+
return [player.id for player in self.players]
|
|
50
|
+
|
|
51
|
+
@computed_field
|
|
52
|
+
@cached_property
|
|
53
|
+
def all_unique_roles(self) -> List[BaseRole]:
|
|
54
|
+
role_dict = {player.role.name: player.role for player in self.players}
|
|
55
|
+
return list(role_dict.values())
|
|
56
|
+
|
|
57
|
+
def model_post_init(self, context: Any, /) -> None:
|
|
58
|
+
self._id_to_player = {p.id: p for p in self.players}
|
|
59
|
+
|
|
60
|
+
def get_player_by_id(self, pid: PlayerID):
|
|
61
|
+
return self._id_to_player.get(pid)
|
|
62
|
+
|
|
63
|
+
def get_players_by_role(self, role: RoleConst):
|
|
64
|
+
return [p for p in self.players if p.role.name == role]
|
|
65
|
+
|
|
66
|
+
def get_players_by_team(self, team: Team):
|
|
67
|
+
return [p for p in self.players if p.role.team == team]
|
|
68
|
+
|
|
69
|
+
def alive_players(self):
|
|
70
|
+
return [p for p in self.players if p.alive]
|
|
71
|
+
|
|
72
|
+
def eliminated_players(self):
|
|
73
|
+
return [p for p in self.players if not p.alive]
|
|
74
|
+
|
|
75
|
+
def revealed_players(self) -> Dict[PlayerID, RoleConst | Team | None]:
|
|
76
|
+
revealed = {}
|
|
77
|
+
if self.night_elimination_reveal_level == RevealLevel.ROLE:
|
|
78
|
+
revealed.update({pid: self.get_player_by_id(pid).role.name for pid in self._night_elimination_player_ids})
|
|
79
|
+
elif self.night_elimination_reveal_level == RevealLevel.TEAM:
|
|
80
|
+
revealed.update({pid: self.get_player_by_id(pid).role.team for pid in self._night_elimination_player_ids})
|
|
81
|
+
elif self.night_elimination_reveal_level == RevealLevel.NO_REVEAL:
|
|
82
|
+
revealed.update({pid: None for pid in self._night_elimination_player_ids})
|
|
83
|
+
|
|
84
|
+
if self.day_exile_reveal_level == RevealLevel.ROLE:
|
|
85
|
+
revealed.update({pid: self.get_player_by_id(pid).role.name for pid in self._day_exile_player_ids})
|
|
86
|
+
elif self.day_exile_reveal_level == RevealLevel.TEAM:
|
|
87
|
+
revealed.update({pid: self.get_player_by_id(pid).role.team for pid in self._day_exile_player_ids})
|
|
88
|
+
elif self.day_exile_reveal_level == RevealLevel.NO_REVEAL:
|
|
89
|
+
revealed.update({pid: None for pid in self._day_exile_player_ids})
|
|
90
|
+
return revealed
|
|
91
|
+
|
|
92
|
+
def is_alive(self, player_id: PlayerID):
|
|
93
|
+
return self.get_player_by_id(player_id).alive
|
|
94
|
+
|
|
95
|
+
def alive_players_by_role(self, role: RoleConst):
|
|
96
|
+
return [p for p in self.alive_players() if p.role.name == role]
|
|
97
|
+
|
|
98
|
+
def alive_players_by_team(self, team: Team):
|
|
99
|
+
return [p for p in self.alive_players() if p.role.team == team]
|
|
100
|
+
|
|
101
|
+
def alive_player_counts_per_role(self):
|
|
102
|
+
counts = {role: len(self.alive_players_by_role(role)) for role in RoleConst}
|
|
103
|
+
return counts
|
|
104
|
+
|
|
105
|
+
def alive_player_counts_per_team(self):
|
|
106
|
+
return {team: len(self.alive_players_by_team(team)) for team in Team}
|
|
107
|
+
|
|
108
|
+
_night_eliminate_queue: List[PlayerID] = PrivateAttr(default_factory=list)
|
|
109
|
+
|
|
110
|
+
def queue_eliminate(self, target: Player):
|
|
111
|
+
self._night_eliminate_queue.append(target.id)
|
|
112
|
+
|
|
113
|
+
def clear_eliminate_queue(self):
|
|
114
|
+
self._night_eliminate_queue.clear()
|
|
115
|
+
|
|
116
|
+
_night_doctor_save_queue: List[PlayerID] = PrivateAttr(default_factory=list)
|
|
117
|
+
|
|
118
|
+
def queue_doctor_save(self, target: Player):
|
|
119
|
+
self._night_doctor_save_queue.append(target.id)
|
|
120
|
+
|
|
121
|
+
def get_event_by_name(self, event_name: EventName) -> List[Event]:
|
|
122
|
+
return self._event_by_type[event_name]
|
|
123
|
+
|
|
124
|
+
def push_event(
|
|
125
|
+
self,
|
|
126
|
+
description: str,
|
|
127
|
+
event_name: EventName,
|
|
128
|
+
public: bool,
|
|
129
|
+
visible_to: Optional[List[PlayerID]] = None,
|
|
130
|
+
data: Optional[Union[DataEntry, Dict[str, Any]]] = None,
|
|
131
|
+
source=MODERATOR_ID,
|
|
132
|
+
visible_in_ui: bool = True,
|
|
133
|
+
):
|
|
134
|
+
visible_to = visible_to or []
|
|
135
|
+
# Night 0 will use day_count 0, Day 1 will use day_count 1, etc.
|
|
136
|
+
day_key = self.day_count
|
|
137
|
+
self.history.setdefault(day_key, [])
|
|
138
|
+
sys_entry = Event(
|
|
139
|
+
day=day_key,
|
|
140
|
+
phase=self.phase,
|
|
141
|
+
detailed_phase=self.detailed_phase,
|
|
142
|
+
event_name=event_name,
|
|
143
|
+
description=description,
|
|
144
|
+
public=public,
|
|
145
|
+
visible_to=visible_to or [],
|
|
146
|
+
data=data,
|
|
147
|
+
source=source,
|
|
148
|
+
visible_in_ui=visible_in_ui,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
self.history[day_key].append(sys_entry)
|
|
152
|
+
self._event_by_type[event_name].append(sys_entry)
|
|
153
|
+
self._event_queue.append(sys_entry)
|
|
154
|
+
|
|
155
|
+
public_view = sys_entry.view_by_access(user_level=DataAccessLevel.PUBLIC)
|
|
156
|
+
personal_view = sys_entry.view_by_access(user_level=DataAccessLevel.PERSONAL)
|
|
157
|
+
|
|
158
|
+
# observers message pushing below
|
|
159
|
+
if public:
|
|
160
|
+
for player in self.players:
|
|
161
|
+
if player.id == source:
|
|
162
|
+
player.update(personal_view)
|
|
163
|
+
else:
|
|
164
|
+
player.update(public_view)
|
|
165
|
+
else:
|
|
166
|
+
for player_id in visible_to:
|
|
167
|
+
player = self.get_player_by_id(player_id)
|
|
168
|
+
if player:
|
|
169
|
+
if player.id == source:
|
|
170
|
+
player.update(personal_view)
|
|
171
|
+
else:
|
|
172
|
+
player.update(public_view)
|
|
173
|
+
|
|
174
|
+
# publish events
|
|
175
|
+
self._event_bus.dispatch(sys_entry)
|
|
176
|
+
|
|
177
|
+
def add_phase_divider(self, divider: PhaseDivider):
|
|
178
|
+
"""The phase divider is used to clearly separate phase boundary. This is very useful
|
|
179
|
+
for visualizer updates, where some updates naturally takes a time slice of events as input.
|
|
180
|
+
"""
|
|
181
|
+
self.push_event(
|
|
182
|
+
description=divider.value,
|
|
183
|
+
event_name=EventName.PHASE_DIVIDER,
|
|
184
|
+
public=False,
|
|
185
|
+
data=PhaseDividerDataEntry(divider_type=divider.value),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def eliminate_player(self, pid: PlayerID):
|
|
189
|
+
if pid not in self.all_player_ids:
|
|
190
|
+
logger.warning(f"Tried to eliminate {pid} who is not within valid player ids {self.all_player_ids}.")
|
|
191
|
+
return
|
|
192
|
+
player = self.get_player_by_id(pid)
|
|
193
|
+
if self.phase == Phase.NIGHT:
|
|
194
|
+
self._night_elimination_player_ids.append(pid)
|
|
195
|
+
else:
|
|
196
|
+
self._day_exile_player_ids.append(pid)
|
|
197
|
+
if player:
|
|
198
|
+
player.eliminate(day=self.day_count, phase=self.phase)
|
|
199
|
+
|
|
200
|
+
def consume_messages(self) -> List[Event]:
|
|
201
|
+
messages = list(self._event_queue)
|
|
202
|
+
self._event_queue.clear()
|
|
203
|
+
return messages
|
|
204
|
+
|
|
205
|
+
def get_elimination_info(self):
|
|
206
|
+
return [player.report_elimination() for player in self.players]
|
|
207
|
+
|
|
208
|
+
def register_event_handler(self, event_name: EventName, handler: EventHandler):
|
|
209
|
+
self._event_bus.register(event_name, handler)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_last_action_request(event_views: Sequence[PlayerEventView], event_name: EventName) -> None | PlayerEventView:
|
|
213
|
+
"""Get the action request from the new player history entry view updates."""
|
|
214
|
+
return next((entry for entry in event_views if entry.event_name == event_name), None)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from kaggle_environments.envs.werewolf.game.actions import filter_language
|
|
4
|
+
|
|
5
|
+
test_data = [
|
|
6
|
+
# Test 1: Basic lowercase substitution
|
|
7
|
+
("We must kill the monster.", "We must eliminate the monster."),
|
|
8
|
+
# Test 2: Title case substitution
|
|
9
|
+
("Killing is wrong.", "Eliminating is wrong."),
|
|
10
|
+
# Test 3: Uppercase substitution
|
|
11
|
+
("The town should not LYNCH anyone.", "The town should not EXILE anyone."),
|
|
12
|
+
# Test 4: Word boundary check (should not affect "skill")
|
|
13
|
+
("His skill is unparalleled.", "His skill is unparalleled."),
|
|
14
|
+
# Test 5: Mixed case and multiple substitutions
|
|
15
|
+
(
|
|
16
|
+
"The Mob will lynch the player they think will Kill them.",
|
|
17
|
+
"The Mob will exile the player they think will Eliminate them.",
|
|
18
|
+
),
|
|
19
|
+
# Test 6: Handling different word endings ('-ed', '-s')
|
|
20
|
+
("He killed the dragon, and she kills the goblin.", "He eliminated the dragon, and she eliminates the goblin."),
|
|
21
|
+
# Test 7: No inappropriate words, should return original string
|
|
22
|
+
("This is a perfectly safe sentence.", "This is a perfectly safe sentence."),
|
|
23
|
+
# Test 8: A more complex sentence with a third rule ('murder')
|
|
24
|
+
(
|
|
25
|
+
"The detective solved the Murder, preventing the killer from killing again.",
|
|
26
|
+
"The detective solved the Remove, preventing the eliminator from eliminating again.",
|
|
27
|
+
),
|
|
28
|
+
# Test 9: A tricky title case that isn't at the start of a sentence
|
|
29
|
+
("I think Killing is not the answer.", "I think Eliminating is not the answer."),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.parametrize("input_text, expected_text", test_data)
|
|
34
|
+
def test_clean_script_scenarios(input_text, expected_text):
|
|
35
|
+
"""
|
|
36
|
+
Tests the clean_script_preserve_case function with various scenarios.
|
|
37
|
+
"""
|
|
38
|
+
assert filter_language(input_text) == expected_text
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_empty_string():
|
|
42
|
+
"""
|
|
43
|
+
Tests that an empty string input results in an empty string output.
|
|
44
|
+
"""
|
|
45
|
+
assert filter_language("") == ""
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from kaggle_environments import make
|
|
4
|
+
|
|
5
|
+
URLS = {
|
|
6
|
+
"gemini": "https://logos-world.net/wp-content/uploads/2025/01/Google-Gemini-Symbol.png",
|
|
7
|
+
"openai": "https://images.seeklogo.com/logo-png/46/1/chatgpt-logo-png_seeklogo-465219.png",
|
|
8
|
+
"claude": "https://images.seeklogo.com/logo-png/55/1/claude-logo-png_seeklogo-554534.png",
|
|
9
|
+
"grok": "https://images.seeklogo.com/logo-png/61/1/grok-logo-png_seeklogo-613403.png",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def agents_config():
|
|
15
|
+
roles = ["Werewolf", "Werewolf", "Doctor", "Seer", "Villager", "Villager", "Villager"]
|
|
16
|
+
names = [f"player_{i}" for i in range(len(roles))]
|
|
17
|
+
thumbnails = [
|
|
18
|
+
URLS["gemini"],
|
|
19
|
+
URLS["gemini"],
|
|
20
|
+
URLS["openai"],
|
|
21
|
+
URLS["openai"],
|
|
22
|
+
URLS["openai"],
|
|
23
|
+
URLS["claude"],
|
|
24
|
+
URLS["grok"],
|
|
25
|
+
]
|
|
26
|
+
agents_config = [
|
|
27
|
+
{"role": role, "id": name, "agent_id": "random", "thumbnail": url}
|
|
28
|
+
for role, name, url in zip(roles, names, thumbnails)
|
|
29
|
+
]
|
|
30
|
+
return agents_config
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def env(agents_config):
|
|
35
|
+
env = make("werewolf", debug=True, configuration={"agents": agents_config})
|
|
36
|
+
return env
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_load_env(env):
|
|
40
|
+
agents = ["random"] * 7
|
|
41
|
+
env.run(agents)
|
|
42
|
+
|
|
43
|
+
for i, state in enumerate(env.steps):
|
|
44
|
+
env.render_step_ind = i
|
|
45
|
+
env.renderer(state, env)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_discussion_protocol(agents_config):
|
|
49
|
+
env = make(
|
|
50
|
+
"werewolf",
|
|
51
|
+
debug=True,
|
|
52
|
+
configuration={
|
|
53
|
+
"agents": agents_config,
|
|
54
|
+
"discussion_protocol": {"name": "RoundRobinDiscussion", "params": {"max_rounds": 2}},
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
agents = ["random"] * 7
|
|
58
|
+
env.run(agents)
|
|
59
|
+
env.toJSON()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_no_reveal_options(agents_config):
|
|
63
|
+
env = make(
|
|
64
|
+
"werewolf",
|
|
65
|
+
debug=True,
|
|
66
|
+
configuration={
|
|
67
|
+
"agents": agents_config,
|
|
68
|
+
"night_elimination_reveal_level": "no_reveal",
|
|
69
|
+
"day_exile_reveal_level": "no_reveal",
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
agents = ["random"] * 7
|
|
73
|
+
env.run(agents)
|
|
74
|
+
env.toJSON()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_disable_doctor_self_save():
|
|
78
|
+
roles = ["Werewolf", "Werewolf", "Doctor", "Seer", "Villager", "Villager", "Villager"]
|
|
79
|
+
names = [f"player_{i}" for i in range(len(roles))]
|
|
80
|
+
thumbnails = [
|
|
81
|
+
URLS["gemini"],
|
|
82
|
+
URLS["gemini"],
|
|
83
|
+
URLS["openai"],
|
|
84
|
+
URLS["openai"],
|
|
85
|
+
URLS["openai"],
|
|
86
|
+
URLS["claude"],
|
|
87
|
+
URLS["grok"],
|
|
88
|
+
]
|
|
89
|
+
agents_config = [
|
|
90
|
+
{"role": role, "id": name, "agent_id": "random", "thumbnail": url}
|
|
91
|
+
for role, name, url in zip(roles, names, thumbnails)
|
|
92
|
+
]
|
|
93
|
+
agents_config[2]["role_params"] = {"allow_self_save": False}
|
|
94
|
+
env = make(
|
|
95
|
+
"werewolf",
|
|
96
|
+
debug=True,
|
|
97
|
+
configuration={
|
|
98
|
+
"agents": agents_config,
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
agents = ["random"] * 7
|
|
102
|
+
env.run(agents)
|
|
103
|
+
env.toJSON()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_turn_by_turn_bidding_discussion(agents_config):
|
|
107
|
+
"""Tests the bidding -> chat -> bidding -> chat ... cycle."""
|
|
108
|
+
env = make(
|
|
109
|
+
"werewolf",
|
|
110
|
+
debug=True,
|
|
111
|
+
configuration={
|
|
112
|
+
"agents": agents_config,
|
|
113
|
+
"discussion_protocol": {
|
|
114
|
+
"name": "TurnByTurnBiddingDiscussion",
|
|
115
|
+
"params": {
|
|
116
|
+
"bidding": {
|
|
117
|
+
"name": "UrgencyBiddingProtocol",
|
|
118
|
+
},
|
|
119
|
+
"max_turns": 16,
|
|
120
|
+
"bid_result_public": False,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
agents = ["random"] * 7
|
|
126
|
+
env.run(agents)
|
|
127
|
+
env.render(mode="html")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.skip("Slow test, meant for manual testing.")
|
|
131
|
+
def test_llm_players(agents_config):
|
|
132
|
+
env = make("werewolf", debug=True, configuration={"actTimeout": 30, "agents": agents_config})
|
|
133
|
+
agents = [
|
|
134
|
+
"llm/gemini/gemini-2.5-flash",
|
|
135
|
+
"random",
|
|
136
|
+
"llm/gemini/gemini-2.5-flash",
|
|
137
|
+
"llm/gemini/gemini-2.5-flash",
|
|
138
|
+
"llm/gemini/gemini-2.5-flash",
|
|
139
|
+
"random",
|
|
140
|
+
"random",
|
|
141
|
+
]
|
|
142
|
+
env.run(agents)
|
|
143
|
+
for i, state in enumerate(env.steps):
|
|
144
|
+
env.render_step_ind = i
|
|
145
|
+
env.renderer(state, env)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_default_env():
|
|
149
|
+
env = make("werewolf", debug=True)
|
|
150
|
+
agents = ["random"] * 7
|
|
151
|
+
env.run(agents)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_html_render(env, tmp_path):
|
|
155
|
+
agents = ["random"] * 7
|
|
156
|
+
env.run(agents)
|
|
157
|
+
content = env.render(mode="html")
|
|
158
|
+
replay_file = tmp_path / "game_replay.html"
|
|
159
|
+
with open(replay_file, "w") as handle:
|
|
160
|
+
handle.write(content)
|
|
161
|
+
assert replay_file.exists()
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from kaggle_environments import make
|
|
4
|
+
from kaggle_environments.envs.werewolf.game.protocols.vote import TieBreak
|
|
5
|
+
from kaggle_environments.envs.werewolf.game.consts import EnvInfoKeys, Team
|
|
6
|
+
from kaggle_environments.envs.werewolf.game.records import GameEndResultsDataEntry
|
|
7
|
+
|
|
8
|
+
URLS = {
|
|
9
|
+
"gemini": "https://logos-world.net/wp-content/uploads/2025/01/Google-Gemini-Symbol.png",
|
|
10
|
+
"openai": "https://images.seeklogo.com/logo-png/46/1/chatgpt-logo-png_seeklogo-465219.png",
|
|
11
|
+
"claude": "https://images.seeklogo.com/logo-png/55/1/claude-logo-png_seeklogo-554534.png",
|
|
12
|
+
"grok": "https://images.seeklogo.com/logo-png/61/1/grok-logo-png_seeklogo-613403.png",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def deterministic_agents_config():
|
|
18
|
+
roles = ["Werewolf", "Werewolf", "Doctor", "Seer", "Villager", "Villager", "Villager"]
|
|
19
|
+
names = [f"player_{i}" for i in range(len(roles))]
|
|
20
|
+
thumbnails = [
|
|
21
|
+
URLS["gemini"],
|
|
22
|
+
URLS["gemini"],
|
|
23
|
+
URLS["openai"],
|
|
24
|
+
URLS["openai"],
|
|
25
|
+
URLS["openai"],
|
|
26
|
+
URLS["claude"],
|
|
27
|
+
URLS["grok"],
|
|
28
|
+
]
|
|
29
|
+
agents_config = [
|
|
30
|
+
{"role": role, "id": name, "agent_id": "deterministic", "thumbnail": url}
|
|
31
|
+
for role, name, url in zip(roles, names, thumbnails)
|
|
32
|
+
]
|
|
33
|
+
return agents_config
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def deterministic_config_options():
|
|
38
|
+
options = {
|
|
39
|
+
"discussion_protocol": {
|
|
40
|
+
"name": "RoundRobinDiscussion",
|
|
41
|
+
"params": {"max_rounds": 1, "assign_random_first_speaker": False},
|
|
42
|
+
},
|
|
43
|
+
"day_voting_protocol": {
|
|
44
|
+
"name": "SequentialVoting",
|
|
45
|
+
"params": {"assign_random_first_voter": True, "tie_break": TieBreak.NO_EXILE},
|
|
46
|
+
},
|
|
47
|
+
"werewolf_night_vote_protocol": {
|
|
48
|
+
"name": "SequentialVoting",
|
|
49
|
+
"params": {"assign_random_first_voter": True, "tie_break": TieBreak.NO_EXILE},
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
return options
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_game_result(deterministic_agents_config, deterministic_config_options):
|
|
56
|
+
"""
|
|
57
|
+
Tests that the deterministic werewolves vote to eliminate the first valid target.
|
|
58
|
+
"""
|
|
59
|
+
env = make(
|
|
60
|
+
"werewolf", debug=True, configuration={"agents": deterministic_agents_config, **deterministic_config_options}
|
|
61
|
+
)
|
|
62
|
+
agents = ["deterministic"] * 7
|
|
63
|
+
env.run(agents)
|
|
64
|
+
|
|
65
|
+
result = GameEndResultsDataEntry(**env.info[EnvInfoKeys.GAME_END])
|
|
66
|
+
|
|
67
|
+
assert len(env.steps) == 24
|
|
68
|
+
assert result.winner_team == Team.VILLAGERS
|
|
69
|
+
assert result.winner_ids == ["player_2", "player_3", "player_4", "player_5", "player_6"]
|
|
70
|
+
assert result.loser_ids == ["player_0", "player_1"]
|
|
71
|
+
assert result.scores == {
|
|
72
|
+
"player_2": 1,
|
|
73
|
+
"player_3": 1,
|
|
74
|
+
"player_4": 1,
|
|
75
|
+
"player_5": 1,
|
|
76
|
+
"player_6": 1,
|
|
77
|
+
"player_0": 0,
|
|
78
|
+
"player_1": 0,
|
|
79
|
+
}
|
|
80
|
+
assert result.elimination_info == [
|
|
81
|
+
{"player_id": "player_0", "eliminated_during_day": 1, "eliminated_during_phase": "Day"},
|
|
82
|
+
{"player_id": "player_1", "eliminated_during_day": 2, "eliminated_during_phase": "Day"},
|
|
83
|
+
{"player_id": "player_2", "eliminated_during_day": 0, "eliminated_during_phase": "Night"},
|
|
84
|
+
{"player_id": "player_3", "eliminated_during_day": 1, "eliminated_during_phase": "Night"},
|
|
85
|
+
{"player_id": "player_4", "eliminated_during_day": -1, "eliminated_during_phase": None},
|
|
86
|
+
{"player_id": "player_5", "eliminated_during_day": -1, "eliminated_during_phase": None},
|
|
87
|
+
{"player_id": "player_6", "eliminated_during_day": -1, "eliminated_during_phase": None},
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_parallel_discussion_simultaneous_majority_vote(deterministic_agents_config, deterministic_config_options):
|
|
92
|
+
config = {"agents": deterministic_agents_config, **deterministic_config_options}
|
|
93
|
+
config.update(
|
|
94
|
+
{
|
|
95
|
+
"discussion_protocol": {"name": "ParallelDiscussion", "params": {"ticks": 2}},
|
|
96
|
+
"day_voting_protocol": {"name": "SimultaneousMajority", "params": {"tie_break": TieBreak.NO_EXILE}},
|
|
97
|
+
"werewolf_night_vote_protocol": {
|
|
98
|
+
"name": "SimultaneousMajority",
|
|
99
|
+
"params": {"tie_break": TieBreak.NO_EXILE},
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
env = make("werewolf", debug=True, configuration=config)
|
|
105
|
+
agents = ["deterministic"] * 7
|
|
106
|
+
env.run(agents)
|
|
107
|
+
|
|
108
|
+
result = GameEndResultsDataEntry(**env.info[EnvInfoKeys.GAME_END])
|
|
109
|
+
|
|
110
|
+
assert len(env.steps) == 11
|
|
111
|
+
assert result.winner_team == Team.VILLAGERS
|
|
112
|
+
assert result.winner_ids == ["player_2", "player_3", "player_4", "player_5", "player_6"]
|
|
113
|
+
assert result.loser_ids == ["player_0", "player_1"]
|
|
114
|
+
assert result.scores == {
|
|
115
|
+
"player_2": 1,
|
|
116
|
+
"player_3": 1,
|
|
117
|
+
"player_4": 1,
|
|
118
|
+
"player_5": 1,
|
|
119
|
+
"player_6": 1,
|
|
120
|
+
"player_0": 0,
|
|
121
|
+
"player_1": 0,
|
|
122
|
+
}
|
|
123
|
+
assert result.elimination_info == [
|
|
124
|
+
{"player_id": "player_0", "eliminated_during_day": 1, "eliminated_during_phase": "Day"},
|
|
125
|
+
{"player_id": "player_1", "eliminated_during_day": 2, "eliminated_during_phase": "Day"},
|
|
126
|
+
{"player_id": "player_2", "eliminated_during_day": 0, "eliminated_during_phase": "Night"},
|
|
127
|
+
{"player_id": "player_3", "eliminated_during_day": 1, "eliminated_during_phase": "Night"},
|
|
128
|
+
{"player_id": "player_4", "eliminated_during_day": -1, "eliminated_during_phase": None},
|
|
129
|
+
{"player_id": "player_5", "eliminated_during_day": -1, "eliminated_during_phase": None},
|
|
130
|
+
{"player_id": "player_6", "eliminated_during_day": -1, "eliminated_during_phase": None},
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_round_by_round_bidding_discussion_sequential_vote(deterministic_agents_config, deterministic_config_options):
|
|
135
|
+
config = {"agents": deterministic_agents_config, **deterministic_config_options}
|
|
136
|
+
config.update(
|
|
137
|
+
{
|
|
138
|
+
"discussion_protocol": {
|
|
139
|
+
"name": "RoundByRoundBiddingDiscussion",
|
|
140
|
+
"params": {"bidding": {"name": "UrgencyBiddingProtocol"}, "max_rounds": 2, "bid_result_public": True},
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
env = make("werewolf", debug=True, configuration=config)
|
|
145
|
+
agents = ["deterministic"] * 7
|
|
146
|
+
env.run(agents)
|
|
147
|
+
|
|
148
|
+
result = GameEndResultsDataEntry(**env.info[EnvInfoKeys.GAME_END])
|
|
149
|
+
|
|
150
|
+
assert len(env.steps) == 34
|
|
151
|
+
assert result.winner_team == Team.VILLAGERS
|
|
152
|
+
assert result.winner_ids == ["player_2", "player_3", "player_4", "player_5", "player_6"]
|
|
153
|
+
assert result.loser_ids == ["player_0", "player_1"]
|
|
154
|
+
assert result.scores == {
|
|
155
|
+
"player_2": 1,
|
|
156
|
+
"player_3": 1,
|
|
157
|
+
"player_4": 1,
|
|
158
|
+
"player_5": 1,
|
|
159
|
+
"player_6": 1,
|
|
160
|
+
"player_0": 0,
|
|
161
|
+
"player_1": 0,
|
|
162
|
+
}
|
|
163
|
+
assert result.elimination_info == [
|
|
164
|
+
{"player_id": "player_0", "eliminated_during_day": 1, "eliminated_during_phase": "Day"},
|
|
165
|
+
{"player_id": "player_1", "eliminated_during_day": 2, "eliminated_during_phase": "Day"},
|
|
166
|
+
{"player_id": "player_2", "eliminated_during_day": 0, "eliminated_during_phase": "Night"},
|
|
167
|
+
{"player_id": "player_3", "eliminated_during_day": 1, "eliminated_during_phase": "Night"},
|
|
168
|
+
{"player_id": "player_4", "eliminated_during_day": -1, "eliminated_during_phase": None},
|
|
169
|
+
{"player_id": "player_5", "eliminated_during_day": -1, "eliminated_during_phase": None},
|
|
170
|
+
{"player_id": "player_6", "eliminated_during_day": -1, "eliminated_during_phase": None},
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_turn_by_turn_bidding(deterministic_agents_config, deterministic_config_options):
|
|
175
|
+
config = {"agents": deterministic_agents_config, **deterministic_config_options}
|
|
176
|
+
config.update(
|
|
177
|
+
{
|
|
178
|
+
"discussion_protocol": {
|
|
179
|
+
"name": "TurnByTurnBiddingDiscussion",
|
|
180
|
+
"params": {"bidding": {"name": "UrgencyBiddingProtocol"}, "max_turns": 10, "bid_result_public": False},
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
env = make("werewolf", debug=True, configuration=config)
|
|
185
|
+
agents = ["deterministic"] * 7
|
|
186
|
+
env.run(agents)
|
|
187
|
+
|
|
188
|
+
result = GameEndResultsDataEntry(**env.info[EnvInfoKeys.GAME_END])
|
|
189
|
+
|
|
190
|
+
assert len(env.steps) == 34
|
|
191
|
+
assert result.winner_team == Team.VILLAGERS
|
|
192
|
+
assert result.winner_ids == ["player_2", "player_3", "player_4", "player_5", "player_6"]
|
|
193
|
+
assert result.loser_ids == ["player_0", "player_1"]
|
|
194
|
+
assert result.scores == {
|
|
195
|
+
"player_2": 1,
|
|
196
|
+
"player_3": 1,
|
|
197
|
+
"player_4": 1,
|
|
198
|
+
"player_5": 1,
|
|
199
|
+
"player_6": 1,
|
|
200
|
+
"player_0": 0,
|
|
201
|
+
"player_1": 0,
|
|
202
|
+
}
|
|
203
|
+
assert result.elimination_info == [
|
|
204
|
+
{"player_id": "player_0", "eliminated_during_day": 1, "eliminated_during_phase": "Day"},
|
|
205
|
+
{"player_id": "player_1", "eliminated_during_day": 2, "eliminated_during_phase": "Day"},
|
|
206
|
+
{"player_id": "player_2", "eliminated_during_day": 0, "eliminated_during_phase": "Night"},
|
|
207
|
+
{"player_id": "player_3", "eliminated_during_day": 1, "eliminated_during_phase": "Night"},
|
|
208
|
+
{"player_id": "player_4", "eliminated_during_day": -1, "eliminated_during_phase": None},
|
|
209
|
+
{"player_id": "player_5", "eliminated_during_day": -1, "eliminated_during_phase": None},
|
|
210
|
+
{"player_id": "player_6", "eliminated_during_day": -1, "eliminated_during_phase": None},
|
|
211
|
+
]
|