mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
parent
6074fbdf55
commit
55b78d6bfa
5 changed files with 874 additions and 9 deletions
628
run.json
628
run.json
File diff suppressed because one or more lines are too long
204
stages/009-impl_logic@1/diff.patch
Normal file
204
stages/009-impl_logic@1/diff.patch
Normal 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()
|
||||
6
stages/009-impl_logic@1/status.json
Normal file
6
stages/009-impl_logic@1/status.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"outcome": "succeeded",
|
||||
"notes": "Stage completed: impl_logic",
|
||||
"failure_reason": null,
|
||||
"timestamp": "2026-06-04T18:27:48.001055Z"
|
||||
}
|
||||
40
stages/010-verify_logic@1/prompt.md
Normal file
40
stages/010-verify_logic@1/prompt.md
Normal 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.
|
||||
5
stages/010-verify_logic@1/provider_used.json
Normal file
5
stages/010-verify_logic@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