kaggle-environments 1.20.1__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.

Files changed (60) hide show
  1. kaggle_environments/__init__.py +2 -2
  2. kaggle_environments/envs/cabt/cabt.js +8 -8
  3. kaggle_environments/envs/cabt/cg/cg.dll +0 -0
  4. kaggle_environments/envs/cabt/cg/libcg.so +0 -0
  5. kaggle_environments/envs/{open_spiel → open_spiel_env}/games/universal_poker/universal_poker.js +52 -28
  6. kaggle_environments/envs/{open_spiel/open_spiel.py → open_spiel_env/open_spiel_env.py} +37 -1
  7. kaggle_environments/envs/{open_spiel/test_open_spiel.py → open_spiel_env/test_open_spiel_env.py} +65 -1
  8. kaggle_environments/envs/werewolf/GAME_RULE.md +75 -0
  9. kaggle_environments/envs/werewolf/__init__.py +0 -0
  10. kaggle_environments/envs/werewolf/game/__init__.py +0 -0
  11. kaggle_environments/envs/werewolf/game/actions.py +268 -0
  12. kaggle_environments/envs/werewolf/game/base.py +115 -0
  13. kaggle_environments/envs/werewolf/game/consts.py +156 -0
  14. kaggle_environments/envs/werewolf/game/engine.py +580 -0
  15. kaggle_environments/envs/werewolf/game/night_elimination_manager.py +101 -0
  16. kaggle_environments/envs/werewolf/game/protocols/__init__.py +4 -0
  17. kaggle_environments/envs/werewolf/game/protocols/base.py +242 -0
  18. kaggle_environments/envs/werewolf/game/protocols/bid.py +248 -0
  19. kaggle_environments/envs/werewolf/game/protocols/chat.py +467 -0
  20. kaggle_environments/envs/werewolf/game/protocols/factory.py +59 -0
  21. kaggle_environments/envs/werewolf/game/protocols/vote.py +471 -0
  22. kaggle_environments/envs/werewolf/game/records.py +334 -0
  23. kaggle_environments/envs/werewolf/game/roles.py +326 -0
  24. kaggle_environments/envs/werewolf/game/states.py +214 -0
  25. kaggle_environments/envs/werewolf/game/test_actions.py +45 -0
  26. kaggle_environments/envs/werewolf/test_werewolf.py +161 -0
  27. kaggle_environments/envs/werewolf/test_werewolf_deterministic.py +211 -0
  28. kaggle_environments/envs/werewolf/werewolf.js +4377 -0
  29. kaggle_environments/envs/werewolf/werewolf.json +286 -0
  30. kaggle_environments/envs/werewolf/werewolf.py +602 -0
  31. kaggle_environments/static/player.html +19 -1
  32. kaggle_environments-1.21.0.dist-info/METADATA +30 -0
  33. {kaggle_environments-1.20.1.dist-info → kaggle_environments-1.21.0.dist-info}/RECORD +55 -36
  34. kaggle_environments/envs/chess/chess.js +0 -4289
  35. kaggle_environments/envs/chess/chess.json +0 -60
  36. kaggle_environments/envs/chess/chess.py +0 -4241
  37. kaggle_environments/envs/chess/test_chess.py +0 -60
  38. kaggle_environments-1.20.1.dist-info/METADATA +0 -315
  39. /kaggle_environments/envs/{open_spiel → open_spiel_env}/__init__.py +0 -0
  40. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/__init__.py +0 -0
  41. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/chess/chess.js +0 -0
  42. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/chess/image_config.jsonl +0 -0
  43. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/chess/openings.jsonl +0 -0
  44. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/connect_four/__init__.py +0 -0
  45. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/connect_four/connect_four.js +0 -0
  46. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/connect_four/connect_four_proxy.py +0 -0
  47. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/go/__init__.py +0 -0
  48. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/go/go.js +0 -0
  49. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/go/go_proxy.py +0 -0
  50. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/tic_tac_toe/__init__.py +0 -0
  51. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/tic_tac_toe/tic_tac_toe.js +0 -0
  52. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/tic_tac_toe/tic_tac_toe_proxy.py +0 -0
  53. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/universal_poker/__init__.py +0 -0
  54. /kaggle_environments/envs/{open_spiel → open_spiel_env}/games/universal_poker/universal_poker_proxy.py +0 -0
  55. /kaggle_environments/envs/{open_spiel → open_spiel_env}/html_playthrough_generator.py +0 -0
  56. /kaggle_environments/envs/{open_spiel → open_spiel_env}/observation.py +0 -0
  57. /kaggle_environments/envs/{open_spiel → open_spiel_env}/proxy.py +0 -0
  58. {kaggle_environments-1.20.1.dist-info → kaggle_environments-1.21.0.dist-info}/WHEEL +0 -0
  59. {kaggle_environments-1.20.1.dist-info → kaggle_environments-1.21.0.dist-info}/entry_points.txt +0 -0
  60. {kaggle_environments-1.20.1.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)