mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-06 02:48:25 +00:00
parent
84b9439e7b
commit
74d93db932
5 changed files with 784 additions and 14 deletions
386
run.json
386
run.json
File diff suppressed because one or more lines are too long
378
stages/002-expand_spec@1/diff.patch
Normal file
378
stages/002-expand_spec@1/diff.patch
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
diff --git a/.ai/card-game-spec.md b/.ai/card-game-spec.md
|
||||
new file mode 100644
|
||||
index 000000000..ccc4bc1b0
|
||||
--- /dev/null
|
||||
+++ b/.ai/card-game-spec.md
|
||||
@@ -0,0 +1,362 @@
|
||||
+# Technical Specification: Terminal-Based FreeCell Solitaire in Python
|
||||
+
|
||||
+This document specifies the architecture, domain logic, user interface, and testing strategy for a terminal-based FreeCell Solitaire game built with Python.
|
||||
+
|
||||
+The application is split into two completely decoupled layers:
|
||||
+1. **Core Domain Model / Game Engine**: Fully testable, framework-independent, pure-Python representations of cards, piles, moves, validation, and game state.
|
||||
+2. **Curses-based Terminal UI (TUI)**: A visual presentation layer built on the standard-library `curses` module, handling input polling, terminal events (like resizing), and colorized rendering.
|
||||
+
|
||||
+---
|
||||
+
|
||||
+## 1. Game Rules & Mechanics
|
||||
+
|
||||
+FreeCell is a solitaire card game played with a single standard 52-card deck. All cards are dealt face-up at the start.
|
||||
+
|
||||
+### 1.1 Table Layout
|
||||
+The game screen consists of three main areas:
|
||||
+- **Free Cells (4 cells)**: Temporary holding spaces. Each cell can hold at most one card of any suit and rank.
|
||||
+- **Foundations (4 piles)**: Destination piles built up by suit from Ace (rank 1) through King (rank 13). Winning the game requires placing all 52 cards here.
|
||||
+- **Tableau (8 columns)**: The main play area. Columns 1–4 are dealt 7 cards each; columns 5–8 are dealt 6 cards each.
|
||||
+
|
||||
+### 1.2 Movement Rules
|
||||
+- **Moving to a Free Cell**: Any single card can be moved to an empty Free Cell.
|
||||
+- **Moving to a Foundation**:
|
||||
+ - An **Ace** can be moved to an empty Foundation pile.
|
||||
+ - A card of rank $R$ and suit $S$ can be placed on a Foundation pile if the current top card of that pile is of suit $S$ and rank $R - 1$.
|
||||
+- **Moving to a Tableau Column**:
|
||||
+ - A single card of rank $R$ and color $C$ can be placed on top of a tableau card of rank $R+1$ and the *opposite* color (e.g., a Black 7 on a Red 8).
|
||||
+ - An empty tableau column can accept any single card.
|
||||
+- **Sequence Moves (Multiple Cards)**:
|
||||
+ - A sequence of cards that is properly ordered (descending ranks and alternating colors) can be moved together from one tableau column to another.
|
||||
+ - The maximum size of a sequence that can be moved depends on the number of empty Free Cells and empty Tableau columns available as temporary landing zones.
|
||||
+ - **Formula for Max Move Size**:
|
||||
+ $$\text{Max Cards} = (1 + \text{Empty Free Cells}) \times 2^{\text{Empty Tableau Columns}}$$
|
||||
+ *Exception*: If the destination column is an empty tableau column, that empty column does not count as "empty" in the exponent of the formula (as it is the final destination, not a temporary transit zone).
|
||||
+
|
||||
+### 1.3 Automatic Moves ("Auto-Homing")
|
||||
+To reduce tedious manual moves, the engine should automatically move cards to the Foundations if they can no longer be used as transition cards for lower-ranked cards. A card of rank $R$ and suit $S$ can safely be "auto-homed" to its Foundation if:
|
||||
+1. It is a legal move (it is on top of a tableau/freecell and matches $R-1$ of its foundation).
|
||||
+2. All cards of the *opposite color* with rank $R-1$ or lower have already been placed in the foundations. This ensures that no card still needs to be built on top of this card in the tableaus.
|
||||
+
|
||||
+---
|
||||
+
|
||||
+## 2. Core Domain Architecture (Non-UI)
|
||||
+
|
||||
+All domain logic must reside in classes that do not import or assume a `curses` environment. This ensures unit tests can run in headless CI environments.
|
||||
+
|
||||
+```
|
||||
++-------------------------------------------------------------+
|
||||
+| Domain Model |
|
||||
+| |
|
||||
+| +----------------+ +---------------+ |
|
||||
+| | Card |---> | Deck | |
|
||||
+| +----------------+ +---------------+ |
|
||||
+| | |
|
||||
+| v |
|
||||
+| +----------------+ |
|
||||
+| | GameState | <--- Handles validation, moves, undo |
|
||||
+| +----------------+ |
|
||||
++-------------------------------------------------------------+
|
||||
+```
|
||||
+
|
||||
+### 2.1 Domain Types & Data Structures
|
||||
+
|
||||
+#### Card Representation
|
||||
+```python
|
||||
+from dataclasses import dataclass
|
||||
+from enum import Enum, auto
|
||||
+
|
||||
+class Suit(Enum):
|
||||
+ HEARTS = "H"
|
||||
+ DIAMONDS = "D"
|
||||
+ CLUBS = "C"
|
||||
+ SPADES = "S"
|
||||
+
|
||||
+ @property
|
||||
+ def color(self) -> str:
|
||||
+ return "RED" if self in (Suit.HEARTS, Suit.DIAMONDS) else "BLACK"
|
||||
+
|
||||
+ @property
|
||||
+ def symbol(self) -> str:
|
||||
+ # Unicode symbols for attractive rendering
|
||||
+ return {
|
||||
+ Suit.HEARTS: "♥",
|
||||
+ Suit.DIAMONDS: "♦",
|
||||
+ Suit.CLUBS: "♣",
|
||||
+ Suit.SPADES: "♠"
|
||||
+ }[self]
|
||||
+
|
||||
+class Rank(Enum):
|
||||
+ ACE = 1
|
||||
+ TWO = 2
|
||||
+ THREE = 3
|
||||
+ FOUR = 4
|
||||
+ FIVE = 5
|
||||
+ SIX = 6
|
||||
+ SEVEN = 7
|
||||
+ EIGHT = 8
|
||||
+ NINE = 9
|
||||
+ TEN = 10
|
||||
+ JACK = 11
|
||||
+ QUEEN = 12
|
||||
+ KING = 13
|
||||
+
|
||||
+ @property
|
||||
+ def label(self) -> str:
|
||||
+ if self.value == 1: return "A"
|
||||
+ if self.value == 11: return "J"
|
||||
+ if self.value == 12: return "Q"
|
||||
+ if self.value == 13: return "K"
|
||||
+ return str(self.value)
|
||||
+
|
||||
+@dataclass(frozen=True)
|
||||
+class Card:
|
||||
+ suit: Suit
|
||||
+ rank: Rank
|
||||
+
|
||||
+ def __repr__(self) -> str:
|
||||
+ return f"{self.rank.label}{self.suit.symbol}"
|
||||
+```
|
||||
+
|
||||
+#### Deck & Setup
|
||||
+```python
|
||||
+import random
|
||||
+from typing import List
|
||||
+
|
||||
+class Deck:
|
||||
+ def __init__(self, seed: int = None):
|
||||
+ self.cards = [Card(suit, rank) for suit in Suit for rank in Rank]
|
||||
+ if seed is not None:
|
||||
+ random.seed(seed)
|
||||
+ random.shuffle(self.cards)
|
||||
+
|
||||
+ def deal(self) -> List[List[Card]]:
|
||||
+ """Deals 52 cards into 8 columns."""
|
||||
+ tableaus: List[List[Card]] = [[] for _ in range(8)]
|
||||
+ for i, card in enumerate(self.cards):
|
||||
+ tableaus[i % 8].append(card)
|
||||
+ return tableaus
|
||||
+```
|
||||
+
|
||||
+#### Move Representation
|
||||
+To support an robust Undo/Redo stack, each move is captured as a detailed state transition:
|
||||
+```python
|
||||
+from typing import Union
|
||||
+
|
||||
+class LocationType(Enum):
|
||||
+ TABLEAU = auto()
|
||||
+ FREECELL = auto()
|
||||
+ FOUNDATION = auto()
|
||||
+
|
||||
+@dataclass(frozen=True)
|
||||
+class Position:
|
||||
+ type: LocationType
|
||||
+ index: int # 0-7 for Tableau, 0-3 for FreeCell, 0-3 for Foundation
|
||||
+
|
||||
+@dataclass
|
||||
+class MoveRecord:
|
||||
+ from_pos: Position
|
||||
+ to_pos: Position
|
||||
+ cards: List[Card] # Captured for single or sequence moves
|
||||
+ auto_moves: List['MoveRecord'] = None # Nested moves triggered by auto-homing
|
||||
+```
|
||||
+
|
||||
+### 2.2 Core Engine (`GameState`)
|
||||
+The state engine encapsulates all state, mutations, validation, and history.
|
||||
+
|
||||
+```python
|
||||
+class GameState:
|
||||
+ def __init__(self, seed: int = None):
|
||||
+ self.tableaus: List[List[Card]] = Deck(seed).deal()
|
||||
+ self.freecells: List[Union[Card, None]] = [None] * 4
|
||||
+ self.foundations: dict[Suit, List[Card]] = {suit: [] for suit in Suit}
|
||||
+ self.undo_stack: List[MoveRecord] = []
|
||||
+ self.redo_stack: List[MoveRecord] = []
|
||||
+
|
||||
+ def get_card_at(self, position: Position) -> Union[Card, None]:
|
||||
+ if position.type == LocationType.FREECELL:
|
||||
+ return self.freecells[position.index]
|
||||
+ elif position.type == LocationType.FOUNDATION:
|
||||
+ pile = self.foundations[list(Suit)[position.index]]
|
||||
+ return pile[-1] if pile else None
|
||||
+ elif position.type == LocationType.TABLEAU:
|
||||
+ col = self.tableaus[position.index]
|
||||
+ return col[-1] if col else None
|
||||
+ return None
|
||||
+
|
||||
+ def validate_move(self, from_pos: Position, to_pos: Position, count: int = 1) -> bool:
|
||||
+ """
|
||||
+ Calculates whether moving 'count' cards from from_pos to to_pos is legal.
|
||||
+ """
|
||||
+ # 1. Basic boundary and type validations...
|
||||
+ # 2. Extract card(s) to be moved...
|
||||
+ # 3. Check target constraints (descending rank, alternate color for tableaus; suit-match ascending for foundation)
|
||||
+ # 4. Check capacity constraints if sequence move (using the formula)
|
||||
+ pass
|
||||
+
|
||||
+ def execute_move(self, from_pos: Position, to_pos: Position, count: int = 1) -> bool:
|
||||
+ """
|
||||
+ Executes a move, saves it to the undo stack, runs auto-homing, and clears the redo stack.
|
||||
+ """
|
||||
+ if not self.validate_move(from_pos, to_pos, count):
|
||||
+ return False
|
||||
+ # Perform move logic, handle nested auto-moves, append to undo_stack...
|
||||
+ return True
|
||||
+
|
||||
+ def undo(self) -> bool:
|
||||
+ """Reverts the last move, including nested auto-homing steps."""
|
||||
+ if not self.undo_stack:
|
||||
+ return False
|
||||
+ # Revert changes using move records and push to redo stack...
|
||||
+ return True
|
||||
+
|
||||
+ def check_win(self) -> bool:
|
||||
+ """Returns True if all 52 cards are in the Foundations."""
|
||||
+ return all(len(self.foundations[suit]) == 13 for suit in Suit)
|
||||
+```
|
||||
+
|
||||
+---
|
||||
+
|
||||
+## 3. UI Layout & Visual Design
|
||||
+
|
||||
+The UI will be formatted to fit standard $80 \times 24$ terminal windows, but will dynamically adapt if larger screen space is available.
|
||||
+
|
||||
+### 3.1 Terminal ASCII / Unicode Grid Layout
|
||||
+
|
||||
+```
|
||||
++-----------------------------------------------------------------------------+
|
||||
+| FREECELL SOLITAIRE [SEED: 49281] |
|
||||
+| |
|
||||
+| Free Cells Foundations |
|
||||
+| [A] [B] [C] [D] [♥] [♦] [♣] [♠] |
|
||||
+| +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ |
|
||||
+| | 5♣| | | | | | | | A♥| | | | | | | |
|
||||
+| +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ |
|
||||
+| |
|
||||
+| Tableau Columns |
|
||||
+| [1] [2] [3] [4] [5] [6] [7] [8] |
|
||||
+| +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ |
|
||||
+| | K♠| | J♦| | Q♣| | 9♦| | 8♣| | 6♥| | 10♦| | A♠| |
|
||||
+| | Q♦| | 10♣| | 3♦| | 8♥| | 7♦| | 5♦| | 9♠| | | |
|
||||
+| | J♣| | | | 2♠| | | | 6♠| | | | | | | |
|
||||
+| | 10♦| | | | | | | | 5♥| | | | | | | |
|
||||
+| +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ |
|
||||
+| |
|
||||
+|-----------------------------------------------------------------------------|
|
||||
+| MSG: Selected [1] K♠. Choose destination column or cell. |
|
||||
+| KEYS: [Arrows] Navigate [Space] Select/Drop [U] Undo [R] Restart [Q] Quit|
|
||||
++-----------------------------------------------------------------------------+
|
||||
+```
|
||||
+
|
||||
+### 3.2 Visual Components and Curses Rendering Strategy
|
||||
+
|
||||
+1. **Card Rendering**:
|
||||
+ - Empty slots (cells, tableaus, foundations) are rendered with dashed borders `· · ·` or generic outlines `[ ]`.
|
||||
+ - Cards are rendered with a white background/colored foreground using curses color pairs:
|
||||
+ - **Red Cards (Hearts, Diamonds)**: Red text on a white or black background.
|
||||
+ - **Black Cards (Clubs, Spades)**: Bold white or cyan text on a black background (or black text on a white background).
|
||||
+2. **Double-Buffered Screen**:
|
||||
+ - Use `curses.newwin()` for distinct sub-regions (Top panel, Main game table, Status bar) to organize layout.
|
||||
+ - Run calculations and render elements using memory-efficient double-buffering via `stdscr.noutrefresh()` and a final `curses.doupdate()`.
|
||||
+3. **Cursor Navigation & Selection**:
|
||||
+ - **Keyboard Cursor**: An active selection box/cursor highlighting a single pile/cell is moved via the **Arrow Keys** (or WASD).
|
||||
+ - **Selection State**:
|
||||
+ - First press of `Space` or `Enter` on a non-empty slot highlights the card(s) to move (visual "lift" state).
|
||||
+ - Moving the cursor to another column and pressing `Space` / `Enter` attempts the move.
|
||||
+ - Pressing `Escape` cancels the active selection.
|
||||
+
|
||||
+---
|
||||
+
|
||||
+## 4. Input Handling & UI Interaction
|
||||
+
|
||||
+### 4.1 Curses Main Loop & Input States
|
||||
+
|
||||
+```
|
||||
+ +----------------------+
|
||||
+ | Game Loop |
|
||||
+ +----------------------+
|
||||
+ |
|
||||
+ v
|
||||
+ [ Wait for Key ]
|
||||
+ |
|
||||
+ +--------------+--------------+
|
||||
+ | |
|
||||
+ v v
|
||||
+ [ Action Key ] [ Navigation Key ]
|
||||
+ (Q, U, R, ESC, etc.) (Arrows, Tab, WASD)
|
||||
+ | |
|
||||
+ v v
|
||||
+ Handle Command Move Cursor Highlight
|
||||
+ | |
|
||||
+ +--------------+--------------+
|
||||
+ |
|
||||
+ v
|
||||
+ [ Update State ]
|
||||
+ |
|
||||
+ v
|
||||
+ [ Redraw UI ]
|
||||
+```
|
||||
+
|
||||
+### 4.2 Keybindings Map
|
||||
+
|
||||
+| Key | Action |
|
||||
+| --- | --- |
|
||||
+| `Left / Right / Up / Down` | Navigate cursor across cells, foundations, and tableaus |
|
||||
+| `Space / Enter` | Select card at cursor / Move selected card to cursor target |
|
||||
+| `Escape` | Clear active card selection |
|
||||
+| `u / U` | Undo last move (supports infinite undo back to the start) |
|
||||
+| `r / R` | Restart current game (with same seed/shuffling) |
|
||||
+| `n / N` | Start a completely new game with a random seed |
|
||||
+| `q / Q` | Exit the game gracefully (presents confirm dialog) |
|
||||
+
|
||||
+---
|
||||
+
|
||||
+## 5. Non-Interactive Smoke Mode
|
||||
+
|
||||
+To ensure the CLI can be verified programmatically without invoking an interactive full-screen curses environment, the application must support a non-interactive setup probe via `--smoke`:
|
||||
+
|
||||
+```bash
|
||||
+python3 main.py --smoke
|
||||
+```
|
||||
+
|
||||
+### 5.1 Smoke Mode Requirements:
|
||||
+- Must not call `curses.initscr()`, `curses.wrapper()`, or perform terminal modifications.
|
||||
+- Must initialize the domain engine (`GameState`) with a deterministic test seed.
|
||||
+- Must execute a tiny mock transaction sequence (e.g., attempt 2 valid moves and 1 invalid move, validating assertion outcomes).
|
||||
+- Must print a plain-text confirmation of the test result to stdout.
|
||||
+- Must exit with status code `0` on success, or a non-zero exit code if an import or validation error occurs.
|
||||
+
|
||||
+---
|
||||
+
|
||||
+## 6. Testing Strategy
|
||||
+
|
||||
+Maintaining 100% testability on rules guarantees that bugs are never hidden behind UI refresh issues.
|
||||
+
|
||||
+### 6.1 Unit Tests (Headless)
|
||||
+All game rule constraints must be tested inside a robust suite of unit tests (e.g. using `unittest` or `pytest`):
|
||||
+
|
||||
+- **Deck Verification**: Ensure 52 unique cards are present; verify that deterministic seeding generates identical layouts.
|
||||
+- **Move Validations**:
|
||||
+ - Valid and invalid moves to empty and occupied free cells.
|
||||
+ - Valid sequence building on Tableau (alternating color, consecutive ranks).
|
||||
+ - Multi-card tableau-to-tableau sequence validation obeying the formula: $(1 + \text{empty cells}) \times 2^{\text{empty tableaus}}$.
|
||||
+ - Valid and invalid foundation building (Aces first, ordered ascending by suit).
|
||||
+- **Auto-Homing Logic**: Verify that cards are only auto-homed when all cards of the opposite color of smaller rank have already been homed.
|
||||
+- **Undo/Redo System**: Run a multi-step sequence, trigger undo, verify state completely matches initial configuration.
|
||||
+- **Win Detection**: Construct a state where all but 1 card is homed, verify final homing triggers winning state flag.
|
||||
+
|
||||
+### 6.2 Curses Integration Tests
|
||||
+For the UI layer, tests can mock the terminal dimensions and standard input keys using Python's `unittest.mock`:
|
||||
+- Mock `curses.getch` and verify the cursor coordinates state shifts correctly.
|
||||
+- Verify screen boundary logic prevents cursor overflow out of valid coordinates (e.g. going left of FreeCell column 0 or below the lowest card in a Tableau).
|
||||
+
|
||||
+---
|
||||
+
|
||||
+## 7. Implementation Milestones
|
||||
+
|
||||
+1. **Milestone 1**: Implement pure-domain `Card`, `Deck`, `Position`, and `GameState` engines.
|
||||
+2. **Milestone 2**: Write unit tests proving full game validation logic and automated auto-homing.
|
||||
+3. **Milestone 3**: Implement `--smoke` mode in CLI entrypoint and verify CI execution.
|
||||
+4. **Milestone 4**: Setup curses framework window structure, color palettes, and draw static cards.
|
||||
+5. **Milestone 5**: Implement interactive keyboard cursor, card selection mechanics, and execution hook.
|
||||
+6. **Milestone 6**: Complete Undo-stack integration and terminal resize layout adjustment handler.
|
||||
diff --git a/status.json b/status.json
|
||||
new file mode 100644
|
||||
index 000000000..ee6907d7f
|
||||
--- /dev/null
|
||||
+++ b/status.json
|
||||
@@ -0,0 +1,3 @@
|
||||
+{
|
||||
+ "outcome": "succeeded"
|
||||
+}
|
||||
\ No newline at end of file
|
||||
6
stages/002-expand_spec@1/status.json
Normal file
6
stages/002-expand_spec@1/status.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"outcome": "succeeded",
|
||||
"notes": "Stage completed: expand_spec",
|
||||
"failure_reason": null,
|
||||
"timestamp": "2026-06-04T18:43:25.384693Z"
|
||||
}
|
||||
23
stages/003-impl_setup@1/prompt.md
Normal file
23
stages/003-impl_setup@1/prompt.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
Goal: Build a terminal-based FreeCell solitaire game in Python
|
||||
|
||||
## Completed stages
|
||||
- **expand_spec**: succeeded
|
||||
- Model: gemini-3.5-flash, 85.9k tokens in / 8.3k out
|
||||
- Files: /home/daytona/workspace/fabro/.ai/card-game-spec.md, /home/daytona/workspace/fabro/status.json
|
||||
|
||||
|
||||
Read .ai/card-game-spec.md.
|
||||
|
||||
Create the Python project skeleton under card-game-app/:
|
||||
- pyproject.toml with pytest configured
|
||||
- main.py entrypoint
|
||||
- src/card_game_tui/ package
|
||||
- tests/ directory
|
||||
- README.md stub
|
||||
|
||||
Add minimal importable modules so the project compiles.
|
||||
|
||||
Run:
|
||||
cd card-game-app && python3 -m py_compile main.py src/card_game_tui/*.py
|
||||
|
||||
Write status.json at workspace root: outcome=succeeded if the project skeleton exists and compiles, outcome=failed with failure_reason otherwise.
|
||||
5
stages/003-impl_setup@1/provider_used.json
Normal file
5
stages/003-impl_setup@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