-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathplayers.py
387 lines (323 loc) · 12.7 KB
/
players.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
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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
from typing import Generator, Any, Optional
from deck import Deck
from card import Card
from random import randint
class PlayNotAllowedError(Exception):
def __init__(self, message: str) -> None:
super().__init__(message)
class HumanPlayer:
def __init__(self) -> None:
"""
Class representing human player with a deck of cards rank and makao status
"""
self._hand: list[Card] = []
self.finished: bool = False
self._makao_status: bool = False
self._cards_played: int = 0
self._drew_card: bool = False
self._drew_penalty: bool = False
self._skip_turns: int = 0
self._penalty: bool = False
self._played_king: bool = False
@property
def makao_status(self) -> bool:
return self._makao_status
@property
def finished(self) -> bool:
return self._finished
@finished.setter
def finished(self, finished: bool) -> None:
self._finished = finished
@property
def drew_card(self) -> bool:
"""
Returns True if player has drawn penalty
"""
return self._drew_card
@property
def drew_penalty(self) -> bool:
"""
Returns True if player has played at least one car
"""
return self._drew_penalty
@property
def cards_played(self) -> int:
"""
Returns current amount of cards played in one turn
"""
return self._cards_played
def reset_cards_played(self) -> None:
self._cards_played = 0
@property
def penalty(self) -> bool:
"""
Returns True if player is currently drawing penalty
"""
return self._penalty
@penalty.setter
def penalty(self, new_penalty: bool) -> None:
self._penalty = new_penalty
@property
def played_king(self) -> bool:
return self._played_king
@played_king.setter
def played_king(self, new_val: bool) -> None:
self._played_king = new_val
def reset_turn_status(self):
self._drew_card = False
self._cards_played = 0
self._drew_penalty = False
def check_moved(self) -> bool:
return self.drew_card or bool(self.cards_played) or self.drew_penalty
@property
def skip_turns(self) -> int:
return self._skip_turns
@skip_turns.setter
def skip_turns(self, new_val) -> None:
self._skip_turns = new_val
@property
def hand(self) -> list[Card]:
return self._hand
def draw_card(self, deck: Deck) -> None:
if self.penalty:
self._drew_penalty = True
self._hand.append(deck.deal())
elif not self.drew_card and not self.cards_played:
self._hand.append(deck.deal())
self._drew_card = True
def play_card(self, card: Card) -> None:
if (
self.drew_card and card is self.hand[-1] and not self.cards_played
) or not self.drew_card:
self.hand.remove(card)
self._cards_played += 1
else:
raise PlayNotAllowedError("You cannot play this card")
def makao_set_reset(self, say: bool = False) -> None:
if say:
self._makao_status = True
return
self._makao_status = False
def player_info(self, human_computer: str = "Human player") -> tuple:
cards: str = " ".join([str(card) for card in self.hand])
return (
f"{human_computer}",
f" Deck: {cards}",
f" Makao: {self.makao_status}",
)
def __str__(self) -> str:
return " ".join([str(card) for card in self.hand])
class ComputerPlayer(HumanPlayer):
# TODO:
# - algorithm to choose best moves
def __init__(self) -> None:
"""
Class representing computer player with a deck of cards rank and makao status
"""
super().__init__()
def player_info(self, human_computer: str = "Computer player") -> tuple:
return super().player_info(human_computer)
def selection(self, items: list[str]) -> int:
if len(items) == 4:
return self.suit_select()
else:
return self.val_select(items)
def suit_select(self) -> int:
"""
Randomly selects a suit for the next player
:return: The index of the selected suit
"""
return randint(0, 3)
def val_select(self, items: list[str]) -> int:
"""
Calculates the value to select based on the player's hand
:param items: list of values
:return: Index of selected value or index of None in selection menu
"""
values: list[str] = items[:-1]
hand_values: list[str] = [card.value for card in self.hand]
num_of_occurrences: list[int] = [hand_values.count(value) for value in values]
return len(values) if not max(num_of_occurrences) else max(num_of_occurrences)
def find_best_plays(self, **game_state) -> list[Card]:
"""
Finds the best plays based on the given game parameters and player data
:key players: A list of players in the game
:key center: Current center card
:key prev_len: Length of the previous player's deck
:key next_len: Length of the next player's deck
:return: Moves for computer to play.
"""
self.previous_len: int = game_state.get("prev_len", 0)
self.next_len: int = game_state.get("next_len", 0)
possible_first_moves: list[Card] = self._get_possible_moves(**game_state)
possible_movesets: list[tuple[str, list[Card]]] = self._get_movesets(
possible_first_moves, **game_state
)
if len(possible_movesets) == 1 and possible_movesets[0][0] == "end":
return possible_movesets[0][1]
best_moves: list[Card] = (
[]
if not possible_movesets
else min(
possible_movesets,
key=lambda moveset: self.move_sort_key(moveset[0], len(moveset[1])),
)[1]
)
return best_moves
@staticmethod
def _get_move_descriptor(moveset: list[Card]) -> str:
"""
Get the move descriptor for a given card
:param card: The card for which to get the move descriptor
:return: Description of card's effect on the game
"""
descriptors: dict[str, str] = {
"2": "next_draw",
"3": "next_draw",
"4": "skip",
"jack": "value_req",
"ace": "suit_req",
}
king_descriptors: dict[str, str] = {
"spades": "king_prev_draw",
"hearts": "king_next_draw",
}
card_vals: list[str] = [card.value for card in moveset]
descriptor: Optional[str] = None
for i, val in enumerate(card_vals):
if val == "king":
descriptor = king_descriptors.get(moveset[i].suit, None)
descriptor = descriptors.get(val, None) if not descriptor else descriptor
if descriptor:
break
if not descriptor:
descriptor = "normal"
return descriptor
def _get_possible_moves(self, **game_params) -> list[Card]:
"""
Returns a list of possible first moves for the player
:key center_card: center card
:return: list of possible moves
"""
center_card: Card = game_params.get("center", None)
possible_moves: list[Card] = []
for card in self.hand:
if center_card.can_play(card, **game_params):
possible_moves.append(card)
return possible_moves
@staticmethod
def _simulate_params(first_move: Card, **game_params) -> dict[str, Any]:
"""
Simulates the parameters for a game based on the first move card
:param first_move: The first move card
:return: A dictionary containing the simulated game parameters
"""
new_params: dict[str, Any] = {}
val: str = first_move.value
suit: str = first_move.suit
if val in ["2", "3"]:
new_params.update({"penalty": int(first_move.value)})
elif val == "4":
new_params.update({"skip": 1})
elif val == "king" and suit in ["spades", "hearts"]:
new_params.update({"king": True})
elif val == "ace":
new_params.update({"ace": True})
elif val == "jack":
new_params.update({"jack": True})
if "king" in game_params:
new_params.update({"king": True})
if "value" in game_params:
new_params.update({"value": game_params["value"]})
if "ace" in game_params:
new_params.update({"ace": True})
if "jack" in game_params:
new_params.update({"jack": True})
return new_params
@staticmethod
def check_card_play_conditions(
current_card: Card, card: Card, moveset: list[Card], **game_params
) -> bool:
jack_ace_duplicate: bool = False
if "king" in game_params:
return False
if ("jack" in game_params or "ace" in game_params) and card.value in ["jack", "ace"]:
jack_ace_duplicate = True
return (
card not in moveset
and current_card.can_play(card, **game_params)
and not jack_ace_duplicate
)
def _generate_permutations(
self, moveset: list[Card] = [], **game_params
) -> Generator[list[Card], None, None]:
"""
Generates all possible permutations of movesets based on the current moveset and available cards.
:param moveset: List of already added moves, last item is the last played card
:param **game_params: Additional keyword arguments used to create simulated_params dict.
:return: A generator that yields possible movesets
"""
current_card: Card = moveset[-1]
params: dict[str, Any] = self._simulate_params(current_card, **game_params)
cards = list(
filter(lambda card: self.check_card_play_conditions(current_card, card, moveset, **params), self.hand)
)
if not cards or (
current_card.value == "king" and current_card.suit in ["spades", "hearts"]
) or len(moveset) == 7:
yield moveset
else:
for i, next_card in enumerate(cards):
yield from self._generate_permutations(moveset + [next_card], **params)
def _get_movesets(
self, first_moves: list[Card], **game_params
) -> list[tuple[str, list[Card]]]:
"""
Get the possible movesets based on the first moves and other optional arguments.
:param first_moves: The list of first moves player can make
:param **game_params: Optional keyword arguments passed to
_generate_permutations to simulate game_params
:return: The list of possible movesets, where each moveset is represented as a tuple
containing a descriptor string and a list of cards
"""
possible_movesets: list[tuple[str, list[Card]]] = []
for first_move in first_moves:
moves: list[Card] = [first_move]
for permutation in self._generate_permutations(moves, **game_params):
if len(permutation) <= 4 and len(permutation) == len(self.hand):
return [("end", permutation)]
moveset = permutation
if len(moveset) == len(self.hand) and len(moveset) > 4:
moveset = permutation[: len(moveset) - 1]
descriptor: str = self._get_move_descriptor(moveset)
possible_movesets.append((descriptor, moveset))
return possible_movesets
def move_sort_key(self, descriptor: str, moveset_len: int):
"""
Sorts the movesets based on their importance.
:param descriptor: The descriptor for the moveset.
:return: The importance of the moveset.
The smaller it is the more importan move is
"""
movesets_importance: dict[str, int] = {
"king_next_draw": 1,
"next_draw": 2,
"king_prev_draw": 3,
"value_req": 4,
"suit_req": 4,
"skip": 4,
"normal": 5,
}
if self.previous_len < self.next_len:
(
movesets_importance["king_next_draw"],
movesets_importance["king_prev_draw"],
) = (
movesets_importance["king_prev_draw"],
movesets_importance["king_next_draw"],
)
if self.previous_len <= len(self.hand):
movesets_importance["skip"] = movesets_importance["next_draw"]
moveset_len = moveset_len if moveset_len > 1 else 0
movesets_importance[descriptor] -= moveset_len
return movesets_importance[descriptor]