Eric Botti commited on
Commit
81e1c72
·
1 Parent(s): 5c924a1

streamlit app version of game

Browse files
{src/.streamlit → .streamlit}/config.toml RENAMED
@@ -1,3 +1,6 @@
1
  [theme]
2
  base="dark"
3
  font="monospace"
 
 
 
 
1
  [theme]
2
  base="dark"
3
  font="monospace"
4
+
5
+ [runner]
6
+ magicEnabled=false
src/agent_interfaces.py CHANGED
@@ -26,64 +26,100 @@ class BaseAgentInterface:
26
  self.id = agent_id
27
  self.messages = []
28
 
 
 
 
 
29
  def add_message(self, message: Message):
30
  """Adds a message to the message history, without generating a response."""
31
  bound_message = AgentMessage.from_message(message, self.id, len(self.messages))
32
  save(bound_message)
33
  self.messages.append(bound_message)
34
 
 
 
35
  def respond_to(self, message: Message) -> Message:
36
- """Adds a message to the message history, and generates a response message."""
37
  self.add_message(message)
38
- response = Message(type="agent", content=self._generate_response())
39
- self.add_message(response)
40
  return response
41
 
42
  def respond_to_formatted(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  self,
44
- message: Message,
45
  output_format: Type[OutputFormatModel],
 
46
  max_retries=3,
47
- **kwargs
48
  ) -> OutputFormatModel:
49
- """Adds a message to the message history, and generates a response matching the provided format."""
50
- initial_response = self.respond_to(message)
51
 
52
  reformat_message = Message(type="format", content=output_format.get_format_instructions())
53
 
54
  output = None
55
  retries = 0
56
 
57
- while not output and retries < max_retries:
58
  try:
59
  formatted_response = self.respond_to(reformat_message)
60
- if kwargs:
61
- fields = json.loads(formatted_response.content)
62
- fields.update(kwargs)
63
- output = output_format.model_validate(fields)
64
- else:
65
- output = output_format.model_validate_json(formatted_response.content)
66
-
67
- except ValidationError or JSONDecodeError as e:
 
68
  if retries > max_retries:
69
  raise e
 
70
  retry_message = Message(type="retry", content=f"Error formatting response: {e} \n\n Please try again.")
71
  reformat_message = retry_message
72
 
73
  retries += 1
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  return output
76
 
77
- def _generate_response(self) -> str:
 
 
78
  """Generates a response from the Agent."""
79
  # This is the BaseAgent class, and thus has no response logic
80
  # Subclasses should implement this method to generate a response using the message history
81
  raise NotImplementedError
82
 
83
- @property
84
- def is_ai(self):
85
- return not self.is_human
86
-
87
 
88
  AgentInterface = NewType("AgentInterface", BaseAgentInterface)
89
 
@@ -96,7 +132,7 @@ class OpenAIAgentInterface(BaseAgentInterface):
96
  self.model_name = model_name
97
  self.client = OpenAI()
98
 
99
- def _generate_response(self) -> str:
100
  """Generates a response using the message history"""
101
  open_ai_messages = [message.to_openai() for message in self.messages]
102
 
@@ -111,13 +147,18 @@ class OpenAIAgentInterface(BaseAgentInterface):
111
  class HumanAgentInterface(BaseAgentInterface):
112
  is_human = True
113
 
114
- def respond_to_formatted(self, message: Message, output_format: Type[OutputFormatModel], **kwargs) -> OutputFormatModel:
 
 
 
 
 
115
  """For Human agents, we can trust them enough to format their own responses... for now"""
116
- response = super().respond_to(message)
117
  # only works because current outputs have only 1 field...
118
- fields = {output_format.model_fields.copy().popitem()[0], response.content}
119
- if kwargs:
120
- fields.update(kwargs)
121
  output = output_format.model_validate(fields)
122
 
123
  return output
@@ -139,7 +180,7 @@ class HumanAgentCLI(HumanAgentInterface):
139
  # Prevents the agent from seeing its own messages on the command line
140
  print(message.content)
141
 
142
- def _generate_response(self) -> str:
143
  """Generates a response using the message history"""
144
  response = input()
145
  return response
 
26
  self.id = agent_id
27
  self.messages = []
28
 
