mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
parent
516516a770
commit
abeb8e2742
6 changed files with 884 additions and 11 deletions
633
run.json
633
run.json
File diff suppressed because one or more lines are too long
198
stages/009-impl_logic@1/diff.patch
Normal file
198
stages/009-impl_logic@1/diff.patch
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
diff --git a/card-game-app/tests/test_rules.py b/card-game-app/tests/test_rules.py
|
||||
index b24ab70be..ee6d27d4e 100644
|
||||
--- a/card-game-app/tests/test_rules.py
|
||||
+++ b/card-game-app/tests/test_rules.py
|
||||
@@ -78,3 +78,89 @@ def test_validate_move_sequence():
|
||||
assert not valid
|
||||
assert "Insufficient empty FreeCells" in reason
|
||||
|
||||
+def test_validate_move_to_freecell():
|
||||
+ state = GameState()
|
||||
+ state.free_cells = [None] * 4
|
||||
+ state.tableau[0] = [Card(Rank.ACE, Suit.SPADES)]
|
||||
+
|
||||
+ # Valid move of single card to empty FreeCell
|
||||
+ move_valid = Move('C', 0, 'F', 0, 1)
|
||||
+ valid, reason = validate_move(state, move_valid)
|
||||
+ assert valid
|
||||
+
|
||||
+ # Cannot move a sequence to a FreeCell
|
||||
+ state.tableau[0] = [Card(Rank.TWO, Suit.HEARTS), Card(Rank.ACE, Suit.SPADES)]
|
||||
+ move_seq = Move('C', 0, 'F', 1, 2)
|
||||
+ valid, reason = validate_move(state, move_seq)
|
||||
+ assert not valid
|
||||
+ assert "Cannot move a sequence to a FreeCell" in reason
|
||||
+
|
||||
+ # Cannot move to an occupied FreeCell
|
||||
+ state.free_cells[2] = Card(Rank.KING, Suit.CLUBS)
|
||||
+ move_occ = Move('C', 0, 'F', 2, 1)
|
||||
+ valid, reason = validate_move(state, move_occ)
|
||||
+ assert not valid
|
||||
+ assert "Target FreeCell is occupied" in reason
|
||||
+
|
||||
+def test_validate_move_to_foundation():
|
||||
+ state = GameState()
|
||||
+ state.free_cells = [None] * 4
|
||||
+ state.foundations = {suit: [] for suit in Suit}
|
||||
+ state.tableau[0] = [Card(Rank.ACE, Suit.SPADES)]
|
||||
+ state.tableau[1] = [Card(Rank.TWO, Suit.SPADES)]
|
||||
+
|
||||
+ # Moving Ace of Spades to empty Foundation is valid
|
||||
+ move_ace = Move('C', 0, 'A', 0, 1)
|
||||
+ valid, reason = validate_move(state, move_ace)
|
||||
+ assert valid
|
||||
+
|
||||
+ # Moving Two of Spades to empty Foundation is invalid
|
||||
+ move_two_invalid = Move('C', 1, 'A', 0, 1)
|
||||
+ valid, reason = validate_move(state, move_two_invalid)
|
||||
+ assert not valid
|
||||
+ assert "Foundations must start with an Ace" in reason
|
||||
+
|
||||
+ # Place Ace of Spades in foundation first
|
||||
+ state.foundations[Suit.SPADES] = [Card(Rank.ACE, Suit.SPADES)]
|
||||
+ # Now, moving Two of Spades to foundation is valid
|
||||
+ move_two_valid = Move('C', 1, 'A', 0, 1)
|
||||
+ valid, reason = validate_move(state, move_two_valid)
|
||||
+ assert valid
|
||||
+
|
||||
+ # Moving a non-consecutive card (e.g. Four of Spades) is invalid
|
||||
+ state.tableau[2] = [Card(Rank.FOUR, Suit.SPADES)]
|
||||
+ move_four_invalid = Move('C', 2, 'A', 0, 1)
|
||||
+ valid, reason = validate_move(state, move_four_invalid)
|
||||
+ assert not valid
|
||||
+ assert "Must be next rank up" in reason
|
||||
+
|
||||
+ # Cannot move a sequence to a Foundation
|
||||
+ state.tableau[3] = [Card(Rank.THREE, Suit.HEARTS), Card(Rank.TWO, Suit.SPADES)]
|
||||
+ move_seq = Move('C', 3, 'A', 0, 2)
|
||||
+ valid, reason = validate_move(state, move_seq)
|
||||
+ assert not valid
|
||||
+ assert "Cannot move a sequence to a Foundation" in reason
|
||||
+
|
||||
+def test_validate_move_sequence_invalid_alternating_color():
|
||||
+ state = GameState()
|
||||
+ state.free_cells = [None] * 4
|
||||
+ state.tableau[0] = [Card(Rank.JACK, Suit.HEARTS), Card(Rank.TEN, Suit.DIAMONDS)]
|
||||
+
|
||||
+ # Hearts and Diamonds are both RED. Sequence J♥, 10♦ is invalid!
|
||||
+ move = Move('C', 0, 'C', 1, 2)
|
||||
+ valid, reason = validate_move(state, move)
|
||||
+ assert not valid
|
||||
+ assert "alternating color descending sequence" in reason
|
||||
+
|
||||
+def test_validate_move_sequence_invalid_ranks():
|
||||
+ state = GameState()
|
||||
+ state.free_cells = [None] * 4
|
||||
+ state.tableau[0] = [Card(Rank.JACK, Suit.HEARTS), Card(Rank.NINE, Suit.SPADES)]
|
||||
+
|
||||
+ # Jack (11) and Nine (9) is invalid sequence (should be Jack and Ten)
|
||||
+ move = Move('C', 0, 'C', 1, 2)
|
||||
+ valid, reason = validate_move(state, move)
|
||||
+ assert not valid
|
||||
+ assert "alternating color descending sequence" in reason
|
||||
+
|
||||
+
|
||||
diff --git a/card-game-app/tests/test_state.py b/card-game-app/tests/test_state.py
|
||||
index 8fa28a0f8..57eb946a8 100644
|
||||
--- a/card-game-app/tests/test_state.py
|
||||
+++ b/card-game-app/tests/test_state.py
|
||||
@@ -127,3 +127,99 @@ def test_is_lost():
|
||||
# Thus, no legal moves are possible!
|
||||
assert state.is_lost()
|
||||
|
||||
+def test_execute_invalid_move_no_history():
|
||||
+ state = GameState()
|
||||
+ state.tableau = [[] for _ in range(8)]
|
||||
+ state.free_cells = [None] * 4
|
||||
+ state.tableau[0] = [Card(Rank.KING, Suit.HEARTS)]
|
||||
+ state.tableau[1] = [Card(Rank.TEN, Suit.DIAMONDS)]
|
||||
+
|
||||
+ # Attempt invalid move (cannot place 10♦ on K♥)
|
||||
+ move = Move('C', 1, 'C', 0, 1)
|
||||
+ success, reason = state.execute_move(move)
|
||||
+ assert not success
|
||||
+ assert not state.history # History should remain empty
|
||||
+
|
||||
+def test_multiple_undo_redo():
|
||||
+ state = GameState()
|
||||
+ state.tableau = [[] for _ in range(8)]
|
||||
+ state.free_cells = [None] * 4
|
||||
+ state.foundations = {suit: [] for suit in Suit}
|
||||
+
|
||||
+ # Set up some cards
|
||||
+ state.tableau[0] = [Card(Rank.ACE, Suit.SPADES)]
|
||||
+ state.tableau[1] = [Card(Rank.TWO, Suit.SPADES)]
|
||||
+
|
||||
+ # 1. Move Ace of Spades to foundation
|
||||
+ success, _ = state.execute_move(Move('C', 0, 'A', 0, 1))
|
||||
+ assert success
|
||||
+ assert len(state.foundations[Suit.SPADES]) == 1
|
||||
+
|
||||
+ # 2. Move Two of Spades to foundation (will auto-home, but we move manually)
|
||||
+ success, _ = state.execute_move(Move('C', 1, 'A', 0, 1))
|
||||
+ assert success
|
||||
+ assert len(state.foundations[Suit.SPADES]) == 2
|
||||
+
|
||||
+ # Check state before undo
|
||||
+ assert not state.tableau[0]
|
||||
+ assert not state.tableau[1]
|
||||
+
|
||||
+ # Undo 2nd move
|
||||
+ assert state.undo()
|
||||
+ assert len(state.foundations[Suit.SPADES]) == 1
|
||||
+ assert state.tableau[1] == [Card(Rank.TWO, Suit.SPADES)]
|
||||
+
|
||||
+ # Undo 1st move
|
||||
+ assert state.undo()
|
||||
+ assert len(state.foundations[Suit.SPADES]) == 0
|
||||
+ assert state.tableau[0] == [Card(Rank.ACE, Suit.SPADES)]
|
||||
+
|
||||
+ # No more history
|
||||
+ assert not state.undo()
|
||||
+
|
||||
+ # Redo 1st move
|
||||
+ assert state.redo()
|
||||
+ assert len(state.foundations[Suit.SPADES]) == 1
|
||||
+ assert not state.tableau[0]
|
||||
+
|
||||
+ # Redo 2nd move
|
||||
+ assert state.redo()
|
||||
+ assert len(state.foundations[Suit.SPADES]) == 2
|
||||
+ assert not state.tableau[1]
|
||||
+
|
||||
+ # No more redo history
|
||||
+ assert not state.redo()
|
||||
+
|
||||
+def test_is_lost_false_if_freecell_can_move_to_tableau():
|
||||
+ state = GameState()
|
||||
+ # 8 columns filled with 5♠ -> no moves between columns
|
||||
+ state.tableau = [[Card(Rank.FIVE, Suit.SPADES)] for _ in range(8)]
|
||||
+ # All FreeCells but one occupied by King of Hearts. One has Six of Hearts
|
||||
+ state.free_cells = [
|
||||
+ Card(Rank.KING, Suit.HEARTS),
|
||||
+ Card(Rank.KING, Suit.HEARTS),
|
||||
+ Card(Rank.KING, Suit.HEARTS),
|
||||
+ Card(Rank.FOUR, Suit.HEARTS) # Four of Hearts can go on Five of Spades!
|
||||
+ ]
|
||||
+ state.foundations = {suit: [] for suit in Suit}
|
||||
+
|
||||
+ # Not lost because 4♥ in FreeCell can move onto 5♠ on any Tableau column
|
||||
+ assert not state.is_lost()
|
||||
+
|
||||
+def test_is_lost_false_if_freecell_can_move_to_foundation():
|
||||
+ state = GameState()
|
||||
+ # 8 columns filled with 5♠ -> no moves between columns
|
||||
+ state.tableau = [[Card(Rank.FIVE, Suit.SPADES)] for _ in range(8)]
|
||||
+ # FreeCells occupied, one contains Ace of Spades
|
||||
+ state.free_cells = [
|
||||
+ Card(Rank.KING, Suit.HEARTS),
|
||||
+ Card(Rank.KING, Suit.HEARTS),
|
||||
+ Card(Rank.KING, Suit.HEARTS),
|
||||
+ Card(Rank.ACE, Suit.SPADES) # Ace can go to foundation
|
||||
+ ]
|
||||
+ state.foundations = {suit: [] for suit in Suit}
|
||||
+
|
||||
+ # Not lost because Ace can go to empty Spades Foundation
|
||||
+ assert not state.is_lost()
|
||||
+
|
||||
+
|
||||
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-04T19:39:15.067656Z"
|
||||
}
|
||||
34
stages/010-verify_logic@1/prompt.md
Normal file
34
stages/010-verify_logic@1/prompt.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
Goal: Build a terminal-based FreeCell solitaire game in Python
|
||||
|
||||
## Completed stages
|
||||
- **expand_spec**: succeeded
|
||||
- Model: gemini-3.5-flash, 91.5k tokens in / 10.9k out
|
||||
- Files: .ai/card-game-spec.md, status.json
|
||||
- **impl_setup**: succeeded
|
||||
- Model: gemini-3.5-flash, 174.8k tokens in / 9.7k out
|
||||
- Files: /home/daytona/workspace/fabro/card-game-app/README.md, /home/daytona/workspace/fabro/card-game-app/main.py, /home/daytona/workspace/fabro/card-game-app/pyproject.toml, /home/daytona/workspace/fabro/card-game-app/src/card_game_tui/__init__.py, /home/daytona/workspace/fabro/card-game-app/src/card_game_tui/engine.py, /home/daytona/workspace/fabro/card-game-app/src/card_game_tui/tui.py, /home/daytona/workspace/fabro/card-game-app/tests/__init__.py, /home/daytona/workspace/fabro/card-game-app/tests/test_card.py, /home/daytona/workspace/fabro/card-game-app/tests/test_integration.py, /home/daytona/workspace/fabro/card-game-app/tests/test_rules.py, /home/daytona/workspace/fabro/card-game-app/tests/test_state.py, /home/daytona/workspace/fabro/status.json
|
||||
- **verify_setup**: succeeded
|
||||
- Model: gemini-3.5-flash, 148.1k tokens in / 2.4k 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, 258.5k tokens in / 19.8k out
|
||||
- Files: /home/daytona/workspace/fabro/card-game-app/src/card_game_tui/engine.py, /home/daytona/workspace/fabro/card-game-app/tests/test_rules.py, /home/daytona/workspace/fabro/card-game-app/tests/test_state.py, /home/daytona/workspace/fabro/status.json
|
||||
- **verify_data**: succeeded
|
||||
- Model: gemini-3.5-flash, 134.1k tokens in / 3.8k 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, 209.9k tokens in / 9.8k out
|
||||
- Files: /home/daytona/workspace/fabro/card-game-app/tests/test_rules.py, /home/daytona/workspace/fabro/card-game-app/tests/test_state.py, /home/daytona/workspace/fabro/status.json
|
||||
|
||||
|
||||
Verify the card game logic.
|
||||
|
||||
Run:
|
||||
cd card-game-app && python3 -m pytest tests/ -v
|
||||
|
||||
Check move/action validation, win/loss detection, and undo.
|
||||
|
||||
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"
|
||||
}
|
||||
19
stages/010-verify_logic@1/response.md
Normal file
19
stages/010-verify_logic@1/response.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
I have verified the card game logic and completed all the requirements.
|
||||
|
||||
### Summary of Completed Work
|
||||
1. **Executed the Test Suite**:
|
||||
Ran the tests in `card-game-app` using `pytest`:
|
||||
```bash
|
||||
cd card-game-app && python3 -m pytest tests/ -v
|
||||
```
|
||||
All **21 tests** passed successfully, verifying that cards are correctly represented and all game mechanics are sound.
|
||||
|
||||
2. **Verified Key Features**:
|
||||
- **Move & Action Validation**: Confirmed that alternating colors, descending rank order, FreeCell occupancy, and Foundation sequence rules are strictly enforced. Calculated the supermove limit dynamically according to the standard FreeCell capacity formula: $\text{Max Movable} = (1 + F) \times 2^T$ (with proper $T-1$ adjustment for empty column target destinations).
|
||||
- **Win & Loss Detection**: Verified that `is_won()` successfully identifies when all 52 cards are correctly stacked on their respective Foundation piles. Confirmed that `is_lost()` exhaustively checks all single-card and sequence-card transfer possibilities to identify deadlock states.
|
||||
- **Undo & Redo System**: Confirmed that `save_state` creates a deep structural copy of the columns, free cells, and foundation piles prior to each move execution, supporting complete multi-level undo and redo branches seamlessly.
|
||||
- **Auto-Homing Logic**: Validated the safe auto-homing heuristic, ensuring that card $R$ of a suit is automatically moved to its foundation only when opposite color cards of rank $R-1$ and same-color other suit cards of rank $R-2$ are in foundation.
|
||||
|
||||
3. **Wrote Deliverables**:
|
||||
- Created the detailed findings document at `.ai/verify_logic.md`.
|
||||
- Updated the status document at `status.json` to reflect `outcome=succeeded`.
|
||||
Loading…
Add table
Reference in a new issue