-
Notifications
You must be signed in to change notification settings - Fork 0
/
game.py
222 lines (185 loc) · 8.66 KB
/
game.py
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
from enum import Enum
import numpy as np
class Tile:
class Colors(Enum):
BLACK = 0
WHITE = 1
class Directions(Enum):
PRIVATE = 0
PUBLIC = 1
def __init__(self, color: Colors, number: int, direction=Directions.PRIVATE) -> None:
self.color = color
self.number = number
self.direction = direction
self.history_guesses = [] # List to store historical guesses
def __str__(self) -> str:
return f"Color: {self.color.name}, Number: {self.number}, Direction: {self.direction.name}"
def opponent_print(self) -> None:
if self.direction == self.Directions.PRIVATE:
return f"Color: {self.color.name}"
else:
return f"Color: {self.color.name}, Number: {self.number}"
def add_guess(self, guess: int) -> None:
self.history_guesses.append(guess)
class TableTileSet:
"""
This class is used to store the tiles that are on the table (not yet drawn by players)
Attributes:
max_tile_number (int): The maximum number a tile can have
tile_set (set[Tile]): The set of tiles on the table
Methods:
init_tile_set: Set the tile_set into two sets, one black and one white, with tile numbers ranging from 0 to MAX_TILE_NUMBER
"""
def __init__(self, max_tile_number: int) -> None:
assert max_tile_number and max_tile_number > 0, "Invalid max_tile_number"
self.max_tile_number = max_tile_number
self.tile_set = set()
def init_tile_set(self) -> None:
for color in Tile.Colors:
for number in range(1, self.max_tile_number + 1):
tile = Tile(color, number)
self.tile_set.add(tile)
def get_tile_list(self) -> list[set]:
return sorted(list(self.tile_set), key=lambda x: x.number * 2 + x.color.value)
def __str__(self) -> str:
table_tile_set_str = "".join([str(tile) + ",\n" for tile in self.tile_set])
return f"TableTileSet: \n({table_tile_set_str})"
class PlayerTileSet:
"""
This class is used to store the tiles that are owned by the player and perform actions to the tiles
Attributes:
max_tile_number (int): The maximum number a tile can have
np_random (np.random.Generator): The random number generator
tile_set (set[Tile]): The set of tiles owned by the player
temp_tile (Tile): The tile just drawn by the player and not yet placed into the tile set
Methods:
init_tile_set: Set the tile_set empty
get_tile_list: Get the sorted list of tiles
draw_tile: Draw a tile and set it the temp_tile if direct_draw = False (by default), draw a tile and put it directly into the tile_set if direct_draw = True
make_guess: Make a guess on one of the private tiles owned by other player(s)
verify_guess: Verify the guess made by other players
end_turn: The player decide to end their's turn actively
is_lose: Test if the player loses the game
"""
class InvalidActionErrorEnum(Enum):
TARGET_INDEX_OUT_OF_RANGE = 0
TARGET_INDEX_INVALID = 1
TILE_INDEX_OUT_OF_RANGE = 2
TILE_NUMBER_OUT_OF_RANGE = 3
TILE_ALREADY_PUBLIC = 4
def __init__(self, max_tile_number: int, np_random: np.random.Generator = None) -> None:
assert max_tile_number and max_tile_number > 0, "Invalid max_tile_number"
self.max_tile_number = max_tile_number
self.np_random = np_random if np_random else np.random.default_rng()
self.tile_set = set()
self.temp_tile = None
def init_tile_set(self) -> None:
self.tile_set.clear()
def get_tile_list(self) -> list[Tile]:
return sorted(list(self.tile_set), key=lambda x: x.number * 2 + x.color.value)
def draw_tile(self, table_tile_set, direct_draw=False) -> None:
if len(table_tile_set.tile_set) == 0:
raise ValueError("Empty table error")
else:
tile = self.np_random.choice(table_tile_set.get_tile_list())
if direct_draw:
self.tile_set.add(tile)
else:
self.temp_tile = tile
table_tile_set.tile_set.remove(tile)
def make_guess(
self, all_players: list, target_index: int, tile_index: int, tile_number: int
) -> bool:
if target_index >= len(all_players) or target_index < 0:
raise ValueError(self.InvalidActionErrorEnum.TARGET_INDEX_OUT_OF_RANGE)
if target_index == all_players.index(self) or all_players[target_index].is_lose():
raise ValueError(self.InvalidActionErrorEnum.TARGET_INDEX_INVALID)
if tile_number < 1 or tile_number > self.max_tile_number:
raise ValueError(self.InvalidActionErrorEnum.TILE_NUMBER_OUT_OF_RANGE)
guessTarget = all_players[target_index]
if tile_index < 0 or tile_index >= len(guessTarget.get_tile_list()):
raise ValueError(self.InvalidActionErrorEnum.TILE_INDEX_OUT_OF_RANGE)
elif guessTarget.get_tile_list()[tile_index].direction == Tile.Directions.PUBLIC:
raise ValueError(self.InvalidActionErrorEnum.TILE_ALREADY_PUBLIC)
elif guessTarget.verify_guess(tile_index, tile_number):
return True # right guess
else:
if self.temp_tile != None:
self.temp_tile.direction = Tile.Directions.PUBLIC
self.tile_set.add(self.temp_tile)
self.temp_tile = None
return False # wrong guess
def verify_guess(self, tile_index: int, tile_number: int) -> bool:
tile = self.get_tile_list()[tile_index]
if tile.direction == Tile.Directions.PUBLIC:
raise ValueError(self.InvalidActionErrorEnum.TILE_ALREADY_PUBLIC)
if tile.number == tile_number:
tile.direction = Tile.Directions.PUBLIC
return True
else:
tile.add_guess(tile_number) # Add the guess to the history
return False
def end_turn(self) -> None:
if self.temp_tile != None:
self.temp_tile.direction = Tile.Directions.PRIVATE
self.tile_set.add(self.temp_tile)
self.temp_tile = None
def is_lose(self) -> bool:
return not any(tile.direction == Tile.Directions.PRIVATE for tile in self.tile_set)
class GameHost:
"""
This class performs the game flow
Attributes:
initial_tiles (int): The number of tiles each player starts with
np_random (np.random.Generator): The random number generator
table_tile_set (TableTileSet): A set of tiles on the table
all_players (list[PlayerTileSet]): A list of player instances
Methods:
init_game: Initialize everything about the game
is_game_over: Test if the winner appears
show_self_status: Display the status of the player
show_opponent_status: Display the status of other players
guesses_making_stage: Allow the player to make guesses
start_game: Run the main game routine
"""
def __init__(
self,
numPlayer: int,
initial_tiles: int,
max_tile_number: int,
np_random: np.random.Generator = None,
) -> None:
assert initial_tiles and initial_tiles > 0, "Invalid initial_tiles"
self.initial_tiles = initial_tiles
self.np_random = np_random if np_random else np.random.default_rng()
self.table_tile_set = TableTileSet(max_tile_number)
self.all_players = [
PlayerTileSet(max_tile_number, self.np_random) for count in range(0, numPlayer)
] # Set number of players here
def init_game(self) -> None:
self.table_tile_set.init_tile_set()
for player in self.all_players:
player.init_tile_set()
for draw_count in range(0, self.initial_tiles):
for player in self.all_players:
player.draw_tile(self.table_tile_set, direct_draw=True)
def get_next_player_index(self, current_player: int | PlayerTileSet) -> int:
if isinstance(current_player, (int, np.integer)):
current_player_index = current_player
else:
current_player_index = self.all_players.index(current_player)
alive_player_indecies = [
player_index
for player_index, player in enumerate(self.all_players)
if not player.is_lose()
]
next_player_index = alive_player_indecies[
(alive_player_indecies.index(current_player_index) + 1) % len(alive_player_indecies)
]
return next_player_index
def is_game_over(self) -> bool:
last_players = list(player for player in self.all_players if player.is_lose() == False)
if len(last_players) <= 1:
return True
else:
return False