From ce5b1ccdc933de46536ad22b368bf380ff101e86 Mon Sep 17 00:00:00 2001 From: Fabro Date: Thu, 4 Jun 2026 18:22:44 +0000 Subject: [PATCH] fabro(01KT9X0N42W6PRBVH2N5914EQJ): impl_data (succeeded) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fabro-Run: 01KT9X0N42W6PRBVH2N5914EQJ Fabro-Completed: 6 Fabro-Checkpoint: ddf1cc78194b37278b9ebc7e7aeaba87df0cf707 ⚒️ Generated with [Fabro](https://fabro.sh) --- solitaire-app/src/solitaire_tui/game.py | 112 ++++++++++++++++++------ solitaire-app/tests/test_game.py | 56 ++++++++++++ 2 files changed, 143 insertions(+), 25 deletions(-) diff --git a/solitaire-app/src/solitaire_tui/game.py b/solitaire-app/src/solitaire_tui/game.py index c91f9abbd..5ea50f59d 100644 --- a/solitaire-app/src/solitaire_tui/game.py +++ b/solitaire-app/src/solitaire_tui/game.py @@ -1,5 +1,5 @@ import random -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import List, Optional, Tuple, Dict, Any import copy @@ -13,24 +13,92 @@ class Card: def is_red(self) -> bool: return self.suit in ('H', 'D') + @property + def color(self) -> str: + return "red" if self.is_red else "black" + @property def label(self) -> str: ranks = {1: "A", 11: "J", 12: "Q", 13: "K"} return ranks.get(self.rank, str(self.rank)) + def display_str(self) -> str: + """Returns string representation of card depending on whether it is face up.""" + if self.is_face_up: + suit_syms = {'H': '♥', 'D': '♦', 'C': '♣', 'S': '♠'} + sym = suit_syms.get(self.suit, self.suit) + return f"{self.label}{sym}" + else: + return "##" + def __repr__(self) -> str: - suit_syms = {'H': '♥', 'D': '♦', 'C': '♣', 'S': '♠'} - sym = suit_syms.get(self.suit, self.suit) - face = self.label + sym if self.is_face_up else "##" + face = self.display_str() return f"[{face}]" +class Deck: + def __init__(self, seed: Optional[int] = None) -> None: + self.cards: List[Card] = [] + self.seed = seed + self.reset() + + def reset(self) -> None: + """Creates standard 52-card deck, face down.""" + self.cards = [ + Card(suit=s, rank=r, is_face_up=False) + for s in ['H', 'D', 'C', 'S'] + for r in range(1, 14) + ] + + def shuffle(self) -> None: + """Shuffles the deck using random or seed.""" + if self.seed is not None: + random.seed(self.seed) + else: + random.seed() + random.shuffle(self.cards) + + def draw(self) -> Card: + """Draws/pops a card from the deck.""" + return self.cards.pop() + + def __len__(self) -> int: + return len(self.cards) + + +class Pile(list): + """Base pile representation.""" + @property + def top_card(self) -> Optional[Card]: + return self[-1] if self else None + + +class StockPile(Pile): + """Stock pile representation (face-down draw pile).""" + pass + + +class WastePile(Pile): + """Waste pile representation (face-up drawn cards).""" + pass + + +class FoundationPile(Pile): + """Foundation pile representation (builds up from Ace to King by suit).""" + pass + + +class TableauPile(Pile): + """Tableau pile representation (7 columns of cards).""" + pass + + class GameState: def __init__(self, seed: Optional[int] = None) -> None: - self.stock: List[Card] = [] - self.waste: List[Card] = [] - self.foundations: List[List[Card]] = [[] for _ in range(4)] - self.tableau: List[List[Card]] = [[] for _ in range(7)] + self.stock: StockPile = StockPile() + self.waste: WastePile = WastePile() + self.foundations: List[FoundationPile] = [FoundationPile() for _ in range(4)] + self.tableau: List[TableauPile] = [TableauPile() for _ in range(7)] self.history: List[Dict[str, Any]] = [] self.seed = seed self.deal() @@ -65,32 +133,26 @@ class GameState: def deal(self) -> None: """Creates a standard 52-card deck, shuffles, and deals a new game.""" - suits = ['H', 'D', 'C', 'S'] - deck = [Card(suit=s, rank=r, is_face_up=False) for s in suits for r in range(1, 14)] - - if self.seed is not None: - random.seed(self.seed) - else: - random.seed() - random.shuffle(deck) + deck = Deck(seed=self.seed) + deck.shuffle() - self.stock = [] - self.waste = [] - self.foundations = [[] for _ in range(4)] - self.tableau = [[] for _ in range(7)] + self.stock = StockPile() + self.waste = WastePile() + self.foundations = [FoundationPile() for _ in range(4)] + self.tableau = [TableauPile() for _ in range(7)] self.history = [] # Deal to Tableau # Col 0 gets 1 card, Col 1 gets 2 cards, ..., Col 6 gets 7 cards for i in range(7): for j in range(i + 1): - card = deck.pop() + card = deck.draw() if j == i: card.is_face_up = True self.tableau[i].append(card) # Remaining cards go to Stock (face-down) - self.stock = deck + self.stock = StockPile(deck.cards) def draw(self) -> bool: """Draws one card from Stock to Waste. Recycles Waste to Stock if Stock is empty.""" @@ -103,10 +165,10 @@ class GameState: return False # Recycle Waste back to Stock # When we flip waste back to stock, we preserve order by reversing - self.stock = list(reversed(self.waste)) + self.stock = StockPile(reversed(self.waste)) for card in self.stock: card.is_face_up = False - self.waste = [] + self.waste = WastePile() return True card = self.stock.pop() @@ -219,7 +281,7 @@ class GameState: dest_pile = self.tableau[dest_col] moving_stack = src_pile[card_idx:] - self.tableau[src_col] = src_pile[:card_idx] + self.tableau[src_col] = TableauPile(src_pile[:card_idx]) dest_pile.extend(moving_stack) self.auto_reveal(src_col) diff --git a/solitaire-app/tests/test_game.py b/solitaire-app/tests/test_game.py index 03ec90162..9c82d1b13 100644 --- a/solitaire-app/tests/test_game.py +++ b/solitaire-app/tests/test_game.py @@ -126,5 +126,61 @@ class TestSolitaireGame(unittest.TestCase): self.assertTrue(self.game.check_win()) + def test_card_color_and_helpers(self): + # Card should have color property and display_str helper + red_card = Card(suit='H', rank=1, is_face_up=True) + black_card = Card(suit='S', rank=13, is_face_up=True) + face_down_card = Card(suit='D', rank=10, is_face_up=False) + + self.assertEqual(red_card.color, "red") + self.assertEqual(black_card.color, "black") + self.assertTrue(red_card.is_red) + self.assertFalse(black_card.is_red) + + self.assertEqual(red_card.display_str(), "A♥") + self.assertEqual(black_card.display_str(), "K♠") + self.assertEqual(face_down_card.display_str(), "##") + + # Repr check + self.assertEqual(repr(red_card), "[A♥]") + self.assertEqual(repr(face_down_card), "[##]") + + def test_deck_creation_and_shuffling(self): + from solitaire_tui.game import Deck + deck = Deck(seed=42) + self.assertEqual(len(deck), 52) + + # Deck cards should be face-down + for card in deck.cards: + self.assertFalse(card.is_face_up) + + # Drawing a card + top_card = deck.cards[-1] + drawn = deck.draw() + self.assertEqual(drawn, top_card) + self.assertEqual(len(deck), 51) + + # Shuffle + deck2 = Deck(seed=42) + deck2.shuffle() + # Verify that the deck order has changed from unshuffled + unshuffled_deck = Deck() + self.assertNotEqual([c.suit + str(c.rank) for c in deck2.cards], [c.suit + str(c.rank) for c in unshuffled_deck.cards]) + + def test_pile_abstractions(self): + from solitaire_tui.game import StockPile, WastePile, FoundationPile, TableauPile + stock = StockPile([Card('H', 1), Card('D', 2)]) + self.assertIsInstance(stock, list) + self.assertEqual(stock.top_card.rank, 2) + + waste = WastePile() + self.assertIsNone(waste.top_card) + + # Check types in GameState + self.assertIsInstance(self.game.stock, StockPile) + self.assertIsInstance(self.game.waste, WastePile) + self.assertIsInstance(self.game.foundations[0], FoundationPile) + self.assertIsInstance(self.game.tableau[0], TableauPile) + if __name__ == '__main__': unittest.main()