Eric Botti
commited on
Commit
·
1373c22
1
Parent(s):
ae85ba5
player and game abstraction, moved message level control to player
Browse files- src/agent_interfaces.py +8 -6
- src/app.py +3 -3
- src/game.py +84 -280
- src/game_chameleon.py +237 -0
- src/game_utils.py +13 -41
- src/game_utils_chameleon.py +28 -0
- src/main.py +2 -5
- src/message.py +1 -0
- src/player.py +75 -26
src/agent_interfaces.py
CHANGED
@@ -21,10 +21,15 @@ class BaseAgentInterface:
|
|
21 |
|
22 |
def __init__(
|
23 |
self,
|
24 |
-
agent_id: str = None
|
|
|
25 |
):
|
26 |
self.id = agent_id
|
|
|
|
|
|
|
27 |
self.messages = []
|
|
|
28 |
|
29 |
@property
|
30 |
def is_ai(self):
|
@@ -33,7 +38,8 @@ class BaseAgentInterface:
|
|
33 |
def add_message(self, message: Message):
|
34 |
"""Adds a message to the message history, without generating a response."""
|
35 |
bound_message = AgentMessage.from_message(message, self.id, len(self.messages))
|
36 |
-
|
|
|
37 |
self.messages.append(bound_message)
|
38 |
|
39 |
# Respond To methods - These take a message as input and generate a response
|
@@ -166,10 +172,6 @@ class HumanAgentInterface(BaseAgentInterface):
|
|
166 |
|
167 |
class HumanAgentCLI(HumanAgentInterface):
|
168 |
"""A Human agent that uses the command line interface to generate responses."""
|
169 |
-
|
170 |
-
def __init__(self, agent_id: str):
|
171 |
-
super().__init__(agent_id)
|
172 |
-
|
173 |
def add_message(self, message: Message):
|
174 |
super().add_message(message)
|
175 |
if message.type == "verbose":
|
|
|
21 |
|
22 |
def __init__(
|
23 |
self,
|
24 |
+
agent_id: str = None,
|
25 |
+
log_messages: bool = True
|
26 |
):
|
27 |
self.id = agent_id
|
28 |
+
"""The id of the agent."""
|
29 |
+
self.log_messages = log_messages
|
30 |
+
"""Whether to log messages or not."""
|
31 |
self.messages = []
|
32 |
+
"""The message history of the agent."""
|
33 |
|
34 |
@property
|
35 |
def is_ai(self):
|
|
|
38 |
def add_message(self, message: Message):
|
39 |
"""Adds a message to the message history, without generating a response."""
|
40 |
bound_message = AgentMessage.from_message(message, self.id, len(self.messages))
|
41 |
+
if self.log_messages:
|
42 |
+
save(bound_message)
|
43 |
self.messages.append(bound_message)
|
44 |
|
45 |
# Respond To methods - These take a message as input and generate a response
|
|
|
172 |
|
173 |
class HumanAgentCLI(HumanAgentInterface):
|
174 |
"""A Human agent that uses the command line interface to generate responses."""
|
|
|
|
|
|
|
|
|
175 |
def add_message(self, message: Message):
|
176 |
super().add_message(message)
|
177 |
if message.type == "verbose":
|
src/app.py
CHANGED
@@ -3,7 +3,7 @@ from typing import Type
|
|
3 |
import streamlit as st
|
4 |
from streamlit import session_state
|
5 |
|
6 |
-
from
|
7 |
from agent_interfaces import HumanAgentInterface
|
8 |
from message import Message
|
9 |
from prompts import fetch_prompt, format_prompt
|
@@ -81,7 +81,7 @@ class StreamlitChameleonGame(ChameleonGame):
|
|
81 |
self.game_message("All players have spoken. The Chameleon will now guess the secret animal...")
|
82 |
player_responses = self.format_animal_descriptions(exclude=self.chameleon)
|
83 |
self.game_message(format_prompt("chameleon_guess_animal", player_responses=player_responses), self.chameleon)
|
84 |
-
if self.
|
85 |
if not session_state.awaiting_human_input:
|
86 |
session_state.awaiting_human_input = True
|
87 |
else:
|
@@ -141,7 +141,7 @@ with center:
|
|
141 |
|
142 |
if user_input:
|
143 |
if "game" not in st.session_state:
|
144 |
-
st.session_state.game = StreamlitChameleonGame(
|
145 |
session_state.user_input = user_input
|
146 |
st.session_state.game.run_game()
|
147 |
|
|
|
3 |
import streamlit as st
|
4 |
from streamlit import session_state
|
5 |
|
6 |
+
from game_chameleon import ChameleonGame
|
7 |
from agent_interfaces import HumanAgentInterface
|
8 |
from message import Message
|
9 |
from prompts import fetch_prompt, format_prompt
|
|
|
81 |
self.game_message("All players have spoken. The Chameleon will now guess the secret animal...")
|
82 |
player_responses = self.format_animal_descriptions(exclude=self.chameleon)
|
83 |
self.game_message(format_prompt("chameleon_guess_animal", player_responses=player_responses), self.chameleon)
|
84 |
+
if self.human_player().role == "chameleon":
|
85 |
if not session_state.awaiting_human_input:
|
86 |
session_state.awaiting_human_input = True
|
87 |
else:
|
|
|
141 |
|
142 |
if user_input:
|
143 |
if "game" not in st.session_state:
|
144 |
+
st.session_state.game = StreamlitChameleonGame.from_human_name(user_input, StreamlitInterface)
|
145 |
session_state.user_input = user_input
|
146 |
st.session_state.game.run_game()
|
147 |
|
src/game.py
CHANGED
@@ -1,92 +1,33 @@
|
|
1 |
-
import
|
2 |
-
from datetime import datetime
|
3 |
-
from typing import Optional, Type
|
4 |
-
|
5 |
-
from colorama import Fore, Style
|
6 |
|
7 |
from game_utils import *
|
8 |
-
from output_formats import *
|
9 |
from player import Player
|
10 |
-
from
|
11 |
-
from message import Message
|
12 |
from agent_interfaces import HumanAgentCLI, OpenAIAgentInterface, HumanAgentInterface
|
13 |
|
14 |
-
# Default Values
|
15 |
-
NUMBER_OF_PLAYERS = 6
|
16 |
-
WINNING_SCORE = 3
|
17 |
-
|
18 |
|
19 |
# Abstracting the Game Class is a WIP so that future games can be added
|
20 |
class Game:
|
21 |
"""Base class for all games."""
|
22 |
-
def __init__(self, verbose: bool = False, debug: bool = False):
|
23 |
-
self.game_id = game_id()
|
24 |
-
"""The unique id of the game."""
|
25 |
-
self.start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
26 |
-
"""The time the game was started."""
|
27 |
-
self.verbose = verbose
|
28 |
-
"""If True, the game will display verbose messages to the player."""
|
29 |
-
self.debug = debug
|
30 |
-
"""If True, the game will display debug messages to the player."""
|
31 |
-
self.winner_id: str | None = None
|
32 |
-
"""The id of the player who has won the game."""
|
33 |
|
34 |
-
|
35 |
-
|
36 |
-
"""The main game class, handles the game logic and player interactions."""
|
37 |
-
|
38 |
-
winning_score = WINNING_SCORE
|
39 |
-
"""The Number of points required to win the game."""
|
40 |
|
41 |
def __init__(
|
42 |
self,
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
verbose: bool = False,
|
47 |
-
debug: bool = False
|
48 |
):
|
49 |
-
|
50 |
-
|
51 |
-
self.
|
52 |
-
"""
|
53 |
-
self.
|
54 |
-
"""
|
55 |
-
self.all_animal_descriptions: List[List[dict]] = []
|
56 |
-
"""Record of the animal descriptions each player has given for each round."""
|
57 |
-
self.chameleon_guesses: List[str] = []
|
58 |
-
"""Record of what animal the chameleon guessed for each round."""
|
59 |
-
self.herd_vote_tallies: List[List[dict]] = []
|
60 |
-
"""Record of the votes of each herd member for the chameleon for each round."""
|
61 |
-
|
62 |
-
# Gather Player Names
|
63 |
-
if human_name:
|
64 |
-
ai_names = random_names(number_of_players - 1, human_name)
|
65 |
-
self.human_index = random_index(number_of_players)
|
66 |
-
else:
|
67 |
-
ai_names = random_names(number_of_players)
|
68 |
-
self.human_index = None
|
69 |
-
|
70 |
-
# Add Players
|
71 |
-
self.players = []
|
72 |
-
|
73 |
-
for i in range(0, number_of_players):
|
74 |
-
player_id = f"{self.game_id}-{i + 1}"
|
75 |
-
|
76 |
-
if self.human_index == i:
|
77 |
-
name = human_name
|
78 |
-
interface = human_interface(player_id)
|
79 |
-
else:
|
80 |
-
name = ai_names.pop()
|
81 |
-
interface = OpenAIAgentInterface(player_id)
|
82 |
-
|
83 |
-
self.players.append(Player(name, player_id, interface))
|
84 |
|
85 |
-
|
86 |
-
|
87 |
-
self.observer = human_interface(f"{self.game_id}-observer")
|
88 |
-
else:
|
89 |
-
self.observer = None
|
90 |
|
91 |
def player_from_id(self, player_id: str) -> Player:
|
92 |
"""Returns a player from their ID."""
|
@@ -96,236 +37,99 @@ class ChameleonGame(Game):
|
|
96 |
"""Returns a player from their name."""
|
97 |
return next((player for player in self.players if player.name == name), None)
|
98 |
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
return self.player_from_id(self.chameleon_ids[-1])
|
103 |
-
|
104 |
-
@property
|
105 |
-
def herd_animal(self) -> str:
|
106 |
-
"""Returns the current herd animal."""
|
107 |
-
return self.herd_animals[-1]
|
108 |
-
|
109 |
-
@property
|
110 |
-
def round_animal_descriptions(self) -> List[dict]:
|
111 |
-
"""Returns the current animal descriptions."""
|
112 |
-
return self.all_animal_descriptions[-1]
|
113 |
-
|
114 |
-
@property
|
115 |
-
def chameleon_guess(self) -> str:
|
116 |
-
"""Returns the current chameleon guess."""
|
117 |
-
return self.chameleon_guesses[-1]
|
118 |
-
|
119 |
-
@property
|
120 |
-
def herd_vote_tally(self) -> List[dict]:
|
121 |
-
"""Returns the current herd vote tally."""
|
122 |
-
return self.herd_vote_tallies[-1]
|
123 |
-
|
124 |
-
@property
|
125 |
-
def round_number(self) -> int:
|
126 |
-
"""Returns the current round number."""
|
127 |
-
return len(self.herd_animals)
|
128 |
-
|
129 |
-
def format_animal_descriptions(self, exclude: Player = None) -> str:
|
130 |
-
"""Formats the animal description responses of the players into a single string."""
|
131 |
-
formatted_responses = ""
|
132 |
-
for response in self.round_animal_descriptions:
|
133 |
-
# Used to exclude the player who is currently responding, so they don't vote for themselves like a fool
|
134 |
-
if response["player_id"] != exclude.id:
|
135 |
-
player = self.player_from_id(response["player_id"])
|
136 |
-
formatted_responses += f" - {player.name}: {response['description']}\n"
|
137 |
-
|
138 |
-
return formatted_responses
|
139 |
-
|
140 |
-
def observer_message(self, message: Message):
|
141 |
-
"""Sends a message to the observer if there is one."""
|
142 |
-
if self.observer:
|
143 |
-
self.observer.add_message(message)
|
144 |
|
145 |
def game_message(
|
146 |
-
self,
|
|
|
147 |
recipient: Optional[Player] = None, # If None, message is broadcast to all players
|
148 |
-
exclude: bool = False # If True, the message is broadcast to all players except the chosen player
|
|
|
149 |
):
|
150 |
-
"""
|
151 |
-
message
|
|
|
|
|
|
|
|
|
|
|
152 |
|
153 |
if exclude or not recipient:
|
154 |
-
for player in self.players:
|
155 |
-
if player != recipient:
|
156 |
player.interface.add_message(message)
|
157 |
-
self.observer_message(message)
|
158 |
else:
|
159 |
recipient.interface.add_message(message)
|
160 |
|
161 |
-
def verbose_message(self, content: str):
|
162 |
-
"""
|
163 |
-
|
164 |
-
|
165 |
-
if self.human_index:
|
166 |
-
self.players[self.human_index].interface.add_message(message)
|
167 |
-
self.observer_message(message)
|
168 |
-
|
169 |
-
def debug_message(self, content: str):
|
170 |
-
"""Sends a message for a human observer. These messages contain secret information about the players such as their role."""
|
171 |
-
if self.debug:
|
172 |
-
message = Message(type="debug", content=content)
|
173 |
-
if self.human_index:
|
174 |
-
self.players[self.human_index].interface.add_message(message)
|
175 |
-
self.observer_message(message)
|
176 |
|
|
|
|
|
|
|
177 |
|
178 |
-
|
179 |
-
"""
|
180 |
-
|
|
|
181 |
|
182 |
-
|
|
|
|
|
183 |
|
184 |
-
|
|
|
|
|
185 |
|
186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
187 |
|
188 |
-
#
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
|
196 |
-
|
197 |
-
|
198 |
-
# Choose Animal
|
199 |
-
herd_animal = random_animal()
|
200 |
-
self.herd_animals.append(herd_animal)
|
201 |
-
self.debug_message(f"The secret animal is {herd_animal}.")
|
202 |
|
203 |
-
|
204 |
-
|
205 |
-
self.chameleon_ids.append(self.players[chameleon_index].id)
|
206 |
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
self.debug_message(f"{player.name} is the Chameleon!")
|
212 |
else:
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
self.all_animal_descriptions.append([])
|
218 |
-
|
219 |
-
# Empty Tally for Votes
|
220 |
-
self.herd_vote_tallies.append([])
|
221 |
-
|
222 |
-
self.game_message(f"Each player will now take turns describing themselves:")
|
223 |
-
|
224 |
-
def run_round(self):
|
225 |
-
"""Starts the round."""
|
226 |
-
# Phase I: Collect Player Animal Descriptions
|
227 |
-
for current_player in self.players:
|
228 |
-
self.game_message(fetch_prompt("player_describe_animal"), current_player)
|
229 |
-
self.player_turn_animal_description(current_player)
|
230 |
-
|
231 |
-
# Phase II: Chameleon Guesses the Animal
|
232 |
-
self.game_message("All players have spoken. The Chameleon will now guess the secret animal...")
|
233 |
-
player_responses = self.format_animal_descriptions(exclude=self.chameleon)
|
234 |
-
self.game_message(format_prompt("chameleon_guess_animal", player_responses=player_responses), self.chameleon)
|
235 |
-
self.player_turn_chameleon_guess(self.chameleon)
|
236 |
-
|
237 |
-
# Phase III: The Herd Votes for who they think the Chameleon is
|
238 |
-
for current_player in self.players:
|
239 |
-
if current_player.role == "herd":
|
240 |
-
player_responses = self.format_animal_descriptions(exclude=current_player)
|
241 |
-
self.game_message(format_prompt("vote", player_responses=player_responses), current_player)
|
242 |
-
self.player_turn_herd_vote(current_player)
|
243 |
-
|
244 |
-
def player_turn_animal_description(self, player: Player):
|
245 |
-
"""Handles a player's turn to describe themselves."""
|
246 |
-
if player.interface.is_ai:
|
247 |
-
self.verbose_message(f"{player.name} is thinking...")
|
248 |
-
|
249 |
-
prompt = fetch_prompt("player_describe_animal")
|
250 |
-
|
251 |
-
# Get Player Animal Description
|
252 |
-
response = player.interface.generate_formatted_response(AnimalDescriptionFormat)
|
253 |
-
|
254 |
-
self.round_animal_descriptions.append({"player_id": player.id, "description": response.description})
|
255 |
-
|
256 |
-
self.game_message(f"{player.name}: {response.description}", player, exclude=True)
|
257 |
-
|
258 |
-
def player_turn_chameleon_guess(self, chameleon: Player):
|
259 |
-
"""Handles the Chameleon's turn to guess the secret animal."""
|
260 |
-
|
261 |
-
if chameleon.interface.is_ai or self.observer:
|
262 |
-
self.verbose_message("The Chameleon is thinking...")
|
263 |
-
|
264 |
-
response = chameleon.interface.generate_formatted_response(ChameleonGuessFormat)
|
265 |
-
|
266 |
-
self.game_message("The Chameleon has guessed the animal. Now the Herd will vote on who they think the chameleon is.")
|
267 |
-
|
268 |
-
self.chameleon_guesses.append(response.animal)
|
269 |
-
|
270 |
-
def player_turn_herd_vote(self, player: Player):
|
271 |
-
"""Handles a player's turn to vote for the Chameleon."""
|
272 |
-
if player.interface.is_ai:
|
273 |
-
self.verbose_message(f"{player.name} is thinking...")
|
274 |
-
|
275 |
-
# Get Player Vote
|
276 |
-
additional_fields = {"player_names": [p.name for p in self.players if p != player]}
|
277 |
-
response = player.interface.generate_formatted_response(HerdVoteFormat, additional_fields=additional_fields)
|
278 |
-
|
279 |
-
if player.interface.is_ai:
|
280 |
-
self.debug_message(f"{player.name} voted for {response.vote}")
|
281 |
-
|
282 |
-
voted_for_player = self.player_from_name(response.vote)
|
283 |
-
|
284 |
-
# Add Vote to Player Votes
|
285 |
-
self.herd_vote_tally.append({"voter_id": player.id, "voted_for_id": voted_for_player.id})
|
286 |
-
|
287 |
-
def resolve_round(self):
|
288 |
-
"""Resolves the round, assigns points, and prints the results."""
|
289 |
-
self.game_message("All players have voted!")
|
290 |
-
for vote in self.herd_vote_tally:
|
291 |
-
voter = self.player_from_id(vote["voter_id"])
|
292 |
-
voted_for = self.player_from_id(vote["voted_for_id"])
|
293 |
-
self.game_message(f"{voter.name} voted for {voted_for.name}")
|
294 |
-
|
295 |
-
accused_player_id = count_chameleon_votes(self.herd_vote_tally)
|
296 |
-
|
297 |
-
self.game_message(f"The round is over. Calculating results...")
|
298 |
-
self.game_message(
|
299 |
-
f"The Chameleon was {self.chameleon.name}, and they guessed the secret animal was {self.chameleon_guess}.")
|
300 |
-
self.game_message(f"The secret animal was actually was {self.herd_animal}.")
|
301 |
-
|
302 |
-
if accused_player_id:
|
303 |
-
accused_name = self.player_from_id(accused_player_id).name
|
304 |
-
self.game_message(f"The Herd voted for {accused_name} as the Chameleon.")
|
305 |
-
else:
|
306 |
-
self.game_message(f"The Herd could not come to a consensus.")
|
307 |
|
308 |
-
|
309 |
-
# If the Chameleon guesses the correct animal = +1 Point to the Chameleon
|
310 |
-
if self.chameleon_guess.lower() == self.herd_animal.lower():
|
311 |
-
self.chameleon.points += 1
|
312 |
|
313 |
-
#
|
|
|
|
|
314 |
else:
|
315 |
-
|
316 |
-
if player.role == "herd":
|
317 |
-
player.points += 1
|
318 |
-
# If a Herd player votes for the Chameleon = +1 Point to that player
|
319 |
-
for vote in self.herd_vote_tally:
|
320 |
-
if vote["voted_for_id"] == self.chameleon.id:
|
321 |
-
self.player_from_id(vote['voter_id']).points += 1
|
322 |
|
323 |
-
|
324 |
-
if not accused_player_id or accused_player_id != self.chameleon.id:
|
325 |
-
self.chameleon.points += 1
|
326 |
|
327 |
-
# Print Scores
|
328 |
-
player_points = "\n".join([f"{player.name}: {player.points}" for player in self.players])
|
329 |
-
self.game_message(f"Current Game Score:\n{player_points}")
|
330 |
|
331 |
|
|
|
1 |
+
from typing import Optional, Type, List
|
|
|
|
|
|
|
|
|
2 |
|
3 |
from game_utils import *
|
|
|
4 |
from player import Player
|
5 |
+
from message import Message, MessageType
|
|
|
6 |
from agent_interfaces import HumanAgentCLI, OpenAIAgentInterface, HumanAgentInterface
|
7 |
|
|
|
|
|
|
|
|
|
8 |
|
9 |
# Abstracting the Game Class is a WIP so that future games can be added
|
10 |
class Game:
|
11 |
"""Base class for all games."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
+
number_of_players: int
|
14 |
+
"""The number of players in the game."""
|
|
|
|
|
|
|
|
|
15 |
|
16 |
def __init__(
|
17 |
self,
|
18 |
+
game_id: str,
|
19 |
+
players: List[Player],
|
20 |
+
observer: Optional[Player] = None
|
|
|
|
|
21 |
):
|
22 |
+
self.players: List[Player] = players
|
23 |
+
"""The players in the game."""
|
24 |
+
self.observer: Optional[Player] = observer
|
25 |
+
"""An observer who can see all public messages, but doesn't actually play."""
|
26 |
+
self.game_id = game_id
|
27 |
+
"""The unique id of the game."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
|
29 |
+
self.winner_id: str | None = None
|
30 |
+
"""The id of the player who has won the game."""
|
|
|
|
|
|
|
31 |
|
32 |
def player_from_id(self, player_id: str) -> Player:
|
33 |
"""Returns a player from their ID."""
|
|
|
37 |
"""Returns a player from their name."""
|
38 |
return next((player for player in self.players if player.name == name), None)
|
39 |
|
40 |
+
def human_player(self) -> Player:
|
41 |
+
"""Returns the human player."""
|
42 |
+
return next((player for player in self.players if player.interface.is_human), None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
|
44 |
def game_message(
|
45 |
+
self,
|
46 |
+
content: str,
|
47 |
recipient: Optional[Player] = None, # If None, message is broadcast to all players
|
48 |
+
exclude: bool = False, # If True, the message is broadcast to all players except the chosen player
|
49 |
+
message_type: MessageType = "info"
|
50 |
):
|
51 |
+
"""
|
52 |
+
Sends a message to a player or all players.
|
53 |
+
If no recipient is specified, the message is broadcast to all players.
|
54 |
+
If exclude is True, the message is broadcast to all players except the recipient.
|
55 |
+
Some message types are only available to player with access (e.g. verbose, debug).
|
56 |
+
"""
|
57 |
+
message = Message(type=message_type, content=content)
|
58 |
|
59 |
if exclude or not recipient:
|
60 |
+
for player in self.players + [self.observer] if self.observer else self.players:
|
61 |
+
if player != recipient and player.can_receive_message(message_type):
|
62 |
player.interface.add_message(message)
|
|
|
63 |
else:
|
64 |
recipient.interface.add_message(message)
|
65 |
|
66 |
+
def verbose_message(self, content: str, **kwargs):
|
67 |
+
"""
|
68 |
+
Sends a verbose message to all players capable of receiving them.
|
69 |
+
Verbose messages are used to communicate in real time what is happening that cannot be seen publicly.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
|
71 |
+
Ex: "Abby is thinking..."
|
72 |
+
"""
|
73 |
+
self.game_message(content, **kwargs, message_type="verbose")
|
74 |
|
75 |
+
def debug_message(self, content: str, **kwargs):
|
76 |
+
"""
|
77 |
+
Sends a debug message to all players capable of receiving them.
|
78 |
+
Debug messages usually contain secret information and should only be sent when it wouldn't spoil the game.
|
79 |
|
80 |
+
Ex: "Abby is the chameleon."
|
81 |
+
"""
|
82 |
+
self.game_message(content, **kwargs, message_type="debug")
|
83 |
|
84 |
+
def run_game(self):
|
85 |
+
"""Runs the game."""
|
86 |
+
raise NotImplementedError("The run_game method must be implemented by the subclass.")
|
87 |
|
88 |
+
@classmethod
|
89 |
+
def from_human_name(
|
90 |
+
cls, human_name: str = None,
|
91 |
+
human_interface: Type[HumanAgentInterface] = HumanAgentCLI,
|
92 |
+
human_message_level: str = "verbose"
|
93 |
+
):
|
94 |
+
"""
|
95 |
+
Instantiates a game with a human player if a name is provided.
|
96 |
+
Otherwise, the game is instantiated with all AI players and an observer.
|
97 |
+
"""
|
98 |
+
game_id = generate_game_id()
|
99 |
|
100 |
+
# Gather Player Names
|
101 |
+
if human_name:
|
102 |
+
ai_names = random_names(cls.number_of_players - 1, human_name)
|
103 |
+
human_index = random_index(cls.number_of_players)
|
104 |
+
else:
|
105 |
+
ai_names = random_names(cls.number_of_players)
|
106 |
+
human_index = None
|
107 |
|
108 |
+
# Add Players
|
109 |
+
players = []
|
|
|
|
|
|
|
|
|
110 |
|
111 |
+
for i in range(0, cls.number_of_players):
|
112 |
+
player_id = f"{game_id}-{i + 1}"
|
|
|
113 |
|
114 |
+
if human_index == i:
|
115 |
+
name = human_name
|
116 |
+
interface = human_interface(player_id)
|
117 |
+
message_level = human_message_level
|
|
|
118 |
else:
|
119 |
+
name = ai_names.pop()
|
120 |
+
# all AI players use the OpenAI interface for now - this can be changed in the future
|
121 |
+
interface = OpenAIAgentInterface(player_id)
|
122 |
+
message_level = "info"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
|
124 |
+
players.append(Player(name, player_id, interface, message_level))
|
|
|
|
|
|
|
125 |
|
126 |
+
# Add Observer - an Agent who can see all the messages, but doesn't actually play
|
127 |
+
if human_index is None:
|
128 |
+
observer = Player.observer(game_id, interface_type=human_interface)
|
129 |
else:
|
130 |
+
observer = None
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
|
132 |
+
return cls(game_id, players, observer)
|
|
|
|
|
133 |
|
|
|
|
|
|
|
134 |
|
135 |
|
src/game_chameleon.py
ADDED
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Optional, Type
|
2 |
+
|
3 |
+
from game_utils import random_index
|
4 |
+
from game_utils_chameleon import *
|
5 |
+
from output_formats import *
|
6 |
+
from player import ChameleonPlayer, Player
|
7 |
+
from prompts import fetch_prompt, format_prompt
|
8 |
+
from message import Message, MessageType
|
9 |
+
from agent_interfaces import HumanAgentCLI, OpenAIAgentInterface, HumanAgentInterface
|
10 |
+
from game import Game
|
11 |
+
|
12 |
+
# Default Values
|
13 |
+
NUMBER_OF_PLAYERS = 6
|
14 |
+
WINNING_SCORE = 3
|
15 |
+
|
16 |
+
|
17 |
+
class ChameleonGame(Game):
|
18 |
+
"""The main game class, handles the game logic and player interactions."""
|
19 |
+
|
20 |
+
number_of_players = NUMBER_OF_PLAYERS
|
21 |
+
|
22 |
+
winning_score = WINNING_SCORE
|
23 |
+
"""The Number of points required to win the game."""
|
24 |
+
|
25 |
+
def __init__(self, *args, **kwargs):
|
26 |
+
|
27 |
+
super().__init__(*args, **kwargs)
|
28 |
+
|
29 |
+
# Convert the Players to ChameleonPlayers
|
30 |
+
self.players: List[ChameleonPlayer] = [ChameleonPlayer.from_player(player) for player in self.players]
|
31 |
+
|
32 |
+
# Instance Variables
|
33 |
+
self.chameleon_ids: List[str] = []
|
34 |
+
"""Record of which player was the chameleon for each round."""
|
35 |
+
self.herd_animals: List[str] = []
|
36 |
+
"""Record of what animal was the herd animal for each round."""
|
37 |
+
self.all_animal_descriptions: List[List[dict]] = []
|
38 |
+
"""Record of the animal descriptions each player has given for each round."""
|
39 |
+
self.chameleon_guesses: List[str] = []
|
40 |
+
"""Record of what animal the chameleon guessed for each round."""
|
41 |
+
self.herd_vote_tallies: List[List[dict]] = []
|
42 |
+
"""Record of the votes of each herd member for the chameleon for each round."""
|
43 |
+
|
44 |
+
|
45 |
+
|
46 |
+
@property
|
47 |
+
def chameleon(self) -> ChameleonPlayer:
|
48 |
+
"""Returns the current chameleon."""
|
49 |
+
return self.player_from_id(self.chameleon_ids[-1])
|
50 |
+
|
51 |
+
@property
|
52 |
+
def herd_animal(self) -> str:
|
53 |
+
"""Returns the current herd animal."""
|
54 |
+
return self.herd_animals[-1]
|
55 |
+
|
56 |
+
@property
|
57 |
+
def round_animal_descriptions(self) -> List[dict]:
|
58 |
+
"""Returns the current animal descriptions."""
|
59 |
+
return self.all_animal_descriptions[-1]
|
60 |
+
|
61 |
+
@property
|
62 |
+
def chameleon_guess(self) -> str:
|
63 |
+
"""Returns the current chameleon guess."""
|
64 |
+
return self.chameleon_guesses[-1]
|
65 |
+
|
66 |
+
@property
|
67 |
+
def herd_vote_tally(self) -> List[dict]:
|
68 |
+
"""Returns the current herd vote tally."""
|
69 |
+
return self.herd_vote_tallies[-1]
|
70 |
+
|
71 |
+
@property
|
72 |
+
def round_number(self) -> int:
|
73 |
+
"""Returns the current round number."""
|
74 |
+
return len(self.herd_animals)
|
75 |
+
|
76 |
+
def format_animal_descriptions(self, exclude: Player = None) -> str:
|
77 |
+
"""Formats the animal description responses of the players into a single string."""
|
78 |
+
formatted_responses = ""
|
79 |
+
for response in self.round_animal_descriptions:
|
80 |
+
# Used to exclude the player who is currently responding, so they don't vote for themselves like a fool
|
81 |
+
if response["player_id"] != exclude.id:
|
82 |
+
player = self.player_from_id(response["player_id"])
|
83 |
+
formatted_responses += f" - {player.name}: {response['description']}\n"
|
84 |
+
|
85 |
+
return formatted_responses
|
86 |
+
|
87 |
+
async def run_game(self):
|
88 |
+
"""Sets up the game. This includes assigning roles and gathering player names."""
|
89 |
+
self.game_message(fetch_prompt("game_rules"))
|
90 |
+
|
91 |
+
self.setup_round()
|
92 |
+
|
93 |
+
self.run_round()
|
94 |
+
|
95 |
+
self.resolve_round()
|
96 |
+
|
97 |
+
# # Log Game Info
|
98 |
+
# game_log = {
|
99 |
+
# "game_id": self.game_id,
|
100 |
+
# "start_time": self.start_time,
|
101 |
+
# "number_of_players": len(self.players),
|
102 |
+
# "human_player": self.players[self.human_index].id if self.human_index else "None",
|
103 |
+
# }
|
104 |
+
|
105 |
+
def setup_round(self):
|
106 |
+
"""Sets up the round. This includes assigning roles and gathering player names."""
|
107 |
+
# Choose Animal
|
108 |
+
herd_animal = random_animal()
|
109 |
+
self.herd_animals.append(herd_animal)
|
110 |
+
self.debug_message(f"The secret animal is {herd_animal}.")
|
111 |
+
|
112 |
+
# Assign Roles
|
113 |
+
chameleon_index = random_index(len(self.players))
|
114 |
+
self.chameleon_ids.append(self.players[chameleon_index].id)
|
115 |
+
|
116 |
+
for i, player in enumerate(self.players):
|
117 |
+
if i == chameleon_index:
|
118 |
+
player.assign_role("chameleon")
|
119 |
+
self.game_message(fetch_prompt("assign_chameleon"), player)
|
120 |
+
self.debug_message(f"{player.name} is the Chameleon!")
|
121 |
+
else:
|
122 |
+
player.assign_role("herd")
|
123 |
+
self.game_message(format_prompt("assign_herd", herd_animal=herd_animal), player)
|
124 |
+
|
125 |
+
# Empty Animal Descriptions
|
126 |
+
self.all_animal_descriptions.append([])
|
127 |
+
|
128 |
+
# Empty Tally for Votes
|
129 |
+
self.herd_vote_tallies.append([])
|
130 |
+
|
131 |
+
self.game_message(f"Each player will now take turns describing themselves:")
|
132 |
+
|
133 |
+
def run_round(self):
|
134 |
+
"""Starts the round."""
|
135 |
+
# Phase I: Collect Player Animal Descriptions
|
136 |
+
for current_player in self.players:
|
137 |
+
self.game_message(fetch_prompt("player_describe_animal"), current_player)
|
138 |
+
self.player_turn_animal_description(current_player)
|
139 |
+
|
140 |
+
# Phase II: Chameleon Guesses the Animal
|
141 |
+
self.game_message("All players have spoken. The Chameleon will now guess the secret animal...")
|
142 |
+
player_responses = self.format_animal_descriptions(exclude=self.chameleon)
|
143 |
+
self.game_message(format_prompt("chameleon_guess_animal", player_responses=player_responses), self.chameleon)
|
144 |
+
self.player_turn_chameleon_guess(self.chameleon)
|
145 |
+
|
146 |
+
# Phase III: The Herd Votes for who they think the Chameleon is
|
147 |
+
for current_player in self.players:
|
148 |
+
if current_player.role == "herd":
|
149 |
+
player_responses = self.format_animal_descriptions(exclude=current_player)
|
150 |
+
self.game_message(format_prompt("vote", player_responses=player_responses), current_player)
|
151 |
+
self.player_turn_herd_vote(current_player)
|
152 |
+
|
153 |
+
def player_turn_animal_description(self, player: Player):
|
154 |
+
"""Handles a player's turn to describe themselves."""
|
155 |
+
if player.interface.is_ai:
|
156 |
+
self.verbose_message(f"{player.name} is thinking...")
|
157 |
+
|
158 |
+
prompt = fetch_prompt("player_describe_animal")
|
159 |
+
|
160 |
+
# Get Player Animal Description
|
161 |
+
response = player.interface.generate_formatted_response(AnimalDescriptionFormat)
|
162 |
+
|
163 |
+
self.round_animal_descriptions.append({"player_id": player.id, "description": response.description})
|
164 |
+
|
165 |
+
self.game_message(f"{player.name}: {response.description}", player, exclude=True)
|
166 |
+
|
167 |
+
def player_turn_chameleon_guess(self, chameleon: Player):
|
168 |
+
"""Handles the Chameleon's turn to guess the secret animal."""
|
169 |
+
|
170 |
+
if chameleon.interface.is_ai or self.observer:
|
171 |
+
self.verbose_message("The Chameleon is thinking...")
|
172 |
+
|
173 |
+
response = chameleon.interface.generate_formatted_response(ChameleonGuessFormat)
|
174 |
+
|
175 |
+
self.game_message("The Chameleon has guessed the animal. Now the Herd will vote on who they think the chameleon is.")
|
176 |
+
|
177 |
+
self.chameleon_guesses.append(response.animal)
|
178 |
+
|
179 |
+
def player_turn_herd_vote(self, player: Player):
|
180 |
+
"""Handles a player's turn to vote for the Chameleon."""
|
181 |
+
if player.interface.is_ai:
|
182 |
+
self.verbose_message(f"{player.name} is thinking...")
|
183 |
+
|
184 |
+
# Get Player Vote
|
185 |
+
additional_fields = {"player_names": [p.name for p in self.players if p != player]}
|
186 |
+
response = player.interface.generate_formatted_response(HerdVoteFormat, additional_fields=additional_fields)
|
187 |
+
|
188 |
+
self.debug_message(f"{player.name} voted for {response.vote}", recipient=player, exclude=True)
|
189 |
+
|
190 |
+
voted_for_player = self.player_from_name(response.vote)
|
191 |
+
|
192 |
+
# Add Vote to Player Votes
|
193 |
+
self.herd_vote_tally.append({"voter_id": player.id, "voted_for_id": voted_for_player.id})
|
194 |
+
|
195 |
+
def resolve_round(self):
|
196 |
+
"""Resolves the round, assigns points, and prints the results."""
|
197 |
+
self.game_message("All players have voted!")
|
198 |
+
for vote in self.herd_vote_tally:
|
199 |
+
voter = self.player_from_id(vote["voter_id"])
|
200 |
+
voted_for = self.player_from_id(vote["voted_for_id"])
|
201 |
+
self.game_message(f"{voter.name} voted for {voted_for.name}")
|
202 |
+
|
203 |
+
accused_player_id = count_chameleon_votes(self.herd_vote_tally)
|
204 |
+
|
205 |
+
self.game_message(f"The round is over. Calculating results...")
|
206 |
+
self.game_message(
|
207 |
+
f"The Chameleon was {self.chameleon.name}, and they guessed the secret animal was {self.chameleon_guess}.")
|
208 |
+
self.game_message(f"The secret animal was actually was {self.herd_animal}.")
|
209 |
+
|
210 |
+
if accused_player_id:
|
211 |
+
accused_name = self.player_from_id(accused_player_id).name
|
212 |
+
self.game_message(f"The Herd voted for {accused_name} as the Chameleon.")
|
213 |
+
else:
|
214 |
+
self.game_message(f"The Herd could not come to a consensus.")
|
215 |
+
|
216 |
+
# Point Logic
|
217 |
+
# If the Chameleon guesses the correct animal = +1 Point to the Chameleon
|
218 |
+
if self.chameleon_guess.lower() == self.herd_animal.lower():
|
219 |
+
self.chameleon.points += 1
|
220 |
+
|
221 |
+
# If the Chameleon guesses the incorrect animal = +1 Point to each Herd player
|
222 |
+
else:
|
223 |
+
for player in self.players:
|
224 |
+
if player.role == "herd":
|
225 |
+
player.points += 1
|
226 |
+
# If a Herd player votes for the Chameleon = +1 Point to that player
|
227 |
+
for vote in self.herd_vote_tally:
|
228 |
+
if vote["voted_for_id"] == self.chameleon.id:
|
229 |
+
self.player_from_id(vote['voter_id']).points += 1
|
230 |
+
|
231 |
+
# If the Herd fails to accuse the Chameleon = +1 Point to the Chameleon
|
232 |
+
if not accused_player_id or accused_player_id != self.chameleon.id:
|
233 |
+
self.chameleon.points += 1
|
234 |
+
|
235 |
+
# Print Scores
|
236 |
+
player_points = "\n".join([f"{player.name}: {player.points}" for player in self.players])
|
237 |
+
self.game_message(f"Current Game Score:\n{player_points}")
|
src/game_utils.py
CHANGED
@@ -1,54 +1,26 @@
|
|
1 |
-
"""
|
2 |
-
Utilities for the game including random selections and prompts.
|
3 |
-
"""
|
4 |
-
import random
|
5 |
import string
|
6 |
-
import
|
7 |
-
from collections import Counter
|
8 |
|
|
|
9 |
ALPHABET = string.ascii_lowercase + string.digits
|
10 |
ID_LENGTH = 8
|
|
|
11 |
|
12 |
|
13 |
-
def
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
def random_animal():
|
18 |
-
return random.choice(available_animals)
|
19 |
|
20 |
-
|
21 |
-
available_animals = ["Giraffe", "Elephant", "Lion", "Zebra", "Monkey", "Gorilla"]
|
22 |
|
23 |
|
24 |
def random_names(number_of_samples: int, human_name: str = None) -> list[str]:
|
25 |
"""Returns a list of random names, excluding the one of the human player (if provided)"""
|
26 |
-
if human_name and human_name in
|
27 |
-
|
28 |
-
return random.sample(
|
29 |
-
|
30 |
-
|
31 |
-
available_names = ["Jack", "Jill", "Bob", "Courtney", "Fizz", "Mallory"]
|
32 |
-
|
33 |
-
|
34 |
-
def random_index(number_of_players : int) -> int:
|
35 |
-
return random.randint(0, number_of_players - 1)
|
36 |
-
|
37 |
-
|
38 |
-
def count_chameleon_votes(player_votes: list[dict]) -> str | None:
|
39 |
-
"""Counts the votes for each player."""
|
40 |
-
votes = [vote['voted_for_id'] for vote in player_votes]
|
41 |
-
|
42 |
-
freq = Counter(votes)
|
43 |
-
most_voted_player, number_of_votes = freq.most_common()[0]
|
44 |
-
|
45 |
-
# If one player has more than 50% of the votes, the herd accuses them of being the chameleon
|
46 |
-
if number_of_votes / len(player_votes) >= 0.5:
|
47 |
-
return most_voted_player
|
48 |
-
else:
|
49 |
-
return None
|
50 |
|
51 |
|
52 |
-
def
|
53 |
-
|
54 |
-
f.write(json.dumps(log_object) + "\n")
|
|
|
|
|
|
|
|
|
|
|
1 |
import string
|
2 |
+
import random
|
|
|
3 |
|
4 |
+
# Utilities
|
5 |
ALPHABET = string.ascii_lowercase + string.digits
|
6 |
ID_LENGTH = 8
|
7 |
+
AVAILABLE_NAMES = ["Jack", "Jill", "Bob", "Courtney", "Fizz", "Mallory"]
|
8 |
|
9 |
|
10 |
+
def generate_game_id():
|
11 |
+
"""Generates a unique game id."""
|
12 |
+
alphabet = string.ascii_lowercase + string.digits
|
13 |
+
id_length = 8
|
|
|
|
|
14 |
|
15 |
+
return ''.join(random.choices(alphabet, k=id_length)) # Using this instead of uuid for shorter game ids
|
|
|
16 |
|
17 |
|
18 |
def random_names(number_of_samples: int, human_name: str = None) -> list[str]:
|
19 |
"""Returns a list of random names, excluding the one of the human player (if provided)"""
|
20 |
+
if human_name and human_name in AVAILABLE_NAMES:
|
21 |
+
AVAILABLE_NAMES.remove(human_name)
|
22 |
+
return random.sample(AVAILABLE_NAMES, number_of_samples)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
|
25 |
+
def random_index(number_of_players: int) -> int:
|
26 |
+
return random.randint(0, number_of_players - 1)
|
|
src/game_utils_chameleon.py
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Utilities for the game including random selections and prompts.
|
3 |
+
"""
|
4 |
+
import random
|
5 |
+
import string
|
6 |
+
import json
|
7 |
+
from collections import Counter
|
8 |
+
|
9 |
+
|
10 |
+
def random_animal():
|
11 |
+
return random.choice(available_animals)
|
12 |
+
|
13 |
+
|
14 |
+
available_animals = ["Giraffe", "Elephant", "Lion", "Zebra", "Monkey", "Gorilla"]
|
15 |
+
|
16 |
+
|
17 |
+
def count_chameleon_votes(player_votes: list[dict]) -> str | None:
|
18 |
+
"""Counts the votes for each player."""
|
19 |
+
votes = [vote['voted_for_id'] for vote in player_votes]
|
20 |
+
|
21 |
+
freq = Counter(votes)
|
22 |
+
most_voted_player, number_of_votes = freq.most_common()[0]
|
23 |
+
|
24 |
+
# If one player has more than 50% of the votes, the herd accuses them of being the chameleon
|
25 |
+
if number_of_votes / len(player_votes) >= 0.5:
|
26 |
+
return most_voted_player
|
27 |
+
else:
|
28 |
+
return None
|
src/main.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
from
|
2 |
from player import Player
|
3 |
import asyncio
|
4 |
from player import Player
|
@@ -7,10 +7,7 @@ def main():
|
|
7 |
print("Please Enter your name, or leave blank to run an AI only game")
|
8 |
name = input()
|
9 |
|
10 |
-
|
11 |
-
game = ChameleonGame(human_name=name, verbose=True)
|
12 |
-
else:
|
13 |
-
game = ChameleonGame(verbose=True)
|
14 |
|
15 |
asyncio.run(game.run_game())
|
16 |
|
|
|
1 |
+
from game_chameleon import ChameleonGame
|
2 |
from player import Player
|
3 |
import asyncio
|
4 |
from player import Player
|
|
|
7 |
print("Please Enter your name, or leave blank to run an AI only game")
|
8 |
name = input()
|
9 |
|
10 |
+
game = ChameleonGame.from_human_name(name)
|
|
|
|
|
|
|
11 |
|
12 |
asyncio.run(game.run_game())
|
13 |
|
src/message.py
CHANGED
@@ -3,6 +3,7 @@ from pydantic import BaseModel, computed_field
|
|
3 |
|
4 |
MessageType = Literal["prompt", "info", "agent", "retry", "error", "format", "verbose", "debug"]
|
5 |
|
|
|
6 |
class Message(BaseModel):
|
7 |
"""A generic message, these are used to communicate between the game and the players."""
|
8 |
|
|
|
3 |
|
4 |
MessageType = Literal["prompt", "info", "agent", "retry", "error", "format", "verbose", "debug"]
|
5 |
|
6 |
+
|
7 |
class Message(BaseModel):
|
8 |
"""A generic message, these are used to communicate between the game and the players."""
|
9 |
|
src/player.py
CHANGED
@@ -1,38 +1,87 @@
|
|
1 |
-
from typing import Literal
|
2 |
-
import logging
|
3 |
|
4 |
-
from agent_interfaces import AgentInterface
|
|
|
5 |
|
6 |
Role = Literal["chameleon", "herd"]
|
7 |
|
8 |
-
logging.basicConfig(level=logging.WARNING)
|
9 |
-
logger = logging.getLogger("chameleon")
|
10 |
-
|
11 |
|
|
|
12 |
class Player:
|
|
|
13 |
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
points: int = 0
|
21 |
-
"""The number of points the player has."""
|
22 |
-
|
23 |
-
def __init__(
|
24 |
-
self,
|
25 |
-
name: str,
|
26 |
-
player_id: str,
|
27 |
-
interface: AgentInterface
|
28 |
-
):
|
29 |
self.name = name
|
|
|
30 |
self.id = player_id
|
|
|
31 |
self.interface = interface
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
|
33 |
def assign_role(self, role: Role):
|
34 |
-
self.role
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Literal, List, Type
|
|
|
2 |
|
3 |
+
from agent_interfaces import AgentInterface, HumanAgentCLI, BaseAgentInterface
|
4 |
+
from message import MessageType
|
5 |
|
6 |
Role = Literal["chameleon", "herd"]
|
7 |
|
|
|
|
|
|
|
8 |
|
9 |
+
# Abstraction is a WIP and a little premature, but I'd like to reuse this framework to create more Games in the future
|
10 |
class Player:
|
11 |
+
"""Base class for a player"""
|
12 |
|
13 |
+
def __init__(self,
|
14 |
+
name: str,
|
15 |
+
player_id: str,
|
16 |
+
interface: BaseAgentInterface,
|
17 |
+
message_level: str = "info"
|
18 |
+
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
self.name = name
|
20 |
+
"""The name of the player."""
|
21 |
self.id = player_id
|
22 |
+
"""The id of the player."""
|
23 |
self.interface = interface
|
24 |
+
"""The interface used by the agent controlling the player to communicate with the game."""
|
25 |
+
self.message_level = message_level
|
26 |
+
"""The level of messages that the player will receive. Can be "info", "verbose", or "debug"."""
|
27 |
+
|
28 |
+
def can_receive_message(self, message_type: MessageType) -> bool:
|
29 |
+
"""Returns True if the player can receive a message of the type."""
|
30 |
+
if message_type == "verbose" and self.message_level not in ["verbose", "debug"]:
|
31 |
+
return False
|
32 |
+
elif message_type == "debug" and self.message_level != "debug":
|
33 |
+
return False
|
34 |
+
else:
|
35 |
+
return True
|
36 |
+
|
37 |
+
@classmethod
|
38 |
+
def observer(
|
39 |
+
cls,
|
40 |
+
game_id: str,
|
41 |
+
message_level: str = "verbose",
|
42 |
+
interface_type: Type[BaseAgentInterface] = HumanAgentCLI,
|
43 |
+
log_messages: bool = False
|
44 |
+
):
|
45 |
+
"""Creates an observer player."""
|
46 |
+
name = "Observer"
|
47 |
+
player_id = f"{game_id}_observer"
|
48 |
+
interface = interface_type(player_id, log_messages)
|
49 |
+
|
50 |
+
return cls(name, player_id, interface, message_level)
|
51 |
+
|
52 |
+
|
53 |
+
class PlayerSubclass(Player):
|
54 |
+
@classmethod
|
55 |
+
def from_player(cls, player: Player):
|
56 |
+
"""Creates a new instance of the subclass from a player instance."""
|
57 |
+
return cls(player.name, player.id, player.interface, player.message_level)
|
58 |
+
|
59 |
+
|
60 |
+
class ChameleonPlayer(PlayerSubclass):
|
61 |
+
"""A player in the game Chameleon"""
|
62 |
+
|
63 |
+
def __init__(self, *args, **kwargs):
|
64 |
+
super().__init__(*args, **kwargs)
|
65 |
+
|
66 |
+
self.points: int = 0
|
67 |
+
"""The number of points the player has."""
|
68 |
+
self.roles: List[Role] = []
|
69 |
+
"""The role of the player in the game. Can be "chameleon" or "herd". This changes every round."""
|
70 |
|
71 |
def assign_role(self, role: Role):
|
72 |
+
self.roles.append(role)
|
73 |
+
|
74 |
+
@property
|
75 |
+
def role(self) -> Role:
|
76 |
+
"""The current role of the player."""
|
77 |
+
return self.roles[-1]
|
78 |
+
|
79 |
+
@property
|
80 |
+
def rounds_played_as_chameleon(self) -> int:
|
81 |
+
"""The number of times the player has been the Chameleon."""
|
82 |
+
return self.roles.count("chameleon")
|
83 |
+
|
84 |
+
@property
|
85 |
+
def rounds_played_as_herd(self) -> int:
|
86 |
+
"""The number of times the player has been in the Herd."""
|
87 |
+
return self.roles.count("herd")
|