29
+ @property
30
+ def is_ai(self):
31
+ return not self.is_human
32
+
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
40
+
41
  def respond_to(self, message: Message) -> Message:
42
+ """Take a message as input and return a response. Both the message and the response are added to history."""
43
  self.add_message(message)
44
+ response = self.generate_response()
 
45
  return response
46
 
47
  def respond_to_formatted(
48
+ self, message: Message,
49
+ output_format: Type[OutputFormatModel],
50
+ additional_fields: dict = None,
51
+ **kwargs
52
+ ) -> OutputFormatModel:
53
+ """Responds to a message and logs the response."""
54
+ self.add_message(message)
55
+ output = self.generate_formatted_response(output_format, additional_fields, **kwargs)
56
+ return output
57
+
58
+ # Generate response methods - These do not take a message as input and only use the current message history
59
+
60
+ def generate_response(self) -> Message:
61
+ """Generates a response based on the current messages in the history."""
62
+ response = Message(type="agent", content=self._generate())
63
+ self.add_message(response)
64
+ return response
65
+
66
+ def generate_formatted_response(
67
  self,
 
68
  output_format: Type[OutputFormatModel],
69
+ additional_fields: dict = None,
70
  max_retries=3,
 
71
  ) -> OutputFormatModel:
72
+ """Generates a response matching the provided format."""
73
+ initial_response = self.generate_response()
74
 
75
  reformat_message = Message(type="format", content=output_format.get_format_instructions())
76
 
77
  output = None
78
  retries = 0
79
 
80
+ while not output:
81
  try:
82
  formatted_response = self.respond_to(reformat_message)
83
+
84
+ fields = json.loads(formatted_response.content)
85
+ if additional_fields:
86
+ fields.update(additional_fields)
87
+
88
+ output = output_format.model_validate(fields)
89
+
90
+ except ValidationError as e:
91
+ # If the response doesn't match the format, we ask the agent to try again
92
  if retries > max_retries:
93
  raise e
94
+
95
  retry_message = Message(type="retry", content=f"Error formatting response: {e} \n\n Please try again.")
96
  reformat_message = retry_message
97
 
98
  retries += 1
99
 
100
+ except JSONDecodeError as e:
101
+ # Occasionally models will output json as a code block, which will cause a JSONDecodeError
102
+ if retries > max_retries:
103
+ raise e
104
+
105
+ retry_message = Message(type="retry",
106
+ content="There was an Error with your JSON format. Make sure you are not using code blocks."
107
+ "i.e. your response should be:\n{...}\n"
108
+ "Instead of:\n```json\n{...}\n```\n\n Please try again.")
109
+ reformat_message = retry_message
110
+
111
+ retries += 1
112
+
113
  return output
114
 
115
+ # How agents actually generate responses
116
+
117
+ def _generate(self) -> str:
118
  """Generates a response from the Agent."""
119
  # This is the BaseAgent class, and thus has no response logic
120
  # Subclasses should implement this method to generate a response using the message history
121
  raise NotImplementedError
122
 
 
 
 
 
123
 
124
  AgentInterface = NewType("AgentInterface", BaseAgentInterface)
125
 
 
132
  self.model_name = model_name
133
  self.client = OpenAI()
134
 
135
+ def _generate(self) -> str:
136
  """Generates a response using the message history"""
137
  open_ai_messages = [message.to_openai() for message in self.messages]
138
 
 
147
  class HumanAgentInterface(BaseAgentInterface):
148
  is_human = True
149
 
150
+ def generate_formatted_response(
151
+ self,
152
+ output_format: Type[OutputFormatModel],
153
+ additional_fields: dict = None,
154
+ max_retries: int = 3
155
+ ) -> OutputFormatModel:
156
  """For Human agents, we can trust them enough to format their own responses... for now"""
157
+ response = self.generate_response()
158
  # only works because current outputs have only 1 field...
159
+ fields = {output_format.model_fields.copy().popitem()[0]: response.content}
160
+ if additional_fields:
161
+ fields.update(additional_fields)
162
  output = output_format.model_validate(fields)
163
 
164
  return output
 
180
  # Prevents the agent from seeing its own messages on the command line
181
  print(message.content)
182
 
183
+ def _generate(self) -> str:
184
  """Generates a response using the message history"""
185
  response = input()
186
  return response
