|
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 |
|
|
|
|
|
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 |
|
): |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
self.chameleon_index = random_index(number_of_players) |
|
|
|
|
|
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 |
|
|
|
|
|
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)) |
|
|
|
|
|
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: |
|
|
|
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, |
|
exclude: bool = False |
|
): |
|
"""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 |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
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()) |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
if chameleon_will_guess: |
|
|
|
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: |
|
|
|
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)) |
|
|
|
|
|
response = await self.instructional_message(prompt, player, VoteModel) |
|
|
|
|
|
|
|
|
|
player_votes.append(response.vote) |
|
|
|
self.game_message("All players have voted!") |
|
self.game_message(f"Votes: {player_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" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|