Coverage for app/pycardgame/src/presets.py: 100.0%
139 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-07 20:57 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-07 20:57 +0000
1# PyCardGame - A base library for creating card games in Python
2# Copyright (C) 2025 Popa-42
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <https://www.gnu.org/licenses/>.
17from __future__ import annotations
19from typing import List, Literal
21from .. import (
22 CardMeta,
23 DeckMeta,
24 GenericCard,
25 GenericDeck,
26 GenericGame,
27 GenericPlayer,
28)
30T_UnoRanks = Literal["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "Skip",
31 "Reverse", "Draw Two", "Wild", "Wild Draw Four"]
32T_UnoSuits = Literal["Red", "Green", "Blue", "Yellow", "Wild"]
35class UnoCard(
36 GenericCard[T_UnoRanks, T_UnoSuits],
37 metaclass=CardMeta,
38 rank_type=T_UnoRanks,
39 suit_type=T_UnoSuits
40):
41 __slots__ = ("wild",)
43 def __init__(self, rank, suit):
44 super().__init__(rank, suit, False) # type: ignore
46 self.wild = False
48 def is_wild(self):
49 return self.wild
51 def effect(self, game, player, *args): # pragma: no cover
52 pass
54 def __str__(self):
55 if self.get_rank() in ["Wild", "Wild Draw Four"]:
56 return f"{self.get_rank()}"
57 return f"{self.get_suit()} {self.get_rank()}"
60class NumberCard(UnoCard, metaclass=CardMeta, rank_type=T_UnoRanks,
61 suit_type=T_UnoSuits):
62 def __init__(self, rank, suit):
63 super().__init__(rank, suit)
64 self.wild = False
66 def effect(self, game, player, *args):
67 pass
70class DrawTwoCard(UnoCard, metaclass=CardMeta, rank_type=T_UnoRanks,
71 suit_type=T_UnoSuits):
72 def __init__(self, suit):
73 super().__init__("Draw Two", suit)
74 self.wild = False
76 def effect(self, game, player, *args):
77 if isinstance(game, UnoGame):
78 game.draw_count += 2 # Add 2 to the draw count
81class SkipCard(UnoCard, metaclass=CardMeta, rank_type=T_UnoRanks,
82 suit_type=T_UnoSuits):
83 def __init__(self, suit):
84 super().__init__("Skip", suit)
85 self.wild = False
87 def effect(self, game, player, *args):
88 game.next_player()
91class ReverseCard(UnoCard, metaclass=CardMeta, rank_type=T_UnoRanks,
92 suit_type=T_UnoSuits):
93 def __init__(self, suit):
94 super().__init__("Reverse", suit)
95 self.wild = False
97 def effect(self, game, player, *args):
98 game.reverse_direction()
101class WildCard(UnoCard, metaclass=CardMeta, rank_type=T_UnoRanks,
102 suit_type=T_UnoSuits):
103 def __init__(self):
104 super().__init__("Wild", "Wild")
105 self.wild = True
107 def effect(self, game, player, *args):
108 if args and args[0] is not None:
109 self.change_suit(args[0])
110 self.wild = False
111 else:
112 raise ValueError("A new suit must be provided for Wild card.")
113 game.discard_cards(self)
116class WildDrawFourCard(UnoCard, metaclass=CardMeta, rank_type=T_UnoRanks,
117 suit_type=T_UnoSuits):
118 def __init__(self):
119 super().__init__("Wild Draw Four", "Wild")
120 self.wild = True
122 def effect(self, game, player, *args):
123 if args and args[0] is not None:
124 self.change_suit(args[0])
125 self.wild = False
126 else:
127 raise ValueError(
128 "A new suit must be provided for Wild Draw Four card.")
129 game.draw_cards(game.get_next_player(), 4)
130 game.next_player()
133class UnoDeck(
134 GenericDeck[UnoCard],
135 metaclass=DeckMeta,
136 card_type=UnoCard
137):
138 def __init__(self, cards=None):
139 super().__init__()
141 colors: List[T_UnoSuits] = ["Red", "Green", "Blue", # type: ignore
142 "Yellow"]
143 numbers: List[T_UnoRanks] = (["0"] # type: ignore
144 + [str(i) for i in range(1, 10)] * 2)
146 # Create the deck with the specialized card types
147 card_list = [
148 # Create Number Cards (0-9)
149 NumberCard(rank, suit) for suit in colors for rank in numbers
150 ] + [
151 # Create DrawTwo Cards
152 DrawTwoCard(suit) for suit in colors for _ in range(2)
153 ] + [
154 # Create Skip Cards
155 SkipCard(suit) for suit in colors for _ in range(2)
156 ] + [
157 # Create Reverse Cards
158 ReverseCard(suit) for suit in colors for _ in range(2)
159 ] + [
160 # Add Wild Cards
161 WildCard() for _ in range(4)
162 ] + [
163 # Add Wild Draw Four Cards
164 WildDrawFourCard() for _ in range(4)
165 ]
167 self.cards = cards if cards is not None else card_list
169 def __str__(self):
170 return f"UNO Deck with {len(self.cards)} cards."
172 def __repr__(self):
173 return f"{self.__class__.__name__}(cards={self.cards!r})"
176class UnoPlayer(GenericPlayer[UnoCard]):
177 __slots__ = ("uno",)
179 def __init__(self, name, hand=None):
180 super().__init__(name, hand, 0)
181 self.uno = False
183 def call_uno(self):
184 if len(self.hand) == 1:
185 self.uno = True
186 return self.uno
188 def reset_uno(self):
189 self.uno = False
190 return self
192 def __repr__(self):
193 return (f"{self.__class__.__name__}({self.name!r}, "
194 f"hand={self.hand!r}, uno={self.uno!r})")
197class UnoGame(GenericGame[UnoCard]):
198 def __init__(self, *players, draw_pile=None, discard_pile=None,
199 hand_size=7):
200 super().__init__(UnoCard, UnoDeck, draw_pile, discard_pile, None,
201 hand_size, 0, True, *players)
202 self.draw_pile = draw_pile if draw_pile else UnoDeck().shuffle()
203 self.draw_count = 0 # Track accumulated draw count
204 self.game_ended = False
206 def check_valid_play(self, card1, card2=None):
207 if card2 is None:
208 card2 = self.get_top_card()
210 if card1 is None or card2 is None:
211 return False
213 # Only Draw Two cards can be stacked on top of each other
214 if self.draw_count > 0 and card1.get_rank() != "Draw Two":
215 return False
217 if card1.is_wild():
218 return True
219 return card1.rank == card2.rank or card1.suit == card2.suit
221 def get_next_player(self):
222 return self.players[
223 (self.current_player_index + self.direction) % len(self.players)]
225 def start_game(self):
226 self.deal_initial_cards()
227 self.discard_pile.add(self.draw_pile.draw())
228 return self
230 def draw_instead_of_play(self, player=None):
231 player = player or self.get_current_player()
233 if self.draw_count > 0:
234 drawn_cards = self.draw_cards(player, self.draw_count)
235 self.draw_count = 0
236 else:
237 drawn_cards = self.draw_cards(player, 1)
239 return drawn_cards
241 def determine_winner(self):
242 for player in self.players:
243 if len(player) == 0:
244 return player
245 return None
247 def end_game(self, export=None): # type: ignore
248 winner = self.determine_winner()
249 if winner is not None:
250 print(f"{winner.name} wins the game!")
251 else:
252 print("Game ended without a winner.")
254 self.game_ended = True
256 self.draw_pile.clear()
257 self.discard_pile.clear()
258 for player in self.players:
259 player.hand.clear()
261 self.players.clear()
263 # TODO: Export game statistics to a file or database
264 # self.export_statistics(path=export)
266 print("Game resources have been cleared and the game is now closed.")
268 return winner
270 def __str__(self):
271 direction = "Clockwise" if self.direction == 1 else "Counter-clockwise"
272 return ("UNO Game\n"
273 f"Current Player: {self.get_current_player().name}\n"
274 f"Draw Pile: {len(self.draw_pile)} card(s)\n"
275 f"Discard Pile: {len(self.discard_pile)} card(s)\n"
276 f"Direction: {direction}\n"
277 f"Top Card: {self.get_top_card()}\n"
278 "Players:\n" + "\n".join(
279 [f" - {player.name}: {len(player)} card(s)" for player in
280 self.players]))
282 def __repr__(self):
283 return (f"{self.__class__.__name__}("
284 f"players={self.players!r}, "
285 f"draw_pile={self.draw_pile!r}, "
286 f"discard_pile={self.discard_pile!r}, "
287 f"hand_size={self.hand_size!r}, "
288 f"current_player_index={self.current_player_index!r}, "
289 f"direction={self.direction!r})")