src/app.py CHANGED
@@ -1,63 +1,120 @@
1
- import asyncio
2
- from time import sleep
3
 
4
  import streamlit as st
5
- from langchain_core.messages import AIMessage
6
 
7
  from game import Game
 
 
 
8
 
9
  st.set_page_config(layout="wide", page_title="Chameleon", page_icon="img/logo.svg")
10
- human_turn = False
11
 
12
 
13
- def display_message(message):
14
- if message["type"] == "game":
15
- messages_container.markdown(f"{message['content']}")
16
- elif message["type"] == "verbose":
17
- messages_container.markdown(f":green[{message['content']}]")
18
- elif message["type"] == "debug":
19
- messages_container.markdown(f":orange[DEBUG: {message['content']}]")
20
 
21
 
22
- class StreamlitGame(Game):
23
- @staticmethod
24
- async def human_input(prompt: str) -> str:
25
- _user_input = st.chat_input("Your message", key=f"user_input_{st.session_state.user_input_id}")
26
- st.session_state.user_input_id += 1
27
-
28
- while _user_input is None or _user_input == "":
29
- sleep(0.1)
30
-
31
- print(f"User input: {_user_input}")
32
 
33
- response = AIMessage(content=_user_input)
34
 
35
- return response
36
-
37
- def human_message(self, message: str):
38
- message = {"type": "game", "content": message}
39
- st.session_state["messages"].append(message)
40
  display_message(message)
41
 
42
- def verbose_message(self, message: str):
43
- if self.verbose:
44
- message = {"type": "verbose", "content": message}
45
- st.session_state["messages"].append(message)
46
- display_message(message)
47
 
48
- def debug_message(self, message: str):
49
- if self.debug:
50
- message = {"type": "debug", "content": message}
51
- st.session_state["messages"].append(message)
52
- display_message(message)
53
 
54
-
55
- if "messages" not in st.session_state:
56
- st.session_state["messages"] = []
57
- if "game_started" not in st.session_state:
58
- st.session_state["game_started"] = False
59
- if "user_input_id" not in st.session_state:
60
- st.session_state["user_input_id"] = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  margin_size = 1
63
  center_size = 3
@@ -76,18 +133,16 @@ with center:
76
 
77
  messages_container.write("Enter your name to begin...")
78
 
 
 
79
  if st.session_state.messages:
80
  for message in st.session_state.messages:
81
  display_message(message)
82
 
83
- user_input = st.chat_input("Your message")
84
- st.session_state.user_input_id += 1
85
-
86
- if not st.session_state.game_started and user_input:
87
- st.session_state.game_started = True
88
  if "game" not in st.session_state:
89
- st.session_state.game = StreamlitGame(human_name=user_input, verbose=True)
90
-
91
- asyncio.run(st.session_state.game.start())
92
 
93
 
 
1
+ from typing import Type
 
2
 
3
  import streamlit as st
4
+ from streamlit import session_state
5
 
6
  from game import Game
7
+ from agent_interfaces import HumanAgentInterface
8
+ from message import Message
9
+ from prompts import fetch_prompt, format_prompt
10
 
11
  st.set_page_config(layout="wide", page_title="Chameleon", page_icon="img/logo.svg")
 
12
 
13
 
14
+ def display_message(message: Message):
15
+ if message.type == "verbose":
16
+ messages_container.markdown(f":green[{message.content}]")
17
+ elif message.type == "debug":
18
+ messages_container.markdown(f":orange[DEBUG: {message.content}]")
19
+ else:
20
+ messages_container.markdown(f"{message.content}")
21
 
22
 
23
+ if "messages" not in session_state:
24
+ session_state.messages = []
25
+ session_state.awaiting_human_input = False
26
+ session_state.game_state = "game_start"
 
 
 
 
 
 
27
 
 
28
 
29
+ class StreamlitInterface(HumanAgentInterface):
30
+ def add_message(self, message: Message):
31
+ super().add_message(message)
32
+ session_state.messages.append(message)
 
33
  display_message(message)
34
 
35
+ def _generate(self) -> str:
36
+ return session_state.user_input
 
 
 
37
 
 
 
 
 
 
38
 
