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,467 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import json
|
|
3
|
+
import random
|
|
4
|
+
from abc import ABC
|
|
5
|
+
from collections import deque
|
|
6
|
+
from typing import List, Optional, Sequence
|
|
7
|
+
|
|
8
|
+
from kaggle_environments.envs.werewolf.game.actions import Action, BidAction
|
|
9
|
+
from kaggle_environments.envs.werewolf.game.base import PlayerID
|
|
10
|
+
from kaggle_environments.envs.werewolf.game.consts import EventName, StrEnum
|
|
11
|
+
from kaggle_environments.envs.werewolf.game.protocols.base import BiddingProtocol, DiscussionProtocol
|
|
12
|
+
from kaggle_environments.envs.werewolf.game.records import BidResultDataEntry, DiscussionOrderDataEntry
|
|
13
|
+
from kaggle_environments.envs.werewolf.game.states import GameState
|
|
14
|
+
|
|
15
|
+
from .bid import SimpleBiddingProtocol
|
|
16
|
+
from .factory import register_protocol
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@register_protocol(default_params={"max_rounds": 2, "assign_random_first_speaker": True})
|
|
20
|
+
class RoundRobinDiscussion(DiscussionProtocol):
|
|
21
|
+
def __init__(self, max_rounds: int = 1, assign_random_first_speaker: bool = True):
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
max_rounds: rounds of discussion
|
|
26
|
+
assign_random_first_speaker: If true, the first speaker will be determined at the beginning of
|
|
27
|
+
the game randomly, while the order follow that of the player list. Otherwise, will start from the
|
|
28
|
+
0th player from player list.
|
|
29
|
+
"""
|
|
30
|
+
self.max_rounds = max_rounds
|
|
31
|
+
self._queue: deque[str] = deque()
|
|
32
|
+
self._assign_random_first_speaker = assign_random_first_speaker
|
|
33
|
+
self._player_ids = None
|
|
34
|
+
self._first_player_idx = None
|
|
35
|
+
|
|
36
|
+
def reset(self) -> None:
|
|
37
|
+
self._queue = deque()
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def display_name(self) -> str:
|
|
41
|
+
return "Roundrobin"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def rule(self) -> str:
|
|
45
|
+
return f"Players speak in round-robin order for {self.max_rounds} round(s)."
|
|
46
|
+
|
|
47
|
+
def begin(self, state):
|
|
48
|
+
if self._player_ids is None:
|
|
49
|
+
# initialize player_ids once.
|
|
50
|
+
self._player_ids = deque(state.all_player_ids)
|
|
51
|
+
if self._assign_random_first_speaker:
|
|
52
|
+
self._player_ids.rotate(random.randrange(len(self._player_ids)))
|
|
53
|
+
|
|
54
|
+
# Reset queue
|
|
55
|
+
player_order = [pid for pid in self._player_ids if state.is_alive(pid)]
|
|
56
|
+
self._queue = deque(player_order * self.max_rounds)
|
|
57
|
+
if self.max_rounds > 0 and self._queue:
|
|
58
|
+
data = DiscussionOrderDataEntry(chat_order_of_player_ids=player_order)
|
|
59
|
+
state.push_event(
|
|
60
|
+
description="Discussion phase begins. Players will speak in round-robin order. "
|
|
61
|
+
f"Starting from player {player_order[0]} with the following order: {player_order} "
|
|
62
|
+
f"for {self.max_rounds} round(s).",
|
|
63
|
+
event_name=EventName.DISCUSSION_ORDER,
|
|
64
|
+
public=True,
|
|
65
|
+
data=data,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def speakers_for_tick(self, state):
|
|
69
|
+
return [self._queue.popleft()] if self._queue else []
|
|
70
|
+
|
|
71
|
+
def is_discussion_over(self, state: GameState) -> bool:
|
|
72
|
+
return not self._queue # Over if queue is empty
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@register_protocol()
|
|
76
|
+
class RandomOrderDiscussion(DiscussionProtocol):
|
|
77
|
+
def __init__(self):
|
|
78
|
+
self._iters = None
|
|
79
|
+
self._steps = 0
|
|
80
|
+
|
|
81
|
+
def reset(self) -> None:
|
|
82
|
+
self._iters = None
|
|
83
|
+
self._steps = 0
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def display_name(self) -> str:
|
|
87
|
+
return "Random Order Discussion"
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def rule(self) -> str:
|
|
91
|
+
return "Players speak in a random order for one full round."
|
|
92
|
+
|
|
93
|
+
def begin(self, state):
|
|
94
|
+
self._iters = itertools.cycle(
|
|
95
|
+
random.sample([p.id for p in state.alive_players()], k=len(state.alive_players()))
|
|
96
|
+
)
|
|
97
|
+
self._steps = len(state.alive_players()) # one full round
|
|
98
|
+
if self._steps > 0:
|
|
99
|
+
state.push_event(
|
|
100
|
+
description="Discussion phase begins. Players will speak in random order.",
|
|
101
|
+
event_name=EventName.PHASE_CHANGE,
|
|
102
|
+
public=True,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def speakers_for_tick(self, state):
|
|
106
|
+
if self._steps == 0:
|
|
107
|
+
return []
|
|
108
|
+
self._steps -= 1
|
|
109
|
+
return [next(self._iters)]
|
|
110
|
+
|
|
111
|
+
def is_discussion_over(self, state: GameState) -> bool:
|
|
112
|
+
return self._steps == 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@register_protocol()
|
|
116
|
+
class ParallelDiscussion(DiscussionProtocol):
|
|
117
|
+
"""
|
|
118
|
+
Everyone may talk for `ticks` chat turns.
|
|
119
|
+
Useful when you want simultaneous / overlapping chat.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, ticks: int = 3):
|
|
123
|
+
self.ticks = ticks
|
|
124
|
+
self._remaining = 0
|
|
125
|
+
|
|
126
|
+
def reset(self) -> None:
|
|
127
|
+
self._remaining = 0
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def display_name(self) -> str:
|
|
131
|
+
return "Parallel Discussion"
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def rule(self) -> str:
|
|
135
|
+
return f"All players may speak simultaneously for {self.ticks} tick(s)."
|
|
136
|
+
|
|
137
|
+
def begin(self, state):
|
|
138
|
+
self._remaining = self.ticks
|
|
139
|
+
if self.ticks > 0:
|
|
140
|
+
state.push_event(
|
|
141
|
+
description="Parallel discussion phase begins. All players may speak.",
|
|
142
|
+
event_name=EventName.PHASE_CHANGE,
|
|
143
|
+
public=True,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def speakers_for_tick(self, state):
|
|
147
|
+
if self._remaining == 0:
|
|
148
|
+
return []
|
|
149
|
+
self._remaining -= 1
|
|
150
|
+
return [p.id for p in state.alive_players()]
|
|
151
|
+
|
|
152
|
+
def call_for_actions(self, speakers: Sequence[str]) -> List[str]:
|
|
153
|
+
return [
|
|
154
|
+
f"Parallel discussion: All designated players may speak now or remain silent. "
|
|
155
|
+
f"({self._remaining + 1} speaking opportunities remaining, including this one)."
|
|
156
|
+
] * len(speakers)
|
|
157
|
+
|
|
158
|
+
def is_discussion_over(self, state: GameState) -> bool:
|
|
159
|
+
return self._remaining == 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class BiddingDiscussionPhase(StrEnum):
|
|
163
|
+
BIDDING_PHASE = "bidding_phase"
|
|
164
|
+
SPEAKING_PHASE = "speaking_phase"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class BiddingDiscussion(DiscussionProtocol, ABC):
|
|
168
|
+
def __init__(self, bidding: Optional[BiddingProtocol] = None):
|
|
169
|
+
bidding = bidding or SimpleBiddingProtocol()
|
|
170
|
+
self._bidding = bidding
|
|
171
|
+
self._phase = BiddingDiscussionPhase.BIDDING_PHASE
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def bidding(self):
|
|
175
|
+
return self._bidding
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def phase(self):
|
|
179
|
+
return self._phase
|
|
180
|
+
|
|
181
|
+
def is_bidding_phase(self):
|
|
182
|
+
return self._phase == BiddingDiscussionPhase.BIDDING_PHASE
|
|
183
|
+
|
|
184
|
+
def is_speaking_phase(self):
|
|
185
|
+
return self._phase == BiddingDiscussionPhase.SPEAKING_PHASE
|
|
186
|
+
|
|
187
|
+
def set_phase(self, phase: BiddingDiscussionPhase):
|
|
188
|
+
self._phase = phase
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@register_protocol(default_params={"max_turns": 8, "bid_result_public": True})
|
|
192
|
+
class TurnByTurnBiddingDiscussion(BiddingDiscussion):
|
|
193
|
+
"""
|
|
194
|
+
A discussion protocol where players bid for the right to speak each turn.
|
|
195
|
+
This protocol manages the entire bid-speak-bid-speak loop.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(self, bidding: Optional[BiddingProtocol] = None, max_turns: int = 8, bid_result_public: bool = True):
|
|
199
|
+
super().__init__(bidding=bidding)
|
|
200
|
+
self.max_turns = max_turns
|
|
201
|
+
self._turns_taken = 0
|
|
202
|
+
self._speaker: Optional[str] = None
|
|
203
|
+
self._all_passed = False
|
|
204
|
+
self._bid_result_public = bid_result_public
|
|
205
|
+
|
|
206
|
+
def reset(self) -> None:
|
|
207
|
+
self.bidding.reset()
|
|
208
|
+
self.set_phase(BiddingDiscussionPhase.BIDDING_PHASE)
|
|
209
|
+
self._turns_taken = 0
|
|
210
|
+
self._speaker = None
|
|
211
|
+
self._all_passed = False
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def display_name(self) -> str:
|
|
215
|
+
return "Turn-by-turn Bidding Driven Discussion"
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def rule(self) -> str:
|
|
219
|
+
return "\n".join(
|
|
220
|
+
[
|
|
221
|
+
f"Players bid for the right to speak each turn for up to {self.max_turns} turns.",
|
|
222
|
+
f"**Bidding Rule:** {self.bidding.display_name}. {self.bidding.rule}",
|
|
223
|
+
"If everyone bids 0, moderator will directly move on to day voting and no one speaks.",
|
|
224
|
+
]
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def begin(self, state: GameState) -> None:
|
|
228
|
+
self.reset()
|
|
229
|
+
self.bidding.begin(state) # Initial setup for the first bidding round
|
|
230
|
+
|
|
231
|
+
def is_discussion_over(self, state: GameState) -> bool:
|
|
232
|
+
return self._turns_taken >= self.max_turns or self._all_passed
|
|
233
|
+
|
|
234
|
+
def speakers_for_tick(self, state: GameState) -> Sequence[PlayerID]:
|
|
235
|
+
if self.is_discussion_over(state):
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
if self.is_bidding_phase():
|
|
239
|
+
return [p.id for p in state.alive_players()]
|
|
240
|
+
elif self.is_speaking_phase():
|
|
241
|
+
return [self._speaker] if self._speaker else []
|
|
242
|
+
return []
|
|
243
|
+
|
|
244
|
+
def process_actions(self, actions: List[Action], expected_speakers: Sequence[PlayerID], state: GameState) -> None:
|
|
245
|
+
if self.is_bidding_phase():
|
|
246
|
+
self.bidding.process_incoming_bids(actions, state)
|
|
247
|
+
|
|
248
|
+
# Handle players who didn't bid (timed out) by assuming a bid of 0
|
|
249
|
+
all_alive_player_ids = [p.id for p in state.alive_players()]
|
|
250
|
+
if hasattr(self.bidding, "_bids"):
|
|
251
|
+
for action, player_id in zip(actions, expected_speakers):
|
|
252
|
+
if not isinstance(action, BidAction):
|
|
253
|
+
default_bid = BidAction(
|
|
254
|
+
actor_id=player_id, amount=0, day=state.day_count, phase=state.phase.value
|
|
255
|
+
)
|
|
256
|
+
self.bidding.accept(default_bid, state)
|
|
257
|
+
|
|
258
|
+
bids = getattr(self.bidding, "_bids", {})
|
|
259
|
+
|
|
260
|
+
if len(bids) >= len(all_alive_player_ids):
|
|
261
|
+
# If all bids are in
|
|
262
|
+
if all(amount == 0 for amount in bids.values()):
|
|
263
|
+
# If everyone decided to pass
|
|
264
|
+
self._all_passed = True
|
|
265
|
+
state.push_event(
|
|
266
|
+
description="All players passed on speaking. Discussion ends.",
|
|
267
|
+
event_name=EventName.MODERATOR_ANNOUNCEMENT,
|
|
268
|
+
public=True,
|
|
269
|
+
)
|
|
270
|
+
return
|
|
271
|
+
else:
|
|
272
|
+
winner_list = self.bidding.outcome(state)
|
|
273
|
+
self._speaker = winner_list[0] if winner_list else None
|
|
274
|
+
if self._speaker:
|
|
275
|
+
data = BidResultDataEntry(
|
|
276
|
+
winner_player_ids=[self._speaker],
|
|
277
|
+
bid_overview=self.bidding.bids,
|
|
278
|
+
mentioned_players_in_previous_turn=self.bidding.get_last_mentioned(state)[0],
|
|
279
|
+
)
|
|
280
|
+
overview_text = ", ".join([f"{k}: {v}" for k, v in self.bidding.bids.items()])
|
|
281
|
+
state.push_event(
|
|
282
|
+
description=f"Player {self._speaker} won the bid and will speak next.\n"
|
|
283
|
+
f"Bid overview - {overview_text}.",
|
|
284
|
+
event_name=EventName.BID_RESULT,
|
|
285
|
+
public=self._bid_result_public,
|
|
286
|
+
data=data,
|
|
287
|
+
)
|
|
288
|
+
self.set_phase(BiddingDiscussionPhase.SPEAKING_PHASE)
|
|
289
|
+
return
|
|
290
|
+
else:
|
|
291
|
+
self._turns_taken += 1
|
|
292
|
+
if not self.is_discussion_over(state):
|
|
293
|
+
self.bidding.begin(state)
|
|
294
|
+
# continue bidding
|
|
295
|
+
elif self.is_speaking_phase():
|
|
296
|
+
# Process the chat action from the designated speaker
|
|
297
|
+
super().process_actions(actions, expected_speakers, state)
|
|
298
|
+
self._turns_taken += 1
|
|
299
|
+
|
|
300
|
+
# After speaking, transition back to bidding for the next turn
|
|
301
|
+
if not self.is_discussion_over(state):
|
|
302
|
+
self.set_phase(BiddingDiscussionPhase.BIDDING_PHASE)
|
|
303
|
+
self._speaker = None
|
|
304
|
+
self.bidding.begin(state)
|
|
305
|
+
|
|
306
|
+
def prompt_speakers_for_tick(self, state: GameState, speakers: Sequence[PlayerID]) -> None:
|
|
307
|
+
if self.is_bidding_phase():
|
|
308
|
+
data = {"action_json_schema": json.dumps(BidAction.schema_for_player())}
|
|
309
|
+
state.push_event(
|
|
310
|
+
description=(
|
|
311
|
+
f"A new round of discussion begins. Place bid for a chance to speak. "
|
|
312
|
+
f"{self.max_turns - self._turns_taken} turns left to speak."
|
|
313
|
+
),
|
|
314
|
+
event_name=EventName.BID_REQEUST,
|
|
315
|
+
public=True,
|
|
316
|
+
data=data,
|
|
317
|
+
visible_in_ui=False,
|
|
318
|
+
)
|
|
319
|
+
elif self.is_speaking_phase() and self._speaker:
|
|
320
|
+
super().prompt_speakers_for_tick(state, speakers)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@register_protocol(default_params={"max_rounds": 2, "bid_result_public": True})
|
|
324
|
+
class RoundByRoundBiddingDiscussion(BiddingDiscussion):
|
|
325
|
+
"""
|
|
326
|
+
A discussion protocol where players bid at the start of each round to
|
|
327
|
+
determine the speaking order for that round.
|
|
328
|
+
|
|
329
|
+
In each of the N rounds:
|
|
330
|
+
1. A bidding phase occurs where all alive players submit a bid (0-4).
|
|
331
|
+
2. The speaking order is determined by sorting players by their bid amount
|
|
332
|
+
(descending) and then by player ID (ascending) as a tie-breaker.
|
|
333
|
+
3. A speaking phase occurs where each player speaks once according to the
|
|
334
|
+
determined order.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
def __init__(self, bidding: Optional[BiddingProtocol] = None, max_rounds: int = 2, bid_result_public: bool = True):
|
|
338
|
+
"""
|
|
339
|
+
Args:
|
|
340
|
+
bidding: The bidding protocol to use for determining speaking order.
|
|
341
|
+
max_rounds: The total number of discussion rounds.
|
|
342
|
+
bid_result_public: Whether to make the bidding results public.
|
|
343
|
+
"""
|
|
344
|
+
super().__init__(bidding=bidding)
|
|
345
|
+
self.max_rounds = max_rounds
|
|
346
|
+
self._bid_result_public = bid_result_public
|
|
347
|
+
self._current_round = 0
|
|
348
|
+
self._speaking_queue: deque[str] = deque()
|
|
349
|
+
self.reset()
|
|
350
|
+
|
|
351
|
+
def reset(self) -> None:
|
|
352
|
+
"""Resets the protocol to its initial state."""
|
|
353
|
+
self.bidding.reset()
|
|
354
|
+
self.set_phase(BiddingDiscussionPhase.BIDDING_PHASE)
|
|
355
|
+
self._current_round = 0
|
|
356
|
+
self._speaking_queue = deque()
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def display_name(self) -> str:
|
|
360
|
+
return "Round-by-round Bidding Driven Discussion"
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def rule(self) -> str:
|
|
364
|
+
"""A string describing the discussion rule in effect."""
|
|
365
|
+
return "\n".join(
|
|
366
|
+
[
|
|
367
|
+
"Players speak in an order determined by bidding at the beginning of each round. "
|
|
368
|
+
f"There will be {self.max_rounds} round(s) per day.",
|
|
369
|
+
"In each round, all players may speak once.",
|
|
370
|
+
f"**Bidding Rule:** {self.bidding.display_name}. {self.bidding.rule}",
|
|
371
|
+
]
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def begin(self, state: GameState) -> None:
|
|
375
|
+
"""Initializes the protocol for the first round."""
|
|
376
|
+
self.reset()
|
|
377
|
+
self.bidding.begin(state)
|
|
378
|
+
|
|
379
|
+
def is_discussion_over(self, state: GameState) -> bool:
|
|
380
|
+
"""Checks if all rounds have been completed."""
|
|
381
|
+
return self._current_round >= self.max_rounds
|
|
382
|
+
|
|
383
|
+
def speakers_for_tick(self, state: GameState) -> Sequence[PlayerID]:
|
|
384
|
+
"""Returns the players who are allowed to act in the current tick."""
|
|
385
|
+
if self.is_discussion_over(state):
|
|
386
|
+
return []
|
|
387
|
+
|
|
388
|
+
if self.is_bidding_phase():
|
|
389
|
+
# In the bidding phase, all alive players can bid.
|
|
390
|
+
return [p.id for p in state.alive_players()]
|
|
391
|
+
elif self.is_speaking_phase():
|
|
392
|
+
# In the speaking phase, the next player in the queue speaks.
|
|
393
|
+
return [self._speaking_queue.popleft()] if self._speaking_queue else []
|
|
394
|
+
return []
|
|
395
|
+
|
|
396
|
+
def process_actions(self, actions: List[Action], expected_speakers: Sequence[PlayerID], state: GameState) -> None:
|
|
397
|
+
"""Processes incoming actions from players."""
|
|
398
|
+
if self.is_bidding_phase():
|
|
399
|
+
self.bidding.process_incoming_bids(actions, state)
|
|
400
|
+
|
|
401
|
+
# Assume a bid of 0 for any players who timed out.
|
|
402
|
+
all_alive_player_ids = [p.id for p in state.alive_players()]
|
|
403
|
+
if hasattr(self.bidding, "_bids"):
|
|
404
|
+
for player_id in all_alive_player_ids:
|
|
405
|
+
if player_id not in self.bidding.bids:
|
|
406
|
+
default_bid = BidAction(
|
|
407
|
+
actor_id=player_id, amount=0, day=state.day_count, phase=state.phase.value
|
|
408
|
+
)
|
|
409
|
+
self.bidding.accept(default_bid, state)
|
|
410
|
+
|
|
411
|
+
# Determine speaking order based on bids.
|
|
412
|
+
# Sort by bid amount (desc) and then player ID (asc).
|
|
413
|
+
bids = self.bidding.bids
|
|
414
|
+
sorted_bidders = sorted(bids.items(), key=lambda item: (-item[1], item[0]))
|
|
415
|
+
|
|
416
|
+
self._speaking_queue = deque([player_id for player_id, bid_amount in sorted_bidders])
|
|
417
|
+
|
|
418
|
+
# Announce the speaking order for the round.
|
|
419
|
+
data = DiscussionOrderDataEntry(chat_order_of_player_ids=list(self._speaking_queue))
|
|
420
|
+
speaking_order_text = ", ".join([f"{pid} ({amount})" for pid, amount in sorted_bidders])
|
|
421
|
+
|
|
422
|
+
state.push_event(
|
|
423
|
+
description=f"Bidding for round {self._current_round + 1} has concluded. The speaking order, "
|
|
424
|
+
f"with bid amounts in parentheses, is: {speaking_order_text}.",
|
|
425
|
+
event_name=EventName.BID_RESULT,
|
|
426
|
+
public=self._bid_result_public,
|
|
427
|
+
data=data,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Transition to the speaking phase.
|
|
431
|
+
self.set_phase(BiddingDiscussionPhase.SPEAKING_PHASE)
|
|
432
|
+
|
|
433
|
+
elif self.is_speaking_phase():
|
|
434
|
+
# Process the chat action from the current speaker.
|
|
435
|
+
super().process_actions(actions, expected_speakers, state)
|
|
436
|
+
|
|
437
|
+
# Check if the round is over (i.e., the speaking queue is empty).
|
|
438
|
+
if not self._speaking_queue:
|
|
439
|
+
self._current_round += 1
|
|
440
|
+
state.push_event(
|
|
441
|
+
description=f"End of discussion round {self._current_round}.",
|
|
442
|
+
event_name=EventName.PHASE_CHANGE,
|
|
443
|
+
public=True,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# If the game isn't over, prepare for the next round's bidding.
|
|
447
|
+
if not self.is_discussion_over(state):
|
|
448
|
+
self.set_phase(BiddingDiscussionPhase.BIDDING_PHASE)
|
|
449
|
+
self.bidding.begin(state)
|
|
450
|
+
|
|
451
|
+
def prompt_speakers_for_tick(self, state: GameState, speakers: Sequence[PlayerID]) -> None:
|
|
452
|
+
"""Prompts the active players for their next action."""
|
|
453
|
+
if self.is_bidding_phase():
|
|
454
|
+
data = {"action_json_schema": json.dumps(BidAction.schema_for_player())}
|
|
455
|
+
state.push_event(
|
|
456
|
+
description=(
|
|
457
|
+
f"Round {self._current_round + 1} of {self.max_rounds} begins. "
|
|
458
|
+
"Place your bid to determine speaking order."
|
|
459
|
+
),
|
|
460
|
+
event_name=EventName.BID_REQEUST,
|
|
461
|
+
public=True,
|
|
462
|
+
data=data,
|
|
463
|
+
visible_in_ui=False,
|
|
464
|
+
)
|
|
465
|
+
elif self.is_speaking_phase():
|
|
466
|
+
# The default prompt from the base class is sufficient for speaking.
|
|
467
|
+
super().prompt_speakers_for_tick(state, speakers)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, Type
|
|
2
|
+
|
|
3
|
+
# The new unified, flat registry. Maps class names to class objects and default params.
|
|
4
|
+
PROTOCOL_REGISTRY: Dict[str, Dict[str, Any]] = {}
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register_protocol(default_params: Dict = None) -> Callable:
|
|
8
|
+
"""
|
|
9
|
+
A decorator to register a protocol class in the central unified registry.
|
|
10
|
+
The protocol is registered using its class name.
|
|
11
|
+
"""
|
|
12
|
+
if default_params is None:
|
|
13
|
+
default_params = {}
|
|
14
|
+
|
|
15
|
+
def decorator(cls: Type) -> Type:
|
|
16
|
+
name = cls.__name__
|
|
17
|
+
if name in PROTOCOL_REGISTRY:
|
|
18
|
+
raise TypeError(f"Protocol '{name}' is already registered.")
|
|
19
|
+
|
|
20
|
+
PROTOCOL_REGISTRY[name] = {"class": cls, "default_params": default_params}
|
|
21
|
+
return cls
|
|
22
|
+
|
|
23
|
+
return decorator
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_protocol(config: Dict, default_name: str = None) -> Any:
|
|
27
|
+
"""
|
|
28
|
+
Factory function to recursively create protocol instances from a configuration dictionary.
|
|
29
|
+
"""
|
|
30
|
+
if not config and default_name:
|
|
31
|
+
config = {"name": default_name}
|
|
32
|
+
elif not config and not default_name:
|
|
33
|
+
# If no config and no default, we cannot proceed.
|
|
34
|
+
raise ValueError("Cannot create protocol from an empty configuration without a default name.")
|
|
35
|
+
|
|
36
|
+
# Fallback to default_name if 'name' is not in the config
|
|
37
|
+
name = config.get("name", default_name)
|
|
38
|
+
if not name:
|
|
39
|
+
raise ValueError("Protocol name must be provided in config or as a default.")
|
|
40
|
+
|
|
41
|
+
params = config.get("params", {})
|
|
42
|
+
|
|
43
|
+
protocol_info = PROTOCOL_REGISTRY.get(name)
|
|
44
|
+
if not protocol_info:
|
|
45
|
+
raise ValueError(f"Protocol '{name}' not found in the registry.")
|
|
46
|
+
|
|
47
|
+
protocol_class = protocol_info["class"]
|
|
48
|
+
# Start with the protocol's defaults, then override with config params
|
|
49
|
+
final_params = {**protocol_info["default_params"], **params}
|
|
50
|
+
|
|
51
|
+
# --- Recursive Instantiation for Nested Protocols ---
|
|
52
|
+
for param_name, param_value in final_params.items():
|
|
53
|
+
# If a parameter's value is a dictionary that looks like a protocol config
|
|
54
|
+
# (i.e., it has a "name" key), we recursively create it.
|
|
55
|
+
if isinstance(param_value, dict) and "name" in param_value:
|
|
56
|
+
# The nested protocol's config is the param_value itself.
|
|
57
|
+
final_params[param_name] = create_protocol(param_value)
|
|
58
|
+
|
|
59
|
+
return protocol_class(**final_params)
|