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,242 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Dict, List, Optional, Sequence, Tuple
|
|
5
|
+
|
|
6
|
+
from kaggle_environments.envs.werewolf.game.actions import Action, BidAction, ChatAction
|
|
7
|
+
from kaggle_environments.envs.werewolf.game.base import PlayerID
|
|
8
|
+
from kaggle_environments.envs.werewolf.game.consts import EventName
|
|
9
|
+
from kaggle_environments.envs.werewolf.game.records import ChatDataEntry, RequestVillagerToSpeakDataEntry
|
|
10
|
+
from kaggle_environments.envs.werewolf.game.roles import Player
|
|
11
|
+
from kaggle_environments.envs.werewolf.game.states import GameState
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _extract_player_ids_from_string(text: str, all_player_ids: List[PlayerID]) -> List[PlayerID]:
|
|
15
|
+
"""Extracts player IDs mentioned in a string."""
|
|
16
|
+
if not all_player_ids:
|
|
17
|
+
return []
|
|
18
|
+
# Create a regex pattern to find any of the player IDs as whole words
|
|
19
|
+
# Using a set for faster lookups and to handle duplicates from the regex
|
|
20
|
+
pattern = r"\b(" + "|".join(re.escape(pid) for pid in all_player_ids) + r")\b"
|
|
21
|
+
# Use a set to automatically handle duplicates found by the regex
|
|
22
|
+
found_ids = set(re.findall(pattern, text))
|
|
23
|
+
return sorted(list(found_ids)) # sorted for deterministic order
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _find_mentioned_players(text: str, all_player_ids: List[PlayerID]) -> List[PlayerID]:
|
|
27
|
+
"""
|
|
28
|
+
Finds player IDs mentioned in a string of text, ordered by their first appearance.
|
|
29
|
+
Player IDs are treated as whole words.
|
|
30
|
+
Example: "I think gpt-4 is suspicious, what do you think John?" -> ["gpt-4", "John"]
|
|
31
|
+
"""
|
|
32
|
+
if not text or not all_player_ids:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
# Sort by length descending to handle substrings correctly.
|
|
36
|
+
sorted_player_ids = sorted(all_player_ids, key=len, reverse=True)
|
|
37
|
+
pattern = r"\b(" + "|".join(re.escape(pid) for pid in sorted_player_ids) + r")\b"
|
|
38
|
+
|
|
39
|
+
matches = re.finditer(pattern, text)
|
|
40
|
+
|
|
41
|
+
# Deduplicate while preserving order of first appearance
|
|
42
|
+
ordered_mentioned_ids = []
|
|
43
|
+
seen = set()
|
|
44
|
+
for match in matches:
|
|
45
|
+
player_id = match.group(1)
|
|
46
|
+
if player_id not in seen:
|
|
47
|
+
ordered_mentioned_ids.append(player_id)
|
|
48
|
+
seen.add(player_id)
|
|
49
|
+
|
|
50
|
+
return ordered_mentioned_ids
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GameProtocol(ABC):
|
|
54
|
+
@property
|
|
55
|
+
def display_name(self) -> str:
|
|
56
|
+
return self.__class__.__name__
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def rule(self) -> str:
|
|
61
|
+
"""Human-readable format of rule."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class VotingProtocol(GameProtocol):
|
|
65
|
+
"""Collects, validates, and tallies votes."""
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def begin_voting(self, state: GameState, alive_voters: Sequence[Player], potential_targets: Sequence[Player]):
|
|
69
|
+
"""Initialize for a new voting round."""
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def get_voting_prompt(self, state: GameState, player_id: PlayerID) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Returns a string prompt for the specified player, potentially including current tally.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def collect_vote(self, vote_action: Action, state: GameState): # Changed to Action, will check type
|
|
79
|
+
"""Collect an individual vote."""
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def collect_votes(self, player_actions: Dict[str, Action], state: GameState, expected_voters: List[PlayerID]):
|
|
83
|
+
"""Collect a batch of votes."""
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def get_current_tally_info(self, state: GameState) -> Dict[PlayerID, PlayerID]:
|
|
87
|
+
"""
|
|
88
|
+
Return the current tally by a map, where key is player, value is target.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def get_next_voters(self) -> List[PlayerID]:
|
|
93
|
+
"""get the next batch of voters"""
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def done(self):
|
|
97
|
+
"""Check if voting is done."""
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def get_valid_targets(self) -> List[PlayerID]:
|
|
101
|
+
"""get a list of targets"""
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def get_elected(self) -> Optional[PlayerID]:
|
|
105
|
+
"""get the final elected individual, or None if no one was elected."""
|
|
106
|
+
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def reset(self) -> None:
|
|
109
|
+
"""Resets the protocol to its initial state."""
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class BiddingProtocol(GameProtocol):
|
|
114
|
+
"""Drives one auction round and returns the winner(s)."""
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def bids(self) -> Dict[PlayerID, int]:
|
|
119
|
+
"""return a snapshot of the current bids"""
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def get_last_mentioned(state: GameState) -> Tuple[List[PlayerID], str]:
|
|
123
|
+
"""get the players that were mentioned in last player message."""
|
|
124
|
+
last_chat_message = ""
|
|
125
|
+
sorted_days = sorted(state.history.keys(), reverse=True)
|
|
126
|
+
for day in sorted_days:
|
|
127
|
+
for entry in reversed(state.history[day]):
|
|
128
|
+
if entry.event_name == EventName.DISCUSSION and isinstance(entry.data, ChatDataEntry):
|
|
129
|
+
last_chat_message = entry.data.message
|
|
130
|
+
break
|
|
131
|
+
if last_chat_message:
|
|
132
|
+
break
|
|
133
|
+
players = _find_mentioned_players(last_chat_message, state.all_player_ids)
|
|
134
|
+
return players, last_chat_message
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def begin(self, state: GameState) -> None: ...
|
|
138
|
+
|
|
139
|
+
@abstractmethod
|
|
140
|
+
def accept(self, bid: BidAction, state: GameState) -> None: ...
|
|
141
|
+
|
|
142
|
+
@abstractmethod
|
|
143
|
+
def process_incoming_bids(self, actions: List[Action], state: GameState) -> None:
|
|
144
|
+
"""Processes a batch of actions, handling BidActions by calling self.accept()."""
|
|
145
|
+
|
|
146
|
+
@abstractmethod
|
|
147
|
+
def is_finished(self, state: GameState) -> bool: ...
|
|
148
|
+
|
|
149
|
+
@abstractmethod
|
|
150
|
+
def outcome(self, state: GameState) -> list[PlayerID]:
|
|
151
|
+
"""
|
|
152
|
+
Return list of player-ids, ordered by bid strength.
|
|
153
|
+
Could be 1 winner (sealed-bid) or a full ranking (Dutch auction).
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
@abstractmethod
|
|
157
|
+
def reset(self) -> None:
|
|
158
|
+
"""Resets the protocol to its initial state."""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class DiscussionProtocol(GameProtocol):
|
|
162
|
+
"""Drives the order/shape of daytime conversation."""
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def begin(self, state: GameState) -> None:
|
|
166
|
+
"""Optional hook – initialise timers, round counters…"""
|
|
167
|
+
|
|
168
|
+
@abstractmethod
|
|
169
|
+
def speakers_for_tick(self, state: GameState) -> Sequence[PlayerID]:
|
|
170
|
+
"""
|
|
171
|
+
Return the IDs that are *allowed to send a chat action* this tick.
|
|
172
|
+
Return an empty sequence when the discussion phase is over.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
@abstractmethod
|
|
176
|
+
def is_discussion_over(self, state: GameState) -> bool:
|
|
177
|
+
"""Returns True if the entire discussion (including any preliminary phases like bidding) is complete."""
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
@abstractmethod
|
|
181
|
+
def reset(self) -> None:
|
|
182
|
+
"""Resets the protocol to its initial state."""
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
def process_actions(self, actions: List[Action], expected_speakers: Sequence[PlayerID], state: GameState) -> None:
|
|
186
|
+
"""
|
|
187
|
+
Processes a batch of actions. Depending on the protocol's state (e.g., bidding or chatting),
|
|
188
|
+
it will handle relevant actions (like BidAction or ChatAction) from expected_speakers.
|
|
189
|
+
"""
|
|
190
|
+
for act in actions:
|
|
191
|
+
if isinstance(act, ChatAction):
|
|
192
|
+
all_player_ids = [p.id for p in state.players]
|
|
193
|
+
mentioned_ids = _extract_player_ids_from_string(act.message, all_player_ids)
|
|
194
|
+
if expected_speakers and act.actor_id in expected_speakers:
|
|
195
|
+
data = ChatDataEntry(
|
|
196
|
+
actor_id=act.actor_id,
|
|
197
|
+
message=act.message,
|
|
198
|
+
reasoning=act.reasoning,
|
|
199
|
+
mentioned_player_ids=mentioned_ids,
|
|
200
|
+
perceived_threat_level=act.perceived_threat_level,
|
|
201
|
+
action=act,
|
|
202
|
+
)
|
|
203
|
+
state.push_event(
|
|
204
|
+
description=f'Player "{act.actor_id}" (chat): {act.message}',
|
|
205
|
+
# Make public for general discussion
|
|
206
|
+
event_name=EventName.DISCUSSION,
|
|
207
|
+
public=True,
|
|
208
|
+
source=act.actor_id,
|
|
209
|
+
data=data,
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
state.push_event(
|
|
213
|
+
description=f'Player "{act.actor_id}" (chat, out of turn): {act.message}',
|
|
214
|
+
event_name=EventName.DISCUSSION, # Or a specific "INVALID_CHAT" type
|
|
215
|
+
visible_to=[act.actor_id],
|
|
216
|
+
public=False,
|
|
217
|
+
source=act.actor_id,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def call_for_actions(self, speakers: Sequence[PlayerID]) -> List[str]:
|
|
221
|
+
"""prepare moderator call for action for each player."""
|
|
222
|
+
return [f'Player "{speaker_id}", it is your turn to speak.' for speaker_id in speakers]
|
|
223
|
+
|
|
224
|
+
def prompt_speakers_for_tick(self, state: GameState, speakers: Sequence[PlayerID]) -> None:
|
|
225
|
+
"""
|
|
226
|
+
Allows the protocol to make specific announcements or prompts to the current speakers for this tick.
|
|
227
|
+
This method is called by the Moderator after speakers_for_tick() returns a non-empty list of speakers,
|
|
228
|
+
and before process_actions().
|
|
229
|
+
Implementations should use state.push_event() to make announcements.
|
|
230
|
+
These announcements are typically visible only to the speakers, unless they are general status updates.
|
|
231
|
+
"""
|
|
232
|
+
call_for_actions = self.call_for_actions(speakers)
|
|
233
|
+
for speaker_id, call_for_action in zip(speakers, call_for_actions):
|
|
234
|
+
data = RequestVillagerToSpeakDataEntry(action_json_schema=json.dumps(ChatAction.schema_for_player()))
|
|
235
|
+
state.push_event(
|
|
236
|
+
description=call_for_action,
|
|
237
|
+
event_name=EventName.CHAT_REQUEST,
|
|
238
|
+
public=False,
|
|
239
|
+
visible_to=[speaker_id],
|
|
240
|
+
data=data,
|
|
241
|
+
visible_in_ui=False,
|
|
242
|
+
)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
from kaggle_environments.envs.werewolf.game.actions import Action, BidAction
|
|
5
|
+
from kaggle_environments.envs.werewolf.game.base import PlayerID
|
|
6
|
+
from kaggle_environments.envs.werewolf.game.consts import EventName
|
|
7
|
+
from kaggle_environments.envs.werewolf.game.protocols.base import BiddingProtocol
|
|
8
|
+
from kaggle_environments.envs.werewolf.game.records import BidDataEntry, ChatDataEntry
|
|
9
|
+
from kaggle_environments.envs.werewolf.game.states import GameState
|
|
10
|
+
|
|
11
|
+
from .factory import register_protocol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@register_protocol()
|
|
15
|
+
class SimpleBiddingProtocol(BiddingProtocol):
|
|
16
|
+
"""
|
|
17
|
+
A straightforward bidding protocol where speaking priority is determined
|
|
18
|
+
solely by the bid amount.
|
|
19
|
+
- Agents bid with a numerical amount.
|
|
20
|
+
- Higher bids result in earlier speaking turns.
|
|
21
|
+
- Ties are broken deterministically by player ID (ascending).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self._bids: Dict[PlayerID, int] = {}
|
|
26
|
+
self._max_bid = 4
|
|
27
|
+
self.reset()
|
|
28
|
+
|
|
29
|
+
def reset(self) -> None:
|
|
30
|
+
"""Resets the bids for a new round."""
|
|
31
|
+
self._bids = {}
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def display_name(self):
|
|
35
|
+
return "Simple Bidding"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def rule(self) -> str:
|
|
39
|
+
"""Provides a description of the bidding rules."""
|
|
40
|
+
return "\n".join(
|
|
41
|
+
(
|
|
42
|
+
"Players bid with an urgency level (0-4) to determine speaking order.",
|
|
43
|
+
"0: I would like to observe and listen for now.",
|
|
44
|
+
"1: I have some general thoughts to share with the group.",
|
|
45
|
+
"2: I have something critical and specific to contribute to this discussion.",
|
|
46
|
+
"3: It is absolutely urgent for me to speak next.",
|
|
47
|
+
"4: I must respond.",
|
|
48
|
+
"Higher bids speak earlier. Ties are broken by player name (A-Z).",
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def bids(self) -> Dict[PlayerID, int]:
|
|
54
|
+
"""Returns a copy of the current bids."""
|
|
55
|
+
return dict(**self._bids)
|
|
56
|
+
|
|
57
|
+
def begin(self, state: GameState) -> None:
|
|
58
|
+
"""Initializes a new bidding round."""
|
|
59
|
+
self.reset()
|
|
60
|
+
|
|
61
|
+
def accept(self, bid: BidAction, state: GameState) -> None:
|
|
62
|
+
"""Accepts and records a single bid from a player."""
|
|
63
|
+
bid_amount = min(max(0, bid.amount), self._max_bid)
|
|
64
|
+
self._bids[bid.actor_id] = bid_amount
|
|
65
|
+
|
|
66
|
+
data = BidDataEntry(
|
|
67
|
+
actor_id=bid.actor_id,
|
|
68
|
+
reasoning=bid.reasoning,
|
|
69
|
+
perceived_threat_level=bid.perceived_threat_level,
|
|
70
|
+
bid_amount=bid_amount,
|
|
71
|
+
action=bid,
|
|
72
|
+
)
|
|
73
|
+
state.push_event(
|
|
74
|
+
description=f"Player {bid.actor_id} submitted a bid of {bid_amount}.",
|
|
75
|
+
event_name=EventName.BID_ACTION,
|
|
76
|
+
public=False, # Bids are private until the outcome is announced
|
|
77
|
+
visible_to=[bid.actor_id],
|
|
78
|
+
data=data,
|
|
79
|
+
source=bid.actor_id,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def process_incoming_bids(self, actions: List[Action], state: GameState) -> None:
|
|
83
|
+
"""Processes a list of actions, handling any BidActions."""
|
|
84
|
+
for act in actions:
|
|
85
|
+
if isinstance(act, BidAction):
|
|
86
|
+
self.accept(act, state)
|
|
87
|
+
|
|
88
|
+
def is_finished(self, state: GameState) -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Checks if the bidding phase is complete (i.e., all alive players have bid).
|
|
91
|
+
"""
|
|
92
|
+
return len(self._bids) >= len(state.alive_players())
|
|
93
|
+
|
|
94
|
+
def outcome(self, state: GameState) -> list[str]:
|
|
95
|
+
"""
|
|
96
|
+
Determines the final speaking order based on bids.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A list of player IDs sorted by bid (descending) and then player ID (ascending).
|
|
100
|
+
"""
|
|
101
|
+
if not self._bids:
|
|
102
|
+
# If no bids were made, return alive players in their default order.
|
|
103
|
+
return sorted([p.id for p in state.alive_players()])
|
|
104
|
+
|
|
105
|
+
# Sort by bid amount (descending) and then by player ID (ascending) for tie-breaking.
|
|
106
|
+
sorted_bidders = sorted(self._bids.items(), key=lambda item: (-item[1], item[0]))
|
|
107
|
+
return [player_id for player_id, bid_amount in sorted_bidders]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@register_protocol()
|
|
111
|
+
class UrgencyBiddingProtocol(BiddingProtocol):
|
|
112
|
+
"""
|
|
113
|
+
A bidding protocol based on the Werewolf Arena paper.
|
|
114
|
+
- Agents bid with an urgency level (0-4).
|
|
115
|
+
- Highest bidder wins.
|
|
116
|
+
- Ties are broken by prioritizing players mentioned in the previous turn.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def display_name(self) -> str:
|
|
121
|
+
return "Urgency Bidding"
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def rule(self) -> str:
|
|
125
|
+
return "\n".join(
|
|
126
|
+
[
|
|
127
|
+
"Urgency-based bidding. Players bid with an urgency level (0-4).",
|
|
128
|
+
"0: I would like to observe and listen for now.",
|
|
129
|
+
"1: I have some general thoughts to share with the group.",
|
|
130
|
+
"2: I have something critical and specific to contribute to this discussion.",
|
|
131
|
+
"3: It is absolutely urgent for me to speak next.",
|
|
132
|
+
"4: Someone has addressed me directly and I must respond.",
|
|
133
|
+
"Highest bidder wins."
|
|
134
|
+
"Ties are broken by the following priority: (1) players mentioned in the previous turn's chat, "
|
|
135
|
+
"(2) the least spoken player, (3) round robin order of the player list.",
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def bids(self) -> Dict[PlayerID, int]:
|
|
141
|
+
return dict(**self._bids)
|
|
142
|
+
|
|
143
|
+
def __init__(self):
|
|
144
|
+
self._bids: Dict[PlayerID, int] = {}
|
|
145
|
+
self._mentioned_last_turn: List[PlayerID] = []
|
|
146
|
+
|
|
147
|
+
def reset(self) -> None:
|
|
148
|
+
self._bids = {}
|
|
149
|
+
self._mentioned_last_turn = []
|
|
150
|
+
|
|
151
|
+
def begin(self, state: GameState) -> None:
|
|
152
|
+
"""Called at the start of a bidding round to identify recently mentioned players."""
|
|
153
|
+
self.reset()
|
|
154
|
+
# Find the very last chat entry in the history to check for mentions
|
|
155
|
+
self._mentioned_last_turn, last_chat_message = self.get_last_mentioned(state)
|
|
156
|
+
|
|
157
|
+
if last_chat_message:
|
|
158
|
+
if self._mentioned_last_turn:
|
|
159
|
+
state.push_event(
|
|
160
|
+
description=f"Players mentioned last turn (priority in ties): {self._mentioned_last_turn}",
|
|
161
|
+
event_name=EventName.BIDDING_INFO,
|
|
162
|
+
public=True, # So everyone knows who has priority
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def accept(self, bid: BidAction, state: GameState) -> None:
|
|
166
|
+
if 0 <= bid.amount <= 4:
|
|
167
|
+
self._bids[bid.actor_id] = bid.amount
|
|
168
|
+
data = BidDataEntry(
|
|
169
|
+
actor_id=bid.actor_id,
|
|
170
|
+
reasoning=bid.reasoning,
|
|
171
|
+
perceived_threat_level=bid.perceived_threat_level,
|
|
172
|
+
bid_amount=bid.amount,
|
|
173
|
+
action=bid,
|
|
174
|
+
)
|
|
175
|
+
state.push_event(
|
|
176
|
+
description=f"Player {bid.actor_id} submitted bid=({bid.amount}).",
|
|
177
|
+
event_name=EventName.BID_ACTION,
|
|
178
|
+
public=False,
|
|
179
|
+
visible_to=[bid.actor_id],
|
|
180
|
+
data=data,
|
|
181
|
+
source=bid.actor_id,
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
# Invalid bid amount is treated as a bid of 0
|
|
185
|
+
self._bids[bid.actor_id] = 0
|
|
186
|
+
state.push_event(
|
|
187
|
+
description=f"Player {bid.actor_id} submitted an invalid bid amount ({bid.amount}). Treated as 0.",
|
|
188
|
+
event_name=EventName.ERROR,
|
|
189
|
+
public=False,
|
|
190
|
+
visible_to=[bid.actor_id],
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def process_incoming_bids(self, actions: List[Action], state: GameState) -> None:
|
|
194
|
+
for act in actions:
|
|
195
|
+
if isinstance(act, BidAction):
|
|
196
|
+
self.accept(act, state)
|
|
197
|
+
|
|
198
|
+
def is_finished(self, state: GameState) -> bool:
|
|
199
|
+
# This bidding round is considered "finished" when all alive players have bid.
|
|
200
|
+
return len(self._bids) >= len(state.alive_players())
|
|
201
|
+
|
|
202
|
+
def outcome(self, state: GameState) -> list[str]:
|
|
203
|
+
if not self._bids:
|
|
204
|
+
# If no one bids, deterministically pick the first alive player to speak.
|
|
205
|
+
alive_players = state.alive_players()
|
|
206
|
+
return [alive_players[0].id] if alive_players else []
|
|
207
|
+
|
|
208
|
+
max_bid = max(self._bids.values())
|
|
209
|
+
highest_bidders = sorted([pid for pid, amt in self._bids.items() if amt == max_bid])
|
|
210
|
+
|
|
211
|
+
if len(highest_bidders) == 1:
|
|
212
|
+
return highest_bidders
|
|
213
|
+
|
|
214
|
+
# Tie-breaking logic
|
|
215
|
+
candidates = highest_bidders
|
|
216
|
+
|
|
217
|
+
# Rule 1: Players mentioned in the last turn
|
|
218
|
+
mentioned_in_tie = [pid for pid in candidates if pid in self._mentioned_last_turn]
|
|
219
|
+
if mentioned_in_tie:
|
|
220
|
+
candidates = mentioned_in_tie
|
|
221
|
+
|
|
222
|
+
if len(candidates) == 1:
|
|
223
|
+
return candidates
|
|
224
|
+
|
|
225
|
+
# Rule 2: The least spoken individual
|
|
226
|
+
speech_counts = Counter(
|
|
227
|
+
entry.data.actor_id
|
|
228
|
+
for day_events in state.history.values()
|
|
229
|
+
for entry in day_events
|
|
230
|
+
if entry.event_name == EventName.DISCUSSION and isinstance(entry.data, ChatDataEntry)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
candidate_speech_counts = {pid: speech_counts.get(pid, 0) for pid in candidates}
|
|
234
|
+
min_spoken = min(candidate_speech_counts.values())
|
|
235
|
+
least_spoken_candidates = sorted([pid for pid, count in candidate_speech_counts.items() if count == min_spoken])
|
|
236
|
+
|
|
237
|
+
if len(least_spoken_candidates) == 1:
|
|
238
|
+
return least_spoken_candidates
|
|
239
|
+
|
|
240
|
+
candidates = least_spoken_candidates
|
|
241
|
+
|
|
242
|
+
# Rule 3: Round robin order of the player list in state
|
|
243
|
+
for pid in state.all_player_ids:
|
|
244
|
+
if pid in candidates:
|
|
245
|
+
return [pid]
|
|
246
|
+
|
|
247
|
+
# This part should be unreachable if candidates is a subset of all_player_ids
|
|
248
|
+
return [candidates[0]] if candidates else []
|