39
+ class StreamlitGame(Game):
40
+ """A Streamlit version of the Game class that uses a state machine to manage the game state."""
41
+
42
+ def run_game(self):
43
+ """Starts the game."""
44
+ if session_state.game_state == "game_start":
45
+ self.game_message(fetch_prompt("game_rules"))
46
+ session_state.game_state = "setup_round"
47
+
48
+ if session_state.game_state == "setup_round":
49
+ self.setup_round()
50
+ session_state.game_state = "animal_description"
51
+ if session_state.game_state in ["animal_description", "chameleon_guess", "herd_vote"]:
52
+ self.run_round()
53
+ if session_state.game_state == "resolve_round":
54
+ self.resolve_round()
55
+ session_state.game_state = "setup_round"
56
+
57
+ def run_round(self):
58
+ """Starts the round."""
59
+
60
+ # Phase I: Collect Player Animal Descriptions
61
+ if session_state.game_state == "animal_description":
62
+ for current_player in self.players:
63
+ if current_player.id not in [animal_description['player_id'] for animal_description in self.round_animal_descriptions]:
64
+ if current_player.interface.is_human:
65
+ if not session_state.awaiting_human_input:
66
+ self.game_message(fetch_prompt("player_describe_animal"), current_player)
67
+ session_state.awaiting_human_input = True
68
+ break
69
+ else:
70
+ self.player_turn_animal_description(current_player)
71
+ session_state.awaiting_human_input = False
72
+ else:
73
+ self.game_message(fetch_prompt("player_describe_animal"), current_player)
74
+ self.player_turn_animal_description(current_player)
75
+ if len(self.round_animal_descriptions) == len(self.players):
76
+ session_state.game_state = "chameleon_guess"
77
+ session_state.awaiting_human_input = False
78
+
79
+ # Phase II: Chameleon Guesses the Animal
80
+ if session_state.game_state == "chameleon_guess":
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:
88
+ self.player_turn_chameleon_guess(self.chameleon)
89
+ session_state.awaiting_human_input = False
90
+ else:
91
+ self.player_turn_chameleon_guess(self.chameleon)
92
+ session_state.awaiting_human_input = False
93
+
94
+ session_state.game_state = "herd_vote"
95
+
96
+ # Phase III: The Herd Votes for who they think the Chameleon is
97
+ if session_state.game_state == "herd_vote":
98
+ for current_player in self.players:
99
+ if current_player.role == "herd" and current_player.id not in [vote['voter_id'] for vote in self.herd_vote_tally]:
100
+ player_responses = self.format_animal_descriptions(exclude=current_player)
101
+ if current_player.interface.is_human:
102
+ if not session_state.awaiting_human_input:
103
+ self.game_message(format_prompt("vote", player_responses=player_responses), current_player)
104
+ session_state.awaiting_human_input = True
105
+ break
106
+ else:
107
+ self.player_turn_herd_vote(current_player)
108
+ session_state.awaiting_human_input = False
109
+ else:
110
+ self.game_message(format_prompt("vote", player_responses=player_responses), current_player)
111
+ self.player_turn_herd_vote(current_player)
112
+
113
+ if len(self.herd_vote_tally) == len(self.players) - 1:
114
+ session_state.game_state = "resolve_round"
115
+
116
+
117
+ # Streamlit App
118
 
119
  margin_size = 1
120
  center_size = 3
 
133
 
134
  messages_container.write("Enter your name to begin...")
135
 
136
+ user_input = st.chat_input("Your response:")
137
+
138
  if st.session_state.messages:
139
  for message in st.session_state.messages:
140
  display_message(message)
141
 
142
+ if user_input:
 
 
 
 
143
  if "game" not in st.session_state:
144
+ st.session_state.game = StreamlitGame(human_name=user_input, verbose=True, human_interface=StreamlitInterface)
145
+ session_state.user_input = user_input
146
+ st.session_state.game.run_game()
147
 
148
 
src/game.py CHANGED
@@ -19,18 +19,8 @@ WINNING_SCORE = 3
19
  class Game:
20
  """The main game class, handles the game logic and player interactions."""
21
 
22
- number_of_players = NUMBER_OF_PLAYERS
23
- """The number of players in the game."""
24
  winning_score = WINNING_SCORE
25
  """The Number of points required to win the game."""
