mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
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:
parent
dd57c4b4a0
commit
93beebfa44
9 changed files with 268 additions and 1 deletions
32
card-game-app/README.md
Normal file
32
card-game-app/README.md
Normal 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
57
card-game-app/main.py
Normal 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()
|
||||
19
card-game-app/pyproject.toml
Normal file
19
card-game-app/pyproject.toml
Normal 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"]
|
||||
1
card-game-app/src/card_game_tui/__init__.py
Normal file
1
card-game-app/src/card_game_tui/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Card Game TUI Package
|
||||
129
card-game-app/src/card_game_tui/domain.py
Normal file
129
card-game-app/src/card_game_tui/domain.py
Normal 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)
|
||||
8
card-game-app/src/card_game_tui/tui.py
Normal file
8
card-game-app/src/card_game_tui/tui.py
Normal 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.")
|
||||
1
card-game-app/tests/__init__.py
Normal file
1
card-game-app/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Tests package
|
||||
20
card-game-app/tests/test_domain.py
Normal file
20
card-game-app/tests/test_domain.py
Normal 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)
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"outcome": "succeeded"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue