fabro(01KT9YT14FHDYA4VTV9DSFG0FY): impl_setup (succeeded)

Fabro-Run: 01KT9YT14FHDYA4VTV9DSFG0FY
Fabro-Completed: 3
Fabro-Checkpoint: 74d93db932

⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
Fabro 2026-06-04 18:45:00 +00:00
parent dd57c4b4a0
commit 93beebfa44
9 changed files with 268 additions and 1 deletions

32
card-game-app/README.md Normal file
View file

@ -0,0 +1,32 @@
# FreeCell Solitaire TUI
A terminal-based FreeCell Solitaire game built in Python using the `curses` standard library.
## Project Structure
- `main.py`: CLI entrypoint.
- `src/card_game_tui/`: Core package.
- `domain.py`: Game rules, logic, state, and card representations (fully decoupled from UI).
- `tui.py`: Curses-based TUI and interface logic.
- `tests/`: Automated unit and integration tests.
## Installation & Running
This project requires Python 3.10+ and no external dependencies for core play.
To run:
```bash
python3 main.py
```
For smoke-test mode:
```bash
python3 main.py --smoke
```
## Running Tests
To run tests:
```bash
pytest
```

57
card-game-app/main.py Normal file
View file

@ -0,0 +1,57 @@
#!/usr/bin/env python3
import sys
import os
# Add src to python path to make importing easier
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'src')))
from card_game_tui.domain import GameState, Position, LocationType
def run_smoke_test() -> int:
print("Running non-interactive smoke test probe...")
try:
# 1. Initialize GameState with a deterministic seed
seed = 42
state = GameState(seed=seed)
print(f"Initialized GameState with seed {seed}")
# 2. Basic assertions to verify components compile and behave as expected
# Let's verify we have 8 tableau columns
if len(state.tableaus) != 8:
print(f"Error: Expected 8 tableaus, found {len(state.tableaus)}", file=sys.stderr)
return 1
# 3. Validate a mock move
from_pos = Position(LocationType.TABLEAU, 0)
to_pos = Position(LocationType.FREECELL, 0)
is_valid = state.validate_move(from_pos, to_pos)
print(f"Validating move from Tableau 0 to FreeCell 0: {is_valid}")
success = state.execute_move(from_pos, to_pos)
print(f"Executed move from Tableau 0 to FreeCell 0: {success}")
# Check win state
is_win = state.check_win()
print(f"Win checked: {is_win}")
print("SMOKE TEST SUCCESSFUL")
return 0
except Exception as e:
print(f"Smoke test failed with error: {e}", file=sys.stderr)
return 1
def main():
if "--smoke" in sys.argv:
sys.exit(run_smoke_test())
else:
print("FreeCell Solitaire TUI")
print("To run the smoke test, execute with --smoke")
print("Starting interactive UI (Placeholder)...")
# In future milestones, this will invoke the curses UI
from card_game_tui.tui import TUIApp
app = TUIApp()
app.run()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,19 @@
[project]
name = "card-game-tui"
version = "0.1.0"
description = "A terminal-based FreeCell solitaire game in Python"
readme = "README.md"
requires-python = ">=3.10"
dependencies = []
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.pytest.ini_options]
minversion = "6.0"
addopts = "-ra -q"
testpaths = [
"tests",
]
pythonpath = ["src"]

View file

@ -0,0 +1 @@
# Card Game TUI Package

View file

@ -0,0 +1,129 @@
import random
from dataclasses import dataclass
from enum import Enum, auto
from typing import List, Union, Dict
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:
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}"
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
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
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.
"""
# Placeholder for validation
return True
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
return True
def undo(self) -> bool:
"""Reverts the last move, including nested auto-homing steps."""
if not self.undo_stack:
return False
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)

View file

@ -0,0 +1,8 @@
# TUI module placeholder for FreeCell Solitaire TUI
class TUIApp:
def __init__(self):
pass
def run(self) -> None:
print("TUI App placeholder running.")

View file

@ -0,0 +1 @@
# Tests package

View file

@ -0,0 +1,20 @@
from card_game_tui.domain import GameState, Suit, Rank, Card, Deck, LocationType, Position
def test_deck_deals_52_cards():
deck = Deck()
assert len(deck.cards) == 52
def test_game_state_initialization():
state = GameState()
assert len(state.tableaus) == 8
# 4 columns should have 7 cards, 4 should have 6 cards
lengths = [len(col) for col in state.tableaus]
assert sorted(lengths) == [6, 6, 6, 6, 7, 7, 7, 7]
def test_get_card_at():
state = GameState(seed=123)
# Check if we can get cards correctly
pos = Position(LocationType.TABLEAU, 0)
card = state.get_card_at(pos)
assert card is not None
assert isinstance(card, Card)

View file

@ -1,3 +1,3 @@
{
"outcome": "succeeded"
}
}