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)