File size: 11,185 Bytes
0c31321
f596e58
46ba8c8
975158e
c2392fe
 
975158e
 
5de0b8a
485a836
975158e
3ea5035
7877562
 
0c31321
dd5c856
0c31321
172af0f
 
7877562
 
 
 
 
 
0c31321
172af0f
 
c2392fe
172af0f
7877562
172af0f
0c31321
 
f596e58
 
 
0c31321
c2392fe
 
 
3ea5035
 
1a8a579
3ea5035
 
 
172af0f
7877562
 
3ea5035
 
5de0b8a
3ea5035
 
 
 
 
 
f596e58
3ea5035
 
 
 
 
 
7877562
0c31321
172af0f
 
 
 
 
7877562
3ea5035
975158e
3ea5035
 
f596e58
5de0b8a
f596e58
 
 
 
 
 
 
 
 
 
3ea5035
c2392fe
46ba8c8
 
 
 
 
 
 
 
 
 
 
3ac8e2a
7877562
3ac8e2a
46ba8c8
 
 
3ac8e2a
c2392fe
c6c2b98
c2392fe
46ba8c8
c6c2b98
c2392fe
 
 
 
c6c2b98
 
 
 
 
 
c2392fe
 
 
 
 
7877562
 
 
 
5de0b8a
975158e
7877562
c2392fe
5de0b8a
7877562
 
c6c2b98
 
7877562
c6c2b98
 
 
7877562
c6c2b98
 
 
7877562
 
 
 
 
 
 
 
ab29f8e
c6c2b98
5de0b8a
7877562
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46ba8c8
 
5de0b8a
7877562
 
46ba8c8
7877562
 
c2392fe
46ba8c8
 
5de0b8a
46ba8c8
5de0b8a
46ba8c8
dd5c856
c6c2b98
dd5c856
c6c2b98
46ba8c8
7877562
3ea5035
c6c2b98
c2392fe
c6c2b98
dd5c856
c6c2b98
7877562
c6c2b98
 
dd5c856
c6c2b98
c2392fe
c6c2b98
 
 
 
 
dd5c856
c6c2b98
c2392fe
c6c2b98
 
c2392fe
c6c2b98
c2392fe
c6c2b98
 
 
 
3ea5035
975158e
c6c2b98
 
 
878e472
c6c2b98
 
878e472
c6c2b98
878e472
c6c2b98
 
 
 
3ea5035
c6c2b98
 
 
 
46ba8c8
c6c2b98
 
 
 
 
 
 
 
 
 
 
 
 
975158e
c6c2b98
 
 
3ea5035
c6c2b98
 
3ea5035
c6c2b98
 
 
 
172af0f
c6c2b98
 
 
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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
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 = 6
WINNING_SCORE = 11