26
- chameleon_ids: List[str] = []
27
- """Record of which player was the chameleon for each round."""
28
- herd_animals: List[str] = []
29
- """Record of what animal was the herd animal for each round."""
30
- chameleon_guesses: List[str] = []
31
- """Record of what animal the chameleon guessed for each round."""
32
- herd_vote_tallies: List[List[dict]] = []
33
- """Record of the votes of each herd member for the chameleon for each round."""
34
 
35
  def __init__(
36
  self,
@@ -40,9 +30,27 @@ class Game:
40
  verbose: bool = False,
41
  debug: bool = False
42
  ):
43
- # Game ID
44
  self.game_id = game_id()
45
- self.start_time = datetime.now().strftime('%y%m%d-%H%M%S')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  # Gather Player Names
48
  if human_name:
@@ -67,14 +75,9 @@ class Game:
67
 
68
  self.players.append(Player(name, player_id, interface))
69
 
70
- self.verbose = verbose
71
- """If True, the game will display verbose messages to the player."""
72
- self.debug = debug
73
- """If True, the game will display debug messages to the player."""
74
-
75
  # Add Observer - an Agent who can see all the messages, but doesn't actually play
76
- if (self.verbose or self.debug) and not self.human_index:
77
- self.observer = HumanAgentCLI("{self.game_id}-observer")
78
  else:
79
  self.observer = None
80
 
@@ -96,6 +99,11 @@ class Game:
96
  """Returns the current herd animal."""
97
  return self.herd_animals[-1]
98
 
 
 
 
 
 
99
  @property
100
  def chameleon_guess(self) -> str:
101
  """Returns the current chameleon guess."""
@@ -106,6 +114,22 @@ class Game:
106
  """Returns the current herd vote tally."""
107
  return self.herd_vote_tallies[-1]
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  def observer_message(self, message: Message):
110
  """Sends a message to the observer if there is one."""
111
  if self.observer:
@@ -144,22 +168,10 @@ class Game:
144
  self.observer_message(message)
145
 
146
 
147
- async def start(self):
148
  """Sets up the game. This includes assigning roles and gathering player names."""
149
  self.game_message(fetch_prompt("game_rules"))
150
 
151
- winner = None
152
- round_number = 0
153
-
154
- # while not winner:
155
- # round_results = await self.run_round()
156
- # round_number += 1
157
-
158
- # # Check for a Winner
159
- # for player in self.players:
160
- # if player.points >= self.winning_score:
161
- # winner = player # ignoring the possibility of a tie for now
162
-
163
  self.setup_round()
164
 
165
  self.run_round()
@@ -194,29 +206,34 @@ class Game:
194
  player.assign_role("herd")
195
  self.game_message(format_prompt("assign_herd", herd_animal=herd_animal), player)
196
 
 
 
 
 
 
 
 
 
197
  def run_round(self):
198
  """Starts the round."""
199
  # Phase I: Collect Player Animal Descriptions
200
-
201
- self.game_message(f"Each player will now take turns describing themselves:")
202
- for i, current_player in enumerate(self.players):
203
  self.player_turn_animal_description(current_player)
204
 
205
  # Phase II: Chameleon Guesses the Animal
206
-
 
 
207
  self.player_turn_chameleon_guess(self.chameleon)
208
 
209
  # Phase III: The Herd Votes for who they think the Chameleon is
210
- self.game_message("The Chameleon has guessed the animal. Now the Herd will vote on who they think the chameleon is.")
211
-
212
- self.herd_vote_tallies.append([])
213
  for current_player in self.players:
214
  if current_player.role == "herd":
 
 
215
  self.player_turn_herd_vote(current_player)
216
 
217
-
218
-
219
-
220
  def player_turn_animal_description(self, player: Player):
221
  """Handles a player's turn to describe themselves."""
222
  if player.interface.is_ai:
@@ -225,21 +242,21 @@ class Game:
225
  prompt = fetch_prompt("player_describe_animal")
226
 
227
  # Get Player Animal Description
228
- message = Message(type="prompt", content=prompt)
229
- response = player.interface.respond_to_formatted(message, AnimalDescriptionFormat)
 
230
 
231
  self.game_message(f"{player.name}: {response.description}", player, exclude=True)
232
 
233
  def player_turn_chameleon_guess(self, chameleon: Player):
234
  """Handles the Chameleon's turn to guess the secret animal."""
235
- self.game_message("All players have spoken. The Chameleon will now guess the secret animal...")
236
  if chameleon.interface.is_ai or self.observer:
237
  self.verbose_message("The Chameleon is thinking...")
238
 
239
- prompt = fetch_prompt("chameleon_guess_animal")
240
 
241
- message = Message(type="prompt", content=prompt)
242
- response = chameleon.interface.respond_to_formatted(message, ChameleonGuessFormat)
243
 
244
  self.chameleon_guesses.append(response.animal)
245
 
@@ -248,12 +265,9 @@ class Game:
248
  if player.interface.is_ai:
249
  self.verbose_message(f"{player.name} is thinking...")
250
 
251
- prompt = fetch_prompt("vote")
252
-
253
  # Get Player Vote
254
- message = Message(type="prompt", content=prompt)
255
- other_player_names = [p.name for p in self.players if p != player]
256
- response = player.interface.respond_to_formatted(message, HerdVoteFormat, player_names=other_player_names)
257
 
258
  if player.interface.is_ai:
259
  self.debug_message(f"{player.name} voted for {response.vote}")
@@ -261,14 +275,14 @@ class Game:
261
  voted_for_player = self.player_from_name(response.vote)
262
 
263
  # Add Vote to Player Votes
264
- self.herd_vote_tally.append({"voter": player.id, "voted_for": voted_for_player.id})
265
 
266
  def resolve_round(self):
267
  """Resolves the round, assigns points, and prints the results."""
268
  self.game_message("All players have voted!")
269
  for vote in self.herd_vote_tally:
270
- voter = self.player_from_id(vote["voter"])
271
- voted_for = self.player_from_id(vote["voted_for"])
272
  self.game_message(f"{voter.name} voted for {voted_for.name}")
273
 
274
  accused_player_id = count_chameleon_votes(self.herd_vote_tally)
@@ -296,8 +310,8 @@ class Game:
296
  player.points += 1
297
  # If a Herd player votes for the Chameleon = +1 Point to that player
298
  for vote in self.herd_vote_tally:
299
- if vote["voted_for"] == self.chameleon.id:
300
- self.player_from_id(vote['voter']).points += 1
301
 
302
  # If the Herd fails to accuse the Chameleon = +1 Point to the Chameleon
303
  if not accused_player_id or accused_player_id != self.chameleon.id:
 
19
  class Game:
20
  """The main game class, handles the game logic and player interactions."""
21
 
 
 
22
  winning_score = WINNING_SCORE
23
  """The Number of points required to win the game."""
 
 
 
 
 
 
 
 
24
 
25
  def __init__(
26
  self,
 
30
  verbose: bool = False,
31
  debug: bool = False
32
  ):
33
+ # Instance Variables
34
  self.game_id = game_id()
35
+ """The unique id of the game."""
36
+ self.start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
37
+ """The time the game was started."""
38
+ self.verbose = verbose
39
+ """If True, the game will display verbose messages to the player."""
40
+ self.debug = debug
41
+ """If True, the game will display debug messages to the player."""
42
+ self.chameleon_ids: List[str] = []
43
+ """Record of which player was the chameleon for each round."""
44
+ self.herd_animals: List[str] = []
45
+ """Record of what animal was the herd animal for each round."""
46
+ self.all_animal_descriptions: List[List[dict]] = []
47
+ """Record of the animal descriptions each player has given for each round."""
48
+ self.chameleon_guesses: List[str] = []
49
+ """Record of what animal the chameleon guessed for each round."""
50
+ self.herd_vote_tallies: List[List[dict]] = []
51
+ """Record of the votes of each herd member for the chameleon for each round."""
52
+ self.winner_id: str | None = None
53
+ """The id of the player who has won the game."""
54
 
55
  # Gather Player Names
56
  if human_name:
 
75
 
76
  self.players.append(Player(name, player_id, interface))
77
 
 
 
 
 
 
78
  # Add Observer - an Agent who can see all the messages, but doesn't actually play
79
+ if (self.verbose or self.debug) and self.human_index is None:
80
+ self.observer = human_interface(f"{self.game_id}-observer")
81
  else:
82
  self.observer = None
83
 
 
99
  """Returns the current herd animal."""
100
  return self.herd_animals[-1]
101
 
102
+ @property
103
+ def round_animal_descriptions(self) -> List[dict]:
104
+ """Returns the current animal descriptions."""
105
+ return self.all_animal_descriptions[-1]
106
+
107
  @property
108
  def chameleon_guess(self) -> str:
109
  """Returns the current chameleon guess."""
 
114
  """Returns the current herd vote tally."""
115
  return self.herd_vote_tallies[-1]
116
 
117
+ @property
118
+ def round_number(self) -> int:
119
+ """Returns the current round number."""
120
+ return len(self.herd_animals)
121
+
122
+ def format_animal_descriptions(self, exclude: Player = None) -> str:
123
+ """Formats the animal description responses of the players into a single string."""
124
+ formatted_responses = ""
125
+ for response in self.round_animal_descriptions:
126
+ # Used to exclude the player who is currently responding, so they don't vote for themselves like a fool
127
+ if response["player_id"] != exclude.id:
128
+ player = self.player_from_id(response["player_id"])
129
+ formatted_responses += f" - {player.name}: {response['description']}\n"
130
+
131
+ return formatted_responses
132
+
133
  def observer_message(self, message: Message):
134
  """Sends a message to the observer if there is one."""
135
  if self.observer:
 
168
  self.observer_message(message)
169
 
170
 
171
+ async def run_game(self):
172
  """Sets up the game. This includes assigning roles and gathering player names."""
173
  self.game_message(fetch_prompt("game_rules"))
174
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  self.setup_round()
176
 
177
  self.run_round()
 
206
  player.assign_role("herd")
207
  self.game_message(format_prompt("assign_herd", herd_animal=herd_animal), player)
208
 
209
+ # Empty Animal Descriptions
210
+ self.all_animal_descriptions.append([])
211
+
212
+ # Empty Tally for Votes
213
+ self.herd_vote_tallies.append([])
214
+
215
+ self.game_message(f"Each player will now take turns describing themselves:")
216
+
217
  def run_round(self):
218
  """Starts the round."""
219
  # Phase I: Collect Player Animal Descriptions
220
+ for current_player in self.players:
221
+ self.game_message(fetch_prompt("player_describe_animal"), current_player)
 
222
  self.player_turn_animal_description(current_player)
223
 
224
  # Phase II: Chameleon Guesses the Animal
225
+ self.game_message("All players have spoken. The Chameleon will now guess the secret animal...")
226
+ player_responses = self.format_animal_descriptions(exclude=self.chameleon)
227
+ self.game_message(format_prompt("chameleon_guess_animal", player_responses=player_responses), self.chameleon)
228
  self.player_turn_chameleon_guess(self.chameleon)
229
 
230
  # Phase III: The Herd Votes for who they think the Chameleon is
 
 
 
231
  for current_player in self.players:
232
  if current_player.role == "herd":
233
+ player_responses = self.format_animal_descriptions(exclude=current_player)
234
+ self.game_message(format_prompt("vote", player_responses=player_responses), current_player)
235
  self.player_turn_herd_vote(current_player)
236
 
 
 
 
237
  def player_turn_animal_description(self, player: Player):
238
  """Handles a player's turn to describe themselves."""
239
  if player.interface.is_ai:
 
242
  prompt = fetch_prompt("player_describe_animal")
243
 
244
  # Get Player Animal Description
245
+ response = player.interface.generate_formatted_response(AnimalDescriptionFormat)
246
+
247
+ self.round_animal_descriptions.append({"player_id": player.id, "description": response.description})
248
 
249
  self.game_message(f"{player.name}: {response.description}", player, exclude=True)
250
 
251
  def player_turn_chameleon_guess(self, chameleon: Player):
252
  """Handles the Chameleon's turn to guess the secret animal."""
253
+
254
  if chameleon.interface.is_ai or self.observer:
255
  self.verbose_message("The Chameleon is thinking...")
256
 
257
+ response = chameleon.interface.generate_formatted_response(ChameleonGuessFormat)
258
 
259
+ self.game_message("The Chameleon has guessed the animal. Now the Herd will vote on who they think the chameleon is.")
 
260
 
261
  self.chameleon_guesses.append(response.animal)
262
 
 
265
  if player.interface.is_ai:
266
  self.verbose_message(f"{player.name} is thinking...")
