checkpoint

⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
Fabro 2026-06-04 14:29:38 -04:00
parent 6074fbdf55
commit 55b78d6bfa
5 changed files with 874 additions and 9 deletions

628
run.json

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,204 @@
diff --git a/solitaire-app/src/solitaire_tui/game.py b/solitaire-app/src/solitaire_tui/game.py
index 5ea50f59d..9b51e7d49 100644
--- a/solitaire-app/src/solitaire_tui/game.py
+++ b/solitaire-app/src/solitaire_tui/game.py
@@ -190,6 +190,15 @@ class GameState:
if not moving_card.is_face_up:
return False
+ # Validate that the entire stack from card_idx to the end is face-up, alternating, and descending
+ for i in range(card_idx, len(src_pile) - 1):
+ c1 = src_pile[i]
+ c2 = src_pile[i+1]
+ if not c1.is_face_up or not c2.is_face_up:
+ return False
+ if (c1.is_red == c2.is_red) or (c1.rank != c2.rank + 1):
+ return False
+
# If dest is empty, moving card must be a King (rank 13)
dest_pile = self.tableau[dest_col]
if not dest_pile:
@@ -215,6 +224,8 @@ class GameState:
return moving_card.rank == 13
dest_card = dest_pile[-1]
+ if not dest_card.is_face_up:
+ return False
return (moving_card.is_red != dest_card.is_red) and (moving_card.rank == dest_card.rank - 1)
def can_move_waste_to_foundation(self, dest_found: int) -> bool:
@@ -241,6 +252,9 @@ class GameState:
return False
moving_card = src_pile[-1]
+ if not moving_card.is_face_up:
+ return False
+
found_pile = self.foundations[dest_found]
if not found_pile:
@@ -264,6 +278,8 @@ class GameState:
return moving_card.rank == 13
dest_card = dest_pile[-1]
+ if not dest_card.is_face_up:
+ return False
return (moving_card.is_red != dest_card.is_red) and (moving_card.rank == dest_card.rank - 1)
def auto_reveal(self, col_idx: int) -> None:
diff --git a/solitaire-app/tests/test_game.py b/solitaire-app/tests/test_game.py
index 9c82d1b13..56def882a 100644
--- a/solitaire-app/tests/test_game.py
+++ b/solitaire-app/tests/test_game.py
@@ -182,5 +182,151 @@ class TestSolitaireGame(unittest.TestCase):
self.assertIsInstance(self.game.foundations[0], FoundationPile)
self.assertIsInstance(self.game.tableau[0], TableauPile)
+ def test_waste_to_tableau_moves(self):
+ # 1. Clear waste and test cannot move when waste is empty
+ self.game.waste.clear()
+ self.assertFalse(self.game.can_move_waste_to_tableau(dest_col=0))
+
+ # 2. Setup waste top card with a Red 5
+ self.game.waste.append(Card(suit='H', rank=5, is_face_up=True))
+
+ # 3. Setup tableau target with Black 6 (face up) -> Legal move
+ self.game.tableau[0] = [Card(suit='S', rank=6, is_face_up=True)]
+ self.assertTrue(self.game.can_move_waste_to_tableau(dest_col=0))
+
+ # Perform the move
+ success = self.game.move_waste_to_tableau(dest_col=0)
+ self.assertTrue(success)
+ self.assertEqual(len(self.game.waste), 0)
+ self.assertEqual(len(self.game.tableau[0]), 2)
+ self.assertEqual(self.game.tableau[0][-1], Card(suit='H', rank=5, is_face_up=True))
+
+ # 4. Setup waste with Red 5 again, but tableau has Black 5 -> Illegal move (same rank)
+ self.game.waste.append(Card(suit='D', rank=5, is_face_up=True))
+ self.game.tableau[1] = [Card(suit='C', rank=5, is_face_up=True)]
+ self.assertFalse(self.game.can_move_waste_to_tableau(dest_col=1))
+
+ # 5. Setup tableau with Red 6 -> Illegal move (same color)
+ self.game.tableau[2] = [Card(suit='H', rank=6, is_face_up=True)]
+ self.assertFalse(self.game.can_move_waste_to_tableau(dest_col=2))
+
+ # 6. Target is empty -> Illegal move because Red 5 is not King (rank 13)
+ self.game.tableau[3] = []
+ self.assertFalse(self.game.can_move_waste_to_tableau(dest_col=3))
+
+ # 7. Setup waste with King -> Legal to move to empty tableau
+ self.game.waste.clear()
+ self.game.waste.append(Card(suit='S', rank=13, is_face_up=True))
+ self.assertTrue(self.game.can_move_waste_to_tableau(dest_col=3))
+ success = self.game.move_waste_to_tableau(dest_col=3)
+ self.assertTrue(success)
+ self.assertEqual(len(self.game.tableau[3]), 1)
+ self.assertEqual(self.game.tableau[3][0].rank, 13)
+
+ def test_waste_to_foundation_moves(self):
+ # 1. Setup empty foundation, test cannot move waste when empty
+ self.game.foundations[0] = []
+ self.game.waste.clear()
+ self.assertFalse(self.game.can_move_waste_to_foundation(dest_found=0))
+
+ # 2. Setup waste with Ace of Hearts -> Legal
+ self.game.waste.append(Card(suit='H', rank=1, is_face_up=True))
+ self.assertTrue(self.game.can_move_waste_to_foundation(dest_found=0))
+
+ # Perform move
+ success = self.game.move_waste_to_foundation(dest_found=0)
+ self.assertTrue(success)
+ self.assertEqual(len(self.game.foundations[0]), 1)
+ self.assertEqual(self.game.foundations[0][-1], Card(suit='H', rank=1, is_face_up=True))
+
+ # 3. Setup waste with 2 of Hearts -> Legal (same suit, rank+1)
+ self.game.waste.append(Card(suit='H', rank=2, is_face_up=True))
+ self.assertTrue(self.game.can_move_waste_to_foundation(dest_found=0))
+ success = self.game.move_waste_to_foundation(dest_found=0)
+ self.assertTrue(success)
+
+ # 4. Setup waste with 4 of Hearts -> Illegal (rank not +1)
+ self.game.waste.append(Card(suit='H', rank=4, is_face_up=True))
+ self.assertFalse(self.game.can_move_waste_to_foundation(dest_found=0))
+
+ # 5. Setup waste with 3 of Diamonds -> Illegal (wrong suit)
+ self.game.waste.clear()
+ self.game.waste.append(Card(suit='D', rank=3, is_face_up=True))
+ self.assertFalse(self.game.can_move_waste_to_foundation(dest_found=0))
+
+ def test_tableau_to_foundation_moves(self):
+ # 1. Setup tableau with Ace of Clubs, foundation empty -> Legal
+ self.game.tableau[0] = [Card(suit='C', rank=1, is_face_up=True)]
+ self.game.foundations[1] = []
+ self.assertTrue(self.game.can_move_tableau_to_foundation(src_col=0, dest_found=1))
+
+ # Perform move
+ success = self.game.move_tableau_to_foundation(src_col=0, dest_found=1)
+ self.assertTrue(success)
+ self.assertEqual(len(self.game.tableau[0]), 0)
+ self.assertEqual(len(self.game.foundations[1]), 1)
+ self.assertEqual(self.game.foundations[1][-1].rank, 1)
+
+ # 2. Test tableau top card is face-down -> Illegal
+ self.game.tableau[1] = [Card(suit='C', rank=2, is_face_up=False)]
+ self.assertFalse(self.game.can_move_tableau_to_foundation(src_col=1, dest_found=1))
+
+ def test_foundation_to_tableau_moves(self):
+ # 1. Setup foundation with Ace and 2 of Spades, tableau with Red 3 -> Legal
+ self.game.foundations[0] = [
+ Card(suit='S', rank=1, is_face_up=True),
+ Card(suit='S', rank=2, is_face_up=True)
+ ]
+ self.game.tableau[0] = [Card(suit='H', rank=3, is_face_up=True)]
+ self.assertTrue(self.game.can_move_foundation_to_tableau(src_found=0, dest_col=0))
+
+ # Perform move
+ success = self.game.move_foundation_to_tableau(src_found=0, dest_col=0)
+ self.assertTrue(success)
+ self.assertEqual(len(self.game.foundations[0]), 1)
+ self.assertEqual(len(self.game.tableau[0]), 2)
+ self.assertEqual(self.game.tableau[0][-1], Card(suit='S', rank=2, is_face_up=True))
+
+ # 2. Setup foundation with 2 of Spades, tableau with Black 3 -> Illegal (same color)
+ self.game.foundations[0].append(Card(suit='S', rank=2, is_face_up=True))
+ self.game.tableau[1] = [Card(suit='C', rank=3, is_face_up=True)]
+ self.assertFalse(self.game.can_move_foundation_to_tableau(src_found=0, dest_col=1))
+
+ def test_moving_face_up_tableau_runs(self):
+ # Setup tableau with face-down card, then a run of Red 10, Black 9, Red 8 (all face-up)
+ self.game.tableau[0] = [
+ Card(suit='C', rank=12, is_face_up=False), # Face down King
+ Card(suit='D', rank=10, is_face_up=True),
+ Card(suit='S', rank=9, is_face_up=True),
+ Card(suit='H', rank=8, is_face_up=True)
+ ]
+ # Target has Black Jack (11)
+ self.game.tableau[1] = [Card(suit='C', rank=11, is_face_up=True)]
+
+ # Move the entire run starting at Red 10 (card_idx = 1) -> Legal
+ self.assertTrue(self.game.can_move_tableau_to_tableau(src_col=0, dest_col=1, card_idx=1))
+
+ # Perform move
+ success = self.game.move_tableau_to_tableau(src_col=0, dest_col=1, card_idx=1)
+ self.assertTrue(success)
+
+ # Check source pile has only 1 card left and it got automatically flipped face up!
+ self.assertEqual(len(self.game.tableau[0]), 1)
+ self.assertTrue(self.game.tableau[0][0].is_face_up)
+
+ # Check dest pile has 4 cards (Jack, 10, 9, 8) and their order and attributes are correct
+ self.assertEqual(len(self.game.tableau[1]), 4)
+ self.assertEqual(self.game.tableau[1][1], Card(suit='D', rank=10, is_face_up=True))
+ self.assertEqual(self.game.tableau[1][2], Card(suit='S', rank=9, is_face_up=True))
+ self.assertEqual(self.game.tableau[1][3], Card(suit='H', rank=8, is_face_up=True))
+
+ # Check moving an invalid run (e.g. non-alternating or face-down card) is prevented
+ self.game.tableau[2] = [
+ Card(suit='D', rank=5, is_face_up=True),
+ Card(suit='H', rank=4, is_face_up=True) # same color! invalid run
+ ]
+ self.game.tableau[3] = [Card(suit='C', rank=6, is_face_up=True)]
+ self.assertFalse(self.game.can_move_tableau_to_tableau(src_col=2, dest_col=3, card_idx=0))
+
if __name__ == '__main__':
unittest.main()

View file

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

View file

@ -0,0 +1,40 @@
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_data**: succeeded
- Model: gemini-3.5-flash, 92.2k tokens in / 3.7k out
- Files: /home/daytona/workspace/fabro/.ai/verify_data.md, /home/daytona/workspace/fabro/status.json
- **check_data**: succeeded
- **impl_logic**: succeeded
- Model: gemini-3.5-flash, 220.9k tokens in / 21.3k 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 Klondike game logic.
Run:
cd solitaire-app && python3 -m pytest tests/ -v
Check for coverage of:
- Deal invariants
- Tableau move legality
- Foundation move legality
- Stock/waste draw and recycle
- Undo
- Win detection
Write findings to .ai/verify_logic.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"
}