checkpoint

⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
Fabro 2026-06-04 14:24:16 -04:00
parent ddf1cc7819
commit 5042c7820e
5 changed files with 904 additions and 117 deletions

743
run.json

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,240 @@
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()

View file

@ -0,0 +1,6 @@
{
"outcome": "succeeded",
"notes": "Stage completed: impl_data",
"failure_reason": null,
"timestamp": "2026-06-04T18:22:41.080415Z"
}

View file

@ -0,0 +1,27 @@
Goal: Build a terminal-based Klondike solitaire game in Python with a curses TUI
## Completed stages
- **expand_spec**: succeeded
- Model: gemini-3.5-flash, 140.8k tokens in / 24.8k out
- Files: /home/daytona/workspace/fabro/.ai/solitaire-spec.md, /home/daytona/workspace/fabro/solitaire-app/game.py, /home/daytona/workspace/fabro/solitaire-app/main.py, /home/daytona/workspace/fabro/solitaire-app/test_game.py, /home/daytona/workspace/fabro/solitaire-app/ui.py, /home/daytona/workspace/fabro/status.json
- **impl_setup**: succeeded
- Model: gemini-3.5-flash, 148.0k tokens in / 18.3k out
- Files: /home/daytona/workspace/fabro/solitaire-app/README.md, /home/daytona/workspace/fabro/solitaire-app/main.py, /home/daytona/workspace/fabro/solitaire-app/pyproject.toml, /home/daytona/workspace/fabro/solitaire-app/src/solitaire_tui/__init__.py, /home/daytona/workspace/fabro/solitaire-app/src/solitaire_tui/game.py, /home/daytona/workspace/fabro/solitaire-app/src/solitaire_tui/ui.py, /home/daytona/workspace/fabro/solitaire-app/tests/__init__.py, /home/daytona/workspace/fabro/solitaire-app/tests/test_game.py, /home/daytona/workspace/fabro/status.json
- **verify_setup**: succeeded
- Model: gemini-3.5-flash, 154.6k tokens in / 2.2k out
- Files: /home/daytona/workspace/fabro/.ai/verify_setup.md, /home/daytona/workspace/fabro/status.json
- **check_setup**: succeeded
- **impl_data**: succeeded
- Model: gemini-3.5-flash, 218.9k tokens in / 18.1k out
- Files: /home/daytona/workspace/fabro/solitaire-app/src/solitaire_tui/game.py, /home/daytona/workspace/fabro/solitaire-app/tests/test_game.py, /home/daytona/workspace/fabro/status.json
Verify the solitaire data structures.
Run:
cd solitaire-app && python3 -m pytest tests/ -v && python3 -m py_compile main.py src/solitaire_tui/*.py
Check that Card, Deck, pile handling, and initial GameState/deal behavior are covered by tests.
Write findings to .ai/verify_data.md.
Write status.json at workspace root: outcome=succeeded if all checks pass, outcome=failed with failure_reason otherwise.

View file

@ -0,0 +1,5 @@
{
"mode": "agent",
"provider": "gemini",
"model": "gemini-3.5-flash"
}