267
 
 
 
268
  # Get Player Vote
269
+ additional_fields = {"player_names": [p.name for p in self.players if p != player]}
270
+ response = player.interface.generate_formatted_response(HerdVoteFormat, additional_fields=additional_fields)
 
271
 
272
  if player.interface.is_ai:
273
  self.debug_message(f"{player.name} voted for {response.vote}")
 
275
  voted_for_player = self.player_from_name(response.vote)
276
 
277
  # Add Vote to Player Votes
278
+ self.herd_vote_tally.append({"voter_id": player.id, "voted_for_id": voted_for_player.id})
279
 
280
  def resolve_round(self):
281
  """Resolves the round, assigns points, and prints the results."""
282
  self.game_message("All players have voted!")
283
  for vote in self.herd_vote_tally:
284
+ voter = self.player_from_id(vote["voter_id"])
285
+ voted_for = self.player_from_id(vote["voted_for_id"])
286
  self.game_message(f"{voter.name} voted for {voted_for.name}")
287
 
288
  accused_player_id = count_chameleon_votes(self.herd_vote_tally)
 
310
  player.points += 1
311
  # If a Herd player votes for the Chameleon = +1 Point to that player
312
  for vote in self.herd_vote_tally:
313
+ if vote["voted_for_id"] == self.chameleon.id:
314
+ self.player_from_id(vote['voter_id']).points += 1
315
 
316
  # If the Herd fails to accuse the Chameleon = +1 Point to the Chameleon
317
  if not accused_player_id or accused_player_id != self.chameleon.id:
src/game_utils.py CHANGED
@@ -37,7 +37,7 @@ def random_index(number_of_players : int) -> int:
37
 
38
  def count_chameleon_votes(player_votes: list[dict]) -> str | None:
39
  """Counts the votes for each player."""
40
- votes = [vote['voted_for'] for vote in player_votes]
41
 
42
  freq = Counter(votes)
43
  most_voted_player, number_of_votes = freq.most_common()[0]
 
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]
src/main.py CHANGED
@@ -12,7 +12,7 @@ def main():
12
  else:
13
  game = Game(verbose=True)
14
 
15
- asyncio.run(game.start())
16
 
17
 
18
  if __name__ == "__main__":
 
12
  else:
13
  game = Game(verbose=True)
14
 
15
+ asyncio.run(game.run_game())
16
 
17
 
18
  if __name__ == "__main__":
src/output_formats.py CHANGED
@@ -28,12 +28,12 @@ class AnimalDescriptionFormat(OutputFormatModel):
28
  description: str = Field(description="A brief description of the animal")
29
  """A brief description of the animal"""
30
 
31
- @field_validator('description')
32
- @classmethod
33
- def check_starting_character(cls, v) -> str:
34
- if not v[0].upper() == 'I':
35
- raise ValueError("Description must begin with 'I'")
36
- return v
37
 
38
 
39
  class ChameleonGuessFormat(OutputFormatModel):
 
28
  description: str = Field(description="A brief description of the animal")
29
  """A brief description of the animal"""
30
 
31
+ # @field_validator('description')
32
+ # @classmethod
33
+ # def check_starting_character(cls, v) -> str:
34
+ # if not v[0].upper() == 'I':
35
+ # raise ValueError("Description must begin with 'I'")
36
+ # return v
37
 
38
 
39
  class ChameleonGuessFormat(OutputFormatModel):
src/prompts.py CHANGED
@@ -34,10 +34,16 @@ Your Response:"""
34
 
35
  _chameleon_guess_animal = """\
36
  What animal do you think the other players are pretending to be?
 
 
 
37
  """
38
 
39
  _vote_prompt = """\
40
  Now it is time to vote. Choose from the other players who you think the Chameleon is.
 
 
 
41
  """
42
 
43
  prompts = {
 
34
 
35
  _chameleon_guess_animal = """\
36
  What animal do you think the other players are pretending to be?
37
+ Player Responses:
38
+ {player_responses}
39
+ Your Guess:
40
  """
41
 
42
  _vote_prompt = """\
43
  Now it is time to vote. Choose from the other players who you think the Chameleon is.
44
+ Player Responses:
45
+ {player_responses}
46
+ Your Vote:
47
  """
48
 
49
  prompts = {