class Game:
    log_dir = os.path.join(os.pardir, "experiments")
    player_log_file = "{player_id}.jsonl"
    game_log_file = "{game_id}-game.jsonl"
    number_of_players = NUMBER_OF_PLAYERS
    """The number of players in the game."""
    winning_score = WINNING_SCORE
    """The Number of points required to win the game."""
    debug = True
    """If True, the game will print debug messages to the console."""

    def __init__(
            self,
            number_of_players: int = NUMBER_OF_PLAYERS,
            human_name: str = None,
            verbose = False
    ):
        # 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)
        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}"

            log_path = os.path.join(
                self.log_dir,
                self.player_log_file.format(player_id=player_id)
            )

            self.players.append(Player(name, controller, 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":
                        self.human_message(message)
            if self.verbose:
                self.human_message(message)
        else:
            recipient.prompt_queue.append(message)
            if recipient.controller_type == "human":
                self.human_message(message)

    async def instructional_message(self, message: str, player: Player,  output_format: Type[BaseModel]):
        """Sends a message to a specific player and gets their response."""
        if player.controller_type == "human":
            self.human_message(message)
        response = await player.respond_to(message, output_format)
        return response

    # The following methods are used to broadcast messages to a human.
    # They are design so that they can be overridden by a subclass for a different player interface.
    @staticmethod
    def human_message(self, message: str):
        """Sends a message for the human player to read. No response is expected."""
        print(message)

    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 + "DEBUG: " + message + Style.RESET_ALL)

    async def start(self):
        """Sets up the game. This includes assigning roles and gathering player names."""
        self.game_message(fetch_prompt("game_rules"))

        await self.run_round()

        # Log Game Info
        game_log = {
            "game_id": self.game_id,
            "start_time": self.start_time,
            "number_of_players": len(self.players),
            "human_player": self.players[self.human_index].id if self.human_index else "None",
        }
        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)



    async def run_round(self):
        """Starts the round."""

        # Phase I: Choose Animal and Assign Roles

        herd_animal = random_animal()
        self.debug_message(f"The secret animal is {herd_animal}.")

        chameleon_index = random_index(len(self.players))
        chameleon = self.players[chameleon_index]

        for i, player in enumerate(self.players):
            if i == chameleon_index:
                player.assign_role("chameleon")
                self.game_message("You are the **Chameleon**, remain undetected and guess what animal the others are pretending to be", player)
                self.debug_message(f"{player.name} is the Chameleon!")
            else:
                player.assign_role("herd")
                self.game_message(f"You are a **{herd_animal}**, keep this secret at all costs and figure which player is not really a {herd_animal}", player)

        # Phase II: Collect Player Animal Descriptions

        self.game_message(f"Each player will now take turns describing themselves:")
        for i, current_player in enumerate(self.players):
            if current_player.controller_type != "human":
                self.verbose_message(f"{current_player.name} is thinking...")

            if i == 0:
                prompt = "Your Response:"
            else:
                prompt = "It's your turn to describe yourself. Do not repeat responses from other players.\nYour Response:"


            # 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 III: Chameleon Guesses the Animal

        self.game_message("All players have spoken. The Chameleon will now guess the secret animal...")
        if self.human_index != self.chameleon_index:
            self.verbose_message("The Chameleon is thinking...")

        prompt = fetch_prompt("chameleon_guess_animal")

        response = await self.instructional_message(prompt, chameleon, ChameleonGuessAnimalModel)

        chameleon_animal_guess = response.animal

        # Phase IV: The Herd Votes for who they think the Chameleon is
        self.game_message("The Chameleon has guessed the animal. Now the Herd will vote on who they think the chameleon is.")

        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.role == "herd":
                if player.is_ai():
                    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({"voter": player, "vote": response.vote})
                if player.is_ai():
                    self.debug_message(f"{player.name} voted for {response.vote}")


        self.game_message("All players have voted!")
        formatted_votes = '\n'.join([f'{vote["voter"].name}: {vote["vote"]}' for vote in player_votes])
        self.game_message(f"Votes:\n{formatted_votes}")

        # Count Votes
        accused_player = count_chameleon_votes(player_votes)

        # Phase V: Assign Points

        self.game_message(f"The round is over. Caclulating results...")
        self.game_message(
            f"The Chameleon was {chameleon.name}, and they guessed the secret animal was {chameleon_animal_guess}.")
        self.game_message(f"The secret animal was actually was {herd_animal}.")

        if accused_player:
            self.game_message(f"The Herd voted for {accused_player} as the Chameleon.")
        else:
            self.game_message(f"The Herd could not come to a consensus.")

        # Point Logic
        # If the Chameleon guesses the correct animal    =   +1 Point to the Chameleon
        if chameleon_animal_guess.lower() == herd_animal.lower():
            chameleon.points += 1
        # If the Chameleon guesses the incorrect animal  =   +1 Point to each Herd player
        else:
            for player in self.players:
                if player.role == "herd":
                    player.points += 1
        # If a Herd player votes for the Chameleon       =   +1 Point to that player
        for vote in player_votes:
            if vote["vote"] == chameleon.name:
                vote['voter'].points += 1

        # If the Herd fails to accuse the Chameleon      =   +1 Point to the Chameleon
        if not accused_player or accused_player != chameleon.name:
            chameleon.points += 1

        # Check for a Winner
        player_points = "\n".join([f"{player.name}: {player.points}" for player in self.players])

        self.game_message(f"Current Game Score: {player_points}")

        # Log Round Info
        round_log = {
            "herd_animal": herd_animal,
            "chameleon_name": self.players[self.chameleon_index].name,
            "chameleon_guess": chameleon_animal_guess,
            "herd_votes": player_votes,
        }