mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
parent
ddf1cc7819
commit
5042c7820e
5 changed files with 904 additions and 117 deletions
743
run.json
743
run.json
File diff suppressed because one or more lines are too long
240
stages/006-impl_data@1/diff.patch
Normal file
240
stages/006-impl_data@1/diff.patch
Normal 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()
|
||||
6
stages/006-impl_data@1/status.json
Normal file
6
stages/006-impl_data@1/status.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"outcome": "succeeded",
|
||||
"notes": "Stage completed: impl_data",
|
||||
"failure_reason": null,
|
||||
"timestamp": "2026-06-04T18:22:41.080415Z"
|
||||
}
|
||||
27
stages/007-verify_data@1/prompt.md
Normal file
27
stages/007-verify_data@1/prompt.md
Normal 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.
|
||||
5
stages/007-verify_data@1/provider_used.json
Normal file
5
stages/007-verify_data@1/provider_used.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"mode": "agent",
|
||||
"provider": "gemini",
|
||||
"model": "gemini-3.5-flash"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue