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 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
- save(bound_message)
 
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 game import ChameleonGame
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.players[self.human_index].role == "chameleon":
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(human_name=user_input, verbose=True, human_interface=StreamlitInterface)
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 os
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 prompts import fetch_prompt, format_prompt
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
- class ChameleonGame(Game):
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
- number_of_players: int = NUMBER_OF_PLAYERS,
44
- human_name: str = None,
45
- human_interface: Type[HumanAgentInterface] = HumanAgentCLI,
46
- verbose: bool = False,
47
- debug: bool = False
48
  ):
49
- super().__init__(verbose, debug)
50
- # Instance Variables
51
- self.chameleon_ids: List[str] = []
52
- """Record of which player was the chameleon for each round."""
53
- self.herd_animals: List[str] = []
54
- """Record of what animal was the herd animal for each round."""
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
- # Add Observer - an Agent who can see all the messages, but doesn't actually play
86
- if (self.verbose or self.debug) and self.human_index is None:
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
- @property
100
- def chameleon(self) -> Player:
101
- """Returns the current chameleon."""
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, content: str,
 
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
- """Sends a message to a player. No response is expected, however it will be included next time the player is prompted"""
151
- message = Message(type="info", content=content)
 
 
 
 
 
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
- """Sends a message for the human player to read. No response is expected."""
163
- if self.verbose:
164
- message = Message(type="verbose", content=content)
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
- async def run_game(self):
179
- """Sets up the game. This includes assigning roles and gathering player names."""
180
- self.game_message(fetch_prompt("game_rules"))
 
181
 
182
- self.setup_round()
 
 
183
 
184
- self.run_round()
 
 
185
 
186
- self.resolve_round()
 
 
 
 
 
 
 
 
 
 
187
 
188
- # # Log Game Info
189
- # game_log = {
190
- # "game_id": self.game_id,
191
- # "start_time": self.start_time,
192
- # "number_of_players": len(self.players),
193
- # "human_player": self.players[self.human_index].id if self.human_index else "None",
194
- # }
195
 
196
- def setup_round(self):
197
- """Sets up the round. This includes assigning roles and gathering player names."""
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
- # Assign Roles
204
- chameleon_index = random_index(len(self.players))
205
- self.chameleon_ids.append(self.players[chameleon_index].id)
206
 
207
- for i, player in enumerate(self.players):
208
- if i == chameleon_index:
209
- player.assign_role("chameleon")
210
- self.game_message(fetch_prompt("assign_chameleon"), player)
211
- self.debug_message(f"{player.name} is the Chameleon!")
212
  else:
213
- player.assign_role("herd")
214
- self.game_message(format_prompt("assign_herd", herd_animal=herd_animal), player)
215
-
216
- # Empty Animal Descriptions
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
- # Point Logic
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
- # If the Chameleon guesses the incorrect animal = +1 Point to each Herd player
 
 
314
  else:
315
- for player in self.players:
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
- # If the Herd fails to accuse the Chameleon = +1 Point to the Chameleon
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 json
7
- from collections import Counter
8
 
 
9
  ALPHABET = string.ascii_lowercase + string.digits
10
  ID_LENGTH = 8
 
11
 
12
 
13
- def game_id():
14
- return ''.join(random.choices(ALPHABET, k=ID_LENGTH)) # Using this instead of uuid for shorter game ids
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 available_names:
27
- available_names.remove(human_name)
28
- return random.sample(available_names, number_of_samples)
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 log(log_object, log_file):
53
- with open(log_file, "a+") as f:
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 game import ChameleonGame
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
- if name:
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
- role: Role | None = None
15
- """The role of the player in the game. Can be "chameleon" or "herd". This changes every round."""
16
- rounds_played_as_chameleon: int = 0
17
- """The number of times the player has been the Chameleon."""
18
- rounds_played_as_herd: int = 0
19
- """The number of times the player has been in the Herd."""
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 = role
35
- if role == "chameleon":
36
- self.rounds_played_as_chameleon += 1
37
- elif role == "herd":
38
- self.rounds_played_as_herd += 1
 
 
 
 
 
 
 
 
 
 
 
 
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")