File size: 10,529 Bytes
0c31321 f596e58 46ba8c8 975158e c2392fe 975158e 5de0b8a 485a836 975158e 3ea5035 5de0b8a 0c31321 dd5c856 0c31321 172af0f 0c31321 172af0f c2392fe 172af0f c2392fe 172af0f 3ea5035 c2392fe 0c31321 f596e58 0c31321 c2392fe 3ea5035 1a8a579 3ea5035 c2392fe 3ea5035 172af0f c2392fe 3ea5035 5de0b8a 3ea5035 f596e58 3ea5035 172af0f 0c31321 172af0f 3ea5035 975158e 3ea5035 f596e58 5de0b8a f596e58 3ea5035 c2392fe 46ba8c8 c2392fe 46ba8c8 c2392fe 46ba8c8 c2392fe 5de0b8a 975158e 5de0b8a c2392fe 5de0b8a ab29f8e 5de0b8a 46ba8c8 5de0b8a 46ba8c8 c2392fe 46ba8c8 5de0b8a 46ba8c8 5de0b8a 46ba8c8 dd5c856 46ba8c8 3ea5035 46ba8c8 c2392fe 46ba8c8 dd5c856 46ba8c8 dd5c856 46ba8c8 c2392fe 46ba8c8 dd5c856 46ba8c8 c2392fe 46ba8c8 878e472 46ba8c8 c2392fe 46ba8c8 c2392fe 46ba8c8 3ea5035 46ba8c8 975158e 46ba8c8 878e472 46ba8c8 878e472 46ba8c8 878e472 46ba8c8 3ea5035 46ba8c8 c2392fe 46ba8c8 975158e 3ea5035 0c31321 172af0f f596e58 172af0f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
import os
from datetime import datetime
from typing import Optional, Type
from colorama import Fore, Style
from game_utils import *
from models import *
from player import Player
from prompts import fetch_prompt, format_prompt
# Default Values
NUMBER_OF_PLAYERS = 5
class Game:
log_dir = os.path.join(os.pardir, "experiments")
player_log_file = "{player_id}.jsonl"
game_log_file = "{game_id}-game.jsonl"
def __init__(
self,
number_of_players: int = NUMBER_OF_PLAYERS,
human_name: str = None,
verbose: bool = False # If there is a human player game will always be verbose
):
# This function is used to broadcast messages to the human player.
# They are purely informative and do not affect the game.
# Game ID
self.game_id = game_id()
self.start_time = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
self.log_dir = os.path.join(self.log_dir, f"{self.start_time}-{self.game_id}")
os.makedirs(self.log_dir, exist_ok=True)
# Choose Chameleon
self.chameleon_index = random_index(number_of_players)
# Gather Player Names
if human_name:
ai_names = random_names(number_of_players - 1, human_name)
self.human_index = random_index(number_of_players)
self.verbose = True
else:
ai_names = random_names(number_of_players)
self.human_index = None
self.verbose = verbose
# Add Players
self.players = []
for i in range(0, number_of_players):
if self.human_index == i:
name = human_name
controller = "human"
else:
name = ai_names.pop()
controller = "openai"
if self.chameleon_index == i:
role = "chameleon"
else:
role = "herd"
player_id = f"{self.game_id}-{i + 1}-{role}"
log_path = os.path.join(
self.log_dir,
self.player_log_file.format(player_id=player_id)
)
self.players.append(Player(name, controller, role, player_id, log_filepath=log_path))
# Game State
self.player_responses = []
def format_responses(self, exclude: str = None) -> str:
"""Formats the responses of the players into a single string."""
if len(self.player_responses) == 0:
return "None, you are the first player!"
else:
formatted_responses = ""
for response in self.player_responses:
# Used to exclude the player who is currently responding, so they don't vote for themselves like a fool
if response["sender"] != exclude:
formatted_responses += f" - {response['sender']}: {response['response']}\n"
return formatted_responses
def game_message(
self, message: str,
recipient: Optional[Player] = None, # If None, message is broadcast to all players
exclude: bool = False # If True, the message is broadcast to all players except the chosen player
):
"""Sends a message to a player. No response is expected, however it will be included next time the player is prompted"""
if exclude or not recipient:
for player in self.players:
if player != recipient:
player.prompt_queue.append(message)
if player.controller_type == "human":
print(message)
else:
recipient.prompt_queue.append(message)
if recipient.controller_type == "human":
print(message)
@staticmethod
async def instructional_message(message: str, player: Player, output_format: Type[BaseModel]):
"""Sends a message to a specific player and gets their response."""
if player.controller_type == "human":
print(message)
response = await player.respond_to(message, output_format)
return response
# The following methods are used to broadcast messages to a human.
def verbose_message(self, message: str):
"""Sends a message for the human player to read. No response is expected."""
if self.verbose:
print(Fore.GREEN + message + Style.RESET_ALL)
# def debug_message(self, message: str):
# """Sends a message for a human observer. These messages contain secret information about the players such as their role."""
# if self.debug:
# print(Fore.YELLOW + message + Style.RESET_ALL)
# def game_setup(self):
# """Sets up the game. This includes assigning roles and gathering player names."""
# self.verbose_message("Setting up the game...")
#
# for i, player in enumerate(self.players):
# if player.controller_type != "human":
# self.verbose_message(f"Player {i + 1}: {player.name} - {player.role}")
async def start(self):
"""Starts the game."""
self.verbose_message(("Welcome to Chameleon! This is a social deduction game powered by LLMs."))
self.game_message(fetch_prompt("game_rules"))
self.player_responses = []
herd_animal = random_animal()
# Phase I: Collect Player Animal Descriptions
self.game_message(f"Each player will now take turns describing themselves.")
for current_player in self.players:
if current_player.controller_type != "human":
self.verbose_message(f"{current_player.name} is thinking...")
if current_player.role == "chameleon":
prompt = format_prompt("chameleon_animal", player_responses=self.format_responses())
else:
prompt = format_prompt("herd_animal", animal=herd_animal, player_responses=self.format_responses())
# Get Player Animal Description
response = await self.instructional_message(prompt, current_player, AnimalDescriptionModel)
self.player_responses.append({"sender": current_player.name, "response": response.description})
self.game_message(f"{current_player.name}: {response.description}", current_player, exclude=True)
# Phase II: Chameleon Decides if they want to guess the animal (secretly)
self.game_message("All players have spoken. Now the chameleon will decide if they want to guess the animal or not.")
if self.human_index != self.chameleon_index:
self.verbose_message("The chameleon is thinking...")
chameleon = self.players[self.chameleon_index]
prompt = format_prompt("chameleon_guess_decision", player_responses=self.format_responses(exclude=chameleon.name))
response = await self.instructional_message(prompt, chameleon, ChameleonGuessDecisionModel)
if response.decision.lower() == "guess":
chameleon_will_guess = True
else:
chameleon_will_guess = False
# Phase III: Chameleon Guesses Animal or All Players Vote for Chameleon
if chameleon_will_guess:
# Chameleon Guesses Animal
self.game_message(f"{chameleon.name} has revealed themselves to be the chameleon and is guessing the animal...", chameleon, exclude=True)
prompt = fetch_prompt("chameleon_guess_animal")
response = await self.instructional_message(prompt, chameleon, ChameleonGuessAnimalModel)
self.game_message(f"The Chameleon guesses you are pretending to be a {response.animal}", chameleon, exclude=True)
if response.animal.lower() == herd_animal.lower():
self.game_message(f"The Chameleon has guessed the correct animal! The Chameleon wins!")
winner = "chameleon"
else:
self.game_message(f"The Chameleon is incorrect, the true animal is a {herd_animal}. The Herd wins!")
winner = "herd"
else:
# All Players Vote for Chameleon
print("vote time")
self.game_message("The chameleon has decided not to guess the animal. Now all players will vote on who they think the chameleon is.")
player_votes = []
for player in self.players:
if player.controller_type != "human":
self.verbose_message(f"{player.name} is thinking...")
prompt = format_prompt("vote", player_responses=self.format_responses(exclude=player.name))
# Get Player Vote
response = await self.instructional_message(prompt, player, VoteModel)
# check if a valid player was voted for...
# Add Vote to Player Votes
player_votes.append(response.vote)
self.game_message("All players have voted!")
self.game_message(f"Votes: {player_votes}")
# Count Votes
accused_player = count_chameleon_votes(player_votes)
if accused_player:
self.game_message(f"The Herd has accused {accused_player} of being the Chameleon!")
if accused_player == self.players[self.chameleon_index].name:
self.game_message(f"{accused_player} is the Chameleon! The Herd wins!")
winner = "herd"
else:
self.game_message(f"{accused_player} is not the Chameleon! The Chameleon wins!")
self.game_message(f"The real Chameleon was {chameleon.name}.")
winner = "chameleon"
else:
self.game_message("The Herd could not come to a consensus. The Chameleon wins!")
winner = "chameleon"
# Assign Points
# Chameleon Wins - 3 Points
# Herd Wins by Failed Chameleon Guess - 1 Point (each)
# Herd Wins by Correctly Guessing Chameleon - 2 points (each)
# Log Game Info
game_log = {
"game_id": self.game_id,
"start_time": self.start_time,
"herd_animal": herd_animal,
"number_of_players": len(self.players),
"human_player": self.players[self.human_index].id if self.human_index else "None",
"chameleon": self.players[self.chameleon_index].id,
"winner": winner
}
game_log_path = os.path.join(self.log_dir, self.game_log_file.format(game_id=self.game_id))
log(game_log, game_log_path)
|