mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
parent
8b71e8ffe1
commit
28f1dd6c3f
6 changed files with 1253 additions and 61 deletions
628
run.json
628
run.json
File diff suppressed because one or more lines are too long
630
stages/006-impl_data@1/diff.patch
Normal file
630
stages/006-impl_data@1/diff.patch
Normal file
|
|
@ -0,0 +1,630 @@
|
|||
diff --git a/card-game-app/src/card_game_tui/domain.py b/card-game-app/src/card_game_tui/domain.py
|
||||
index db4a2fa8f..fe733cef3 100644
|
||||
--- a/card-game-app/src/card_game_tui/domain.py
|
||||
+++ b/card-game-app/src/card_game_tui/domain.py
|
||||
@@ -1,5 +1,5 @@
|
||||
import random
|
||||
-from dataclasses import dataclass
|
||||
+from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
from typing import List, Union, Dict
|
||||
|
||||
@@ -84,6 +84,10 @@ class MoveRecord:
|
||||
cards: List[Card] # Captured for single or sequence moves
|
||||
auto_moves: List['MoveRecord'] = None # Nested moves triggered by auto-homing
|
||||
|
||||
+ def __post_init__(self):
|
||||
+ if self.auto_moves is None:
|
||||
+ self.auto_moves = []
|
||||
+
|
||||
class GameState:
|
||||
def __init__(self, seed: int = None):
|
||||
self.tableaus: List[List[Card]] = Deck(seed).deal()
|
||||
@@ -107,7 +111,108 @@ class GameState:
|
||||
"""
|
||||
Calculates whether moving 'count' cards from from_pos to to_pos is legal.
|
||||
"""
|
||||
- # Placeholder for validation
|
||||
+ if count < 1:
|
||||
+ return False
|
||||
+
|
||||
+ if from_pos == to_pos:
|
||||
+ return False
|
||||
+
|
||||
+ # Validate index boundaries
|
||||
+ if from_pos.type == LocationType.TABLEAU:
|
||||
+ if not (0 <= from_pos.index < 8):
|
||||
+ return False
|
||||
+ elif from_pos.type == LocationType.FREECELL:
|
||||
+ if not (0 <= from_pos.index < 4):
|
||||
+ return False
|
||||
+ if count != 1:
|
||||
+ return False
|
||||
+ elif from_pos.type == LocationType.FOUNDATION:
|
||||
+ return False
|
||||
+ else:
|
||||
+ return False
|
||||
+
|
||||
+ if to_pos.type == LocationType.TABLEAU:
|
||||
+ if not (0 <= to_pos.index < 8):
|
||||
+ return False
|
||||
+ elif to_pos.type == LocationType.FREECELL:
|
||||
+ if not (0 <= to_pos.index < 4):
|
||||
+ return False
|
||||
+ if count != 1:
|
||||
+ return False
|
||||
+ elif to_pos.type == LocationType.FOUNDATION:
|
||||
+ if not (0 <= to_pos.index < 4):
|
||||
+ return False
|
||||
+ if count != 1:
|
||||
+ return False
|
||||
+ else:
|
||||
+ return False
|
||||
+
|
||||
+ # Extract moving cards
|
||||
+ if from_pos.type == LocationType.FREECELL:
|
||||
+ card = self.freecells[from_pos.index]
|
||||
+ if card is None:
|
||||
+ return False
|
||||
+ moving_cards = [card]
|
||||
+ elif from_pos.type == LocationType.TABLEAU:
|
||||
+ col = self.tableaus[from_pos.index]
|
||||
+ if len(col) < count:
|
||||
+ return False
|
||||
+ moving_cards = col[-count:]
|
||||
+ else:
|
||||
+ return False
|
||||
+
|
||||
+ # Validate sequence if moving multiple cards
|
||||
+ if count > 1:
|
||||
+ for i in range(count - 1):
|
||||
+ c1 = moving_cards[i]
|
||||
+ c2 = moving_cards[i+1]
|
||||
+ if c2.rank.value != c1.rank.value - 1:
|
||||
+ return False
|
||||
+ if c2.suit.color == c1.suit.color:
|
||||
+ return False
|
||||
+
|
||||
+ # Validate destination constraints
|
||||
+ if to_pos.type == LocationType.FREECELL:
|
||||
+ if self.freecells[to_pos.index] is not None:
|
||||
+ return False
|
||||
+
|
||||
+ elif to_pos.type == LocationType.FOUNDATION:
|
||||
+ target_suit = list(Suit)[to_pos.index]
|
||||
+ card = moving_cards[0]
|
||||
+ if card.suit != target_suit:
|
||||
+ return False
|
||||
+
|
||||
+ foundation_pile = self.foundations[target_suit]
|
||||
+ if card.rank == Rank.ACE:
|
||||
+ if len(foundation_pile) != 0:
|
||||
+ return False
|
||||
+ else:
|
||||
+ if len(foundation_pile) != card.rank.value - 1:
|
||||
+ return False
|
||||
+
|
||||
+ elif to_pos.type == LocationType.TABLEAU:
|
||||
+ dest_col = self.tableaus[to_pos.index]
|
||||
+ first_moving_card = moving_cards[0]
|
||||
+ if len(dest_col) > 0:
|
||||
+ dest_top_card = dest_col[-1]
|
||||
+ if first_moving_card.rank.value != dest_top_card.rank.value - 1:
|
||||
+ return False
|
||||
+ if first_moving_card.suit.color == dest_top_card.suit.color:
|
||||
+ return False
|
||||
+
|
||||
+ # Capacity constraint
|
||||
+ empty_freecells = sum(1 for c in self.freecells if c is None)
|
||||
+ empty_tableaus = sum(1 for col in self.tableaus if len(col) == 0)
|
||||
+
|
||||
+ if len(dest_col) == 0:
|
||||
+ transit_empty_tableaus = max(0, empty_tableaus - 1)
|
||||
+ else:
|
||||
+ transit_empty_tableaus = empty_tableaus
|
||||
+
|
||||
+ max_cards = (1 + empty_freecells) * (2 ** transit_empty_tableaus)
|
||||
+ if count > max_cards:
|
||||
+ return False
|
||||
+
|
||||
return True
|
||||
|
||||
def execute_move(self, from_pos: Position, to_pos: Position, count: int = 1) -> bool:
|
||||
@@ -116,12 +221,156 @@ class GameState:
|
||||
"""
|
||||
if not self.validate_move(from_pos, to_pos, count):
|
||||
return False
|
||||
+
|
||||
+ # Extract moving cards
|
||||
+ if from_pos.type == LocationType.FREECELL:
|
||||
+ moving_cards = [self.freecells[from_pos.index]]
|
||||
+ elif from_pos.type == LocationType.TABLEAU:
|
||||
+ moving_cards = self.tableaus[from_pos.index][-count:]
|
||||
+ else:
|
||||
+ return False
|
||||
+
|
||||
+ move_record = MoveRecord(from_pos=from_pos, to_pos=to_pos, cards=moving_cards)
|
||||
+
|
||||
+ # Apply the move
|
||||
+ self._apply_single_move(move_record)
|
||||
+
|
||||
+ # Run auto-homing
|
||||
+ self._run_auto_homing(move_record)
|
||||
+
|
||||
+ # Record on stacks
|
||||
+ self.undo_stack.append(move_record)
|
||||
+ self.redo_stack.clear()
|
||||
+ return True
|
||||
+
|
||||
+ def _apply_single_move(self, record: MoveRecord):
|
||||
+ # Remove from from_pos
|
||||
+ if record.from_pos.type == LocationType.FREECELL:
|
||||
+ self.freecells[record.from_pos.index] = None
|
||||
+ elif record.from_pos.type == LocationType.TABLEAU:
|
||||
+ count = len(record.cards)
|
||||
+ self.tableaus[record.from_pos.index] = self.tableaus[record.from_pos.index][:-count]
|
||||
+
|
||||
+ # Put to to_pos
|
||||
+ if record.to_pos.type == LocationType.FREECELL:
|
||||
+ self.freecells[record.to_pos.index] = record.cards[0]
|
||||
+ elif record.to_pos.type == LocationType.FOUNDATION:
|
||||
+ target_suit = list(Suit)[record.to_pos.index]
|
||||
+ self.foundations[target_suit].append(record.cards[0])
|
||||
+ elif record.to_pos.type == LocationType.TABLEAU:
|
||||
+ self.tableaus[record.to_pos.index].extend(record.cards)
|
||||
+
|
||||
+ def _revert_single_move(self, record: MoveRecord):
|
||||
+ # Remove from to_pos
|
||||
+ if record.to_pos.type == LocationType.FREECELL:
|
||||
+ self.freecells[record.to_pos.index] = None
|
||||
+ elif record.to_pos.type == LocationType.FOUNDATION:
|
||||
+ target_suit = list(Suit)[record.to_pos.index]
|
||||
+ self.foundations[target_suit].pop()
|
||||
+ elif record.to_pos.type == LocationType.TABLEAU:
|
||||
+ count = len(record.cards)
|
||||
+ self.tableaus[record.to_pos.index] = self.tableaus[record.to_pos.index][:-count]
|
||||
+
|
||||
+ # Put to from_pos
|
||||
+ if record.from_pos.type == LocationType.FREECELL:
|
||||
+ self.freecells[record.from_pos.index] = record.cards[0]
|
||||
+ elif record.from_pos.type == LocationType.TABLEAU:
|
||||
+ self.tableaus[record.from_pos.index].extend(record.cards)
|
||||
+
|
||||
+ def _should_auto_home(self, card: Card) -> bool:
|
||||
+ """
|
||||
+ Checks if a card can be safely auto-homed to the foundations.
|
||||
+ """
|
||||
+ target_foundation = self.foundations[card.suit]
|
||||
+ if card.rank.value != len(target_foundation) + 1:
|
||||
+ return False
|
||||
+
|
||||
+ # Opposite color suits must have all lower cards in the foundations
|
||||
+ if card.suit.color == "RED":
|
||||
+ opp_suits = [Suit.CLUBS, Suit.SPADES]
|
||||
+ else:
|
||||
+ opp_suits = [Suit.HEARTS, Suit.DIAMONDS]
|
||||
+
|
||||
+ for s_opp in opp_suits:
|
||||
+ if len(self.foundations[s_opp]) < card.rank.value - 1:
|
||||
+ return False
|
||||
+
|
||||
return True
|
||||
|
||||
+ def _run_auto_homing(self, move_record: MoveRecord):
|
||||
+ """
|
||||
+ Scans freecells and tableau tops, moving eligible cards to foundations.
|
||||
+ Repeats until no more cards can be auto-homed.
|
||||
+ """
|
||||
+ any_homed = True
|
||||
+ while any_homed:
|
||||
+ any_homed = False
|
||||
+
|
||||
+ # Check free cells
|
||||
+ for i, card in enumerate(self.freecells):
|
||||
+ if card is not None and self._should_auto_home(card):
|
||||
+ from_p = Position(LocationType.FREECELL, i)
|
||||
+ suit_idx = list(Suit).index(card.suit)
|
||||
+ to_p = Position(LocationType.FOUNDATION, suit_idx)
|
||||
+
|
||||
+ auto_rec = MoveRecord(from_pos=from_p, to_pos=to_p, cards=[card])
|
||||
+ self._apply_single_move(auto_rec)
|
||||
+ move_record.auto_moves.append(auto_rec)
|
||||
+
|
||||
+ any_homed = True
|
||||
+ break
|
||||
+
|
||||
+ if any_homed:
|
||||
+ continue
|
||||
+
|
||||
+ # Check tableaus
|
||||
+ for i, col in enumerate(self.tableaus):
|
||||
+ if col:
|
||||
+ card = col[-1]
|
||||
+ if self._should_auto_home(card):
|
||||
+ from_p = Position(LocationType.TABLEAU, i)
|
||||
+ suit_idx = list(Suit).index(card.suit)
|
||||
+ to_p = Position(LocationType.FOUNDATION, suit_idx)
|
||||
+
|
||||
+ auto_rec = MoveRecord(from_pos=from_p, to_pos=to_p, cards=[card])
|
||||
+ self._apply_single_move(auto_rec)
|
||||
+ move_record.auto_moves.append(auto_rec)
|
||||
+
|
||||
+ any_homed = True
|
||||
+ break
|
||||
+
|
||||
def undo(self) -> bool:
|
||||
"""Reverts the last move, including nested auto-homing steps."""
|
||||
if not self.undo_stack:
|
||||
return False
|
||||
+
|
||||
+ move_record = self.undo_stack.pop()
|
||||
+
|
||||
+ # Revert auto moves in reverse order
|
||||
+ for auto_move in reversed(move_record.auto_moves):
|
||||
+ self._revert_single_move(auto_move)
|
||||
+
|
||||
+ # Revert the main move
|
||||
+ self._revert_single_move(move_record)
|
||||
+
|
||||
+ self.redo_stack.append(move_record)
|
||||
+ return True
|
||||
+
|
||||
+ def redo(self) -> bool:
|
||||
+ """Reapplies the last undone move."""
|
||||
+ if not self.redo_stack:
|
||||
+ return False
|
||||
+
|
||||
+ move_record = self.redo_stack.pop()
|
||||
+
|
||||
+ # Apply the main move
|
||||
+ self._apply_single_move(move_record)
|
||||
+
|
||||
+ # Re-apply auto moves
|
||||
+ for auto_move in move_record.auto_moves:
|
||||
+ self._apply_single_move(auto_move)
|
||||
+
|
||||
+ self.undo_stack.append(move_record)
|
||||
return True
|
||||
|
||||
def check_win(self) -> bool:
|
||||
diff --git a/card-game-app/tests/test_domain.py b/card-game-app/tests/test_domain.py
|
||||
index 3ddb4344b..0fa4360ed 100644
|
||||
--- a/card-game-app/tests/test_domain.py
|
||||
+++ b/card-game-app/tests/test_domain.py
|
||||
@@ -1,3 +1,4 @@
|
||||
+import copy
|
||||
from card_game_tui.domain import GameState, Suit, Rank, Card, Deck, LocationType, Position
|
||||
|
||||
def test_deck_deals_52_cards():
|
||||
@@ -18,3 +19,331 @@ def test_get_card_at():
|
||||
card = state.get_card_at(pos)
|
||||
assert card is not None
|
||||
assert isinstance(card, Card)
|
||||
+
|
||||
+def test_deck_deterministic_seeding():
|
||||
+ deck1 = Deck(seed=42)
|
||||
+ deck2 = Deck(seed=42)
|
||||
+ assert deck1.cards == deck2.cards
|
||||
+
|
||||
+ # Verify uniqueness of 52 cards
|
||||
+ assert len(set(deck1.cards)) == 52
|
||||
+
|
||||
+def test_moves_to_free_cells():
|
||||
+ state = GameState()
|
||||
+ state.tableaus = [[] for _ in range(8)]
|
||||
+ state.freecells = [None] * 4
|
||||
+ state.foundations = {suit: [] for suit in Suit}
|
||||
+
|
||||
+ # Put a card in Tableau 0
|
||||
+ c_hearts = Card(Suit.HEARTS, Rank.TEN)
|
||||
+ state.tableaus[0] = [c_hearts]
|
||||
+
|
||||
+ from_pos = Position(LocationType.TABLEAU, 0)
|
||||
+ to_pos = Position(LocationType.FREECELL, 0)
|
||||
+
|
||||
+ # Valid single card move to empty freecell
|
||||
+ assert state.validate_move(from_pos, to_pos, count=1) is True
|
||||
+
|
||||
+ # Try invalid sequence count to free cell
|
||||
+ assert state.validate_move(from_pos, to_pos, count=2) is False
|
||||
+
|
||||
+ # Execute move
|
||||
+ assert state.execute_move(from_pos, to_pos, count=1) is True
|
||||
+ assert state.freecells[0] == c_hearts
|
||||
+ assert len(state.tableaus[0]) == 0
|
||||
+
|
||||
+ # Try moving to occupied freecell
|
||||
+ state.tableaus[0] = [Card(Suit.SPADES, Rank.FIVE)]
|
||||
+ assert state.validate_move(from_pos, to_pos, count=1) is False
|
||||
+
|
||||
+ # Try moving to invalid freecell index
|
||||
+ invalid_to_pos = Position(LocationType.FREECELL, 4)
|
||||
+ assert state.validate_move(from_pos, invalid_to_pos, count=1) is False
|
||||
+
|
||||
+def test_moves_to_foundations():
|
||||
+ state = GameState()
|
||||
+ state.tableaus = [[] for _ in range(8)]
|
||||
+ state.freecells = [None] * 4
|
||||
+ state.foundations = {suit: [] for suit in Suit}
|
||||
+
|
||||
+ ace_hearts = Card(Suit.HEARTS, Rank.ACE)
|
||||
+ two_hearts = Card(Suit.HEARTS, Rank.TWO)
|
||||
+ three_hearts = Card(Suit.HEARTS, Rank.THREE)
|
||||
+ ace_diamonds = Card(Suit.DIAMONDS, Rank.ACE)
|
||||
+
|
||||
+ state.tableaus[0] = [ace_hearts]
|
||||
+ state.tableaus[1] = [two_hearts]
|
||||
+ state.tableaus[2] = [three_hearts]
|
||||
+ state.tableaus[3] = [ace_diamonds]
|
||||
+
|
||||
+ # Move Ace of Hearts to index 0 (HEARTS) foundation
|
||||
+ # list(Suit) indices: 0: HEARTS, 1: DIAMONDS, 2: CLUBS, 3: SPADES
|
||||
+ hearts_found = Position(LocationType.FOUNDATION, 0)
|
||||
+ diamonds_found = Position(LocationType.FOUNDATION, 1)
|
||||
+
|
||||
+ # Valid Ace to empty foundation
|
||||
+ assert state.validate_move(Position(LocationType.TABLEAU, 0), hearts_found, count=1) is True
|
||||
+ # Invalid non-Ace to empty foundation
|
||||
+ assert state.validate_move(Position(LocationType.TABLEAU, 1), hearts_found, count=1) is False
|
||||
+ # Invalid Ace to wrong suit foundation
|
||||
+ assert state.validate_move(Position(LocationType.TABLEAU, 3), hearts_found, count=1) is False
|
||||
+
|
||||
+ # Execute valid Ace move
|
||||
+ assert state.execute_move(Position(LocationType.TABLEAU, 0), hearts_found, count=1) is True
|
||||
+ assert state.foundations[Suit.HEARTS] == [ace_hearts]
|
||||
+
|
||||
+ # Now Two of Hearts is on Tableau 1. Can it move to Hearts foundation? Yes.
|
||||
+ assert state.validate_move(Position(LocationType.TABLEAU, 1), hearts_found, count=1) is True
|
||||
+ # Can Three of Hearts move? No, since it needs Two of Hearts first.
|
||||
+ assert state.validate_move(Position(LocationType.TABLEAU, 2), hearts_found, count=1) is False
|
||||
+
|
||||
+def test_moves_to_tableau_single_card():
|
||||
+ state = GameState()
|
||||
+ state.tableaus = [[] for _ in range(8)]
|
||||
+ state.freecells = [None] * 4
|
||||
+ state.foundations = {suit: [] for suit in Suit}
|
||||
+
|
||||
+ # Red 8 (Hearts) on Black 9 (Spades)
|
||||
+ c_red8 = Card(Suit.HEARTS, Rank.EIGHT)
|
||||
+ c_black9 = Card(Suit.SPADES, Rank.NINE)
|
||||
+
|
||||
+ state.tableaus[0] = [c_red8]
|
||||
+ state.tableaus[1] = [c_black9]
|
||||
+
|
||||
+ # Test moving Red 8 onto Black 9
|
||||
+ assert state.validate_move(Position(LocationType.TABLEAU, 0), Position(LocationType.TABLEAU, 1), count=1) is True
|
||||
+
|
||||
+ # Test same color invalid move: Red 8 (Hearts) on Red 9 (Diamonds)
|
||||
+ c_red9 = Card(Suit.DIAMONDS, Rank.NINE)
|
||||
+ state.tableaus[1] = [c_red9]
|
||||
+ assert state.validate_move(Position(LocationType.TABLEAU, 0), Position(LocationType.TABLEAU, 1), count=1) is False
|
||||
+
|
||||
+ # Test wrong rank invalid move: Red 8 on Black 10 (Spades)
|
||||
+ c_black10 = Card(Suit.SPADES, Rank.TEN)
|
||||
+ state.tableaus[1] = [c_black10]
|
||||
+ assert state.validate_move(Position(LocationType.TABLEAU, 0), Position(LocationType.TABLEAU, 1), count=1) is False
|
||||
+
|
||||
+ # Test moving to empty tableau
|
||||
+ state.tableaus[1] = []
|
||||
+ assert state.validate_move(Position(LocationType.TABLEAU, 0), Position(LocationType.TABLEAU, 1), count=1) is True
|
||||
+
|
||||
+def test_sequence_moves_tableau_to_tableau():
|
||||
+ state = GameState()
|
||||
+ state.tableaus = [[] for _ in range(8)]
|
||||
+ state.freecells = [None] * 4
|
||||
+ state.foundations = {suit: [] for suit in Suit}
|
||||
+
|
||||
+ # Red 10, Black 9, Red 8, Black 7, Red 6
|
||||
+ c10 = Card(Suit.HEARTS, Rank.TEN)
|
||||
+ c9 = Card(Suit.SPADES, Rank.NINE)
|
||||
+ c8 = Card(Suit.DIAMONDS, Rank.EIGHT)
|
||||
+ c7 = Card(Suit.CLUBS, Rank.SEVEN)
|
||||
+ c6 = Card(Suit.HEARTS, Rank.SIX)
|
||||
+
|
||||
+ state.tableaus[0] = [c10, c9, c8, c7, c6]
|
||||
+ state.tableaus[1] = [Card(Suit.SPADES, Rank.JACK)]
|
||||
+
|
||||
+ # 4 empty freecells, 6 empty tableaus.
|
||||
+ # Exclude destination: no, destination is Col 1 which has 1 card (not empty).
|
||||
+ # Transit empty tableaus = 6
|
||||
+ # Max cards = (1 + 4) * 2^6 = 5 * 64 = 320 cards.
|
||||
+ # Moving 5 cards should be valid!
|
||||
+ from_pos = Position(LocationType.TABLEAU, 0)
|
||||
+ to_pos = Position(LocationType.TABLEAU, 1)
|
||||
+ assert state.validate_move(from_pos, to_pos, count=5) is True
|
||||
+
|
||||
+ # Restrict capacity
|
||||
+ state.freecells = [Card(Suit.CLUBS, Rank.TWO)] * 4 # 0 empty freecells
|
||||
+ for i in range(2, 8):
|
||||
+ state.tableaus[i] = [Card(Suit.DIAMONDS, Rank.KING)] # 0 empty tableaus
|
||||
+ # Now empty freecells = 0, empty tableaus = 0
|
||||
+ # Max cards = (1 + 0) * 2^0 = 1
|
||||
+ # Moving 5 cards should be invalid
|
||||
+ assert state.validate_move(from_pos, to_pos, count=5) is False
|
||||
+
|
||||
+def test_sequence_moves_to_empty_tableau():
|
||||
+ state = GameState()
|
||||
+ state.tableaus = [[] for _ in range(8)]
|
||||
+ state.freecells = [None] * 4
|
||||
+ state.foundations = {suit: [] for suit in Suit}
|
||||
+
|
||||
+ # Sequence of 3 cards on Col 0: Red 10, Black 9, Red 8
|
||||
+ c10 = Card(Suit.HEARTS, Rank.TEN)
|
||||
+ c9 = Card(Suit.SPADES, Rank.NINE)
|
||||
+ c8 = Card(Suit.DIAMONDS, Rank.EIGHT)
|
||||
+ state.tableaus[0] = [c10, c9, c8]
|
||||
+
|
||||
+ # Destination is Col 1 (empty)
|
||||
+ # We want to move 2 cards: Black 9, Red 8
|
||||
+ # Capacity constraint:
|
||||
+ # Destination Col 1 is empty, so transit_empty_tableaus = empty_tableaus - 1.
|
||||
+ # Empty tableaus before move = 7.
|
||||
+ # Transit empty tableaus = 6.
|
||||
+ # Empty free cells = 4.
|
||||
+ # Max cards = (1 + 4) * 2^6 = 320.
|
||||
+ from_pos = Position(LocationType.TABLEAU, 0)
|
||||
+ to_pos = Position(LocationType.TABLEAU, 1)
|
||||
+ assert state.validate_move(from_pos, to_pos, count=2) is True
|
||||
+
|
||||
+ # Restrict capacity so Max Cards is exactly 1
|
||||
+ state.freecells = [Card(Suit.CLUBS, Rank.TWO)] * 4 # 0 empty free cells
|
||||
+ for i in range(2, 8):
|
||||
+ state.tableaus[i] = [Card(Suit.DIAMONDS, Rank.KING)] # 0 transit empty tableaus
|
||||
+ # Empty freecells = 0, empty tableaus = 1 (Col 1).
|
||||
+ # Since Col 1 is the destination, transit_empty_tableaus = 1 - 1 = 0.
|
||||
+ # Max cards = (1 + 0) * 2^0 = 1.
|
||||
+ # Moving 2 cards should be invalid, but 1 card should be valid.
|
||||
+ assert state.validate_move(from_pos, to_pos, count=2) is False
|
||||
+ assert state.validate_move(from_pos, to_pos, count=1) is True
|
||||
+
|
||||
+def test_auto_homing_logic():
|
||||
+ state = GameState()
|
||||
+ state.tableaus = [[] for _ in range(8)]
|
||||
+ state.freecells = [None] * 4
|
||||
+ state.foundations = {suit: [] for suit in Suit}
|
||||
+
|
||||
+ # Hearts is RED. Opposite suits are CLUBS and SPADES.
|
||||
+ # Put Ace of Hearts in Tableau 0.
|
||||
+ state.tableaus[0] = [Card(Suit.HEARTS, Rank.ACE)]
|
||||
+ # Put King of Spades in Tableau 1 to use as a dummy move trigger.
|
||||
+ state.tableaus[1] = [Card(Suit.SPADES, Rank.KING)]
|
||||
+
|
||||
+ assert len(state.foundations[Suit.HEARTS]) == 0
|
||||
+
|
||||
+ # Execute move of King of Spades to FreeCell 0. This should trigger auto-homing of Ace of Hearts.
|
||||
+ from_pos = Position(LocationType.TABLEAU, 1)
|
||||
+ to_pos = Position(LocationType.FREECELL, 0)
|
||||
+ success = state.execute_move(from_pos, to_pos, count=1)
|
||||
+ assert success is True
|
||||
+
|
||||
+ assert state.freecells[0] == Card(Suit.SPADES, Rank.KING)
|
||||
+ assert len(state.tableaus[0]) == 0
|
||||
+ assert len(state.foundations[Suit.HEARTS]) == 1
|
||||
+ assert state.foundations[Suit.HEARTS][0] == Card(Suit.HEARTS, Rank.ACE)
|
||||
+
|
||||
+ # Two of Hearts is put in Tableau 0.
|
||||
+ # Foundations: Hearts has Ace. CLUBS has [], SPADES has [].
|
||||
+ # Opposite color (Black) lower cards are Ace of Clubs and Ace of Spades.
|
||||
+ # Since they are NOT in the foundations, Two of Hearts should NOT auto-home.
|
||||
+ state.tableaus[0] = [Card(Suit.HEARTS, Rank.TWO)]
|
||||
+
|
||||
+ # Move King of Spades from FreeCell 0 to Tableau 1 to trigger auto-homing checks
|
||||
+ from_pos = Position(LocationType.FREECELL, 0)
|
||||
+ to_pos = Position(LocationType.TABLEAU, 1)
|
||||
+ success = state.execute_move(from_pos, to_pos, count=1)
|
||||
+ assert success is True
|
||||
+
|
||||
+ # Two of Hearts should still be in Tableau 0 (not auto-homed)
|
||||
+ assert len(state.tableaus[0]) == 1
|
||||
+ assert state.tableaus[0][0] == Card(Suit.HEARTS, Rank.TWO)
|
||||
+ assert len(state.foundations[Suit.HEARTS]) == 1
|
||||
+
|
||||
+ # Place Ace of Clubs and Ace of Spades in their foundations.
|
||||
+ state.foundations[Suit.CLUBS] = [Card(Suit.CLUBS, Rank.ACE)]
|
||||
+ state.foundations[Suit.SPADES] = [Card(Suit.SPADES, Rank.ACE)]
|
||||
+
|
||||
+ # Move King of Spades back to FreeCell 0 to trigger auto-homing checks
|
||||
+ from_pos = Position(LocationType.TABLEAU, 1)
|
||||
+ to_pos = Position(LocationType.FREECELL, 0)
|
||||
+ success = state.execute_move(from_pos, to_pos, count=1)
|
||||
+ assert success is True
|
||||
+
|
||||
+ # Two of Hearts should now have auto-homed!
|
||||
+ assert len(state.tableaus[0]) == 0
|
||||
+ assert len(state.foundations[Suit.HEARTS]) == 2
|
||||
+ assert state.foundations[Suit.HEARTS][1] == Card(Suit.HEARTS, Rank.TWO)
|
||||
+
|
||||
+def test_undo_redo_system():
|
||||
+ state = GameState()
|
||||
+ state.tableaus = [[] for _ in range(8)]
|
||||
+ state.freecells = [None] * 4
|
||||
+ state.foundations = {suit: [] for suit in Suit}
|
||||
+
|
||||
+ # Col 0: [Red 10 (Hearts)]
|
||||
+ # Col 1: [Black J (Spades)]
|
||||
+ c10 = Card(Suit.HEARTS, Rank.TEN)
|
||||
+ cJ = Card(Suit.SPADES, Rank.JACK)
|
||||
+ state.tableaus[0] = [c10]
|
||||
+ state.tableaus[1] = [cJ]
|
||||
+
|
||||
+ # Save original state for verification
|
||||
+ orig_tableaus = copy.deepcopy(state.tableaus)
|
||||
+ orig_freecells = copy.deepcopy(state.freecells)
|
||||
+ orig_foundations = copy.deepcopy(state.foundations)
|
||||
+
|
||||
+ # 1. Execute a move
|
||||
+ from_pos = Position(LocationType.TABLEAU, 0)
|
||||
+ to_pos = Position(LocationType.TABLEAU, 1)
|
||||
+ success = state.execute_move(from_pos, to_pos, count=1)
|
||||
+ assert success is True
|
||||
+
|
||||
+ assert state.tableaus[0] == []
|
||||
+ assert state.tableaus[1] == [cJ, c10]
|
||||
+
|
||||
+ # 2. Undo the move
|
||||
+ undo_success = state.undo()
|
||||
+ assert undo_success is True
|
||||
+ assert state.tableaus == orig_tableaus
|
||||
+ assert state.freecells == orig_freecells
|
||||
+ assert state.foundations == orig_foundations
|
||||
+
|
||||
+ # 3. Redo the move
|
||||
+ redo_success = state.redo()
|
||||
+ assert redo_success is True
|
||||
+ assert state.tableaus[0] == []
|
||||
+ assert state.tableaus[1] == [cJ, c10]
|
||||
+
|
||||
+ # 4. Undo again to restore
|
||||
+ assert state.undo() is True
|
||||
+ assert state.tableaus == orig_tableaus
|
||||
+
|
||||
+def test_undo_with_auto_homing():
|
||||
+ state = GameState()
|
||||
+ state.tableaus = [[] for _ in range(8)]
|
||||
+ state.freecells = [None] * 4
|
||||
+ state.foundations = {suit: [] for suit in Suit}
|
||||
+
|
||||
+ # Tableau 0: Ace of Hearts
|
||||
+ # Tableau 1: King of Spades
|
||||
+ # Move King of Spades to FreeCell 0. This will trigger auto-homing of Ace of Hearts to foundation.
|
||||
+ state.tableaus[0] = [Card(Suit.HEARTS, Rank.ACE)]
|
||||
+ state.tableaus[1] = [Card(Suit.SPADES, Rank.KING)]
|
||||
+
|
||||
+ orig_tableaus = copy.deepcopy(state.tableaus)
|
||||
+ orig_freecells = copy.deepcopy(state.freecells)
|
||||
+ orig_foundations = copy.deepcopy(state.foundations)
|
||||
+
|
||||
+ from_pos = Position(LocationType.TABLEAU, 1)
|
||||
+ to_pos = Position(LocationType.FREECELL, 0)
|
||||
+ success = state.execute_move(from_pos, to_pos, count=1)
|
||||
+ assert success is True
|
||||
+
|
||||
+ # Check state after move and auto-homing
|
||||
+ assert state.freecells[0] == Card(Suit.SPADES, Rank.KING)
|
||||
+ assert len(state.tableaus[0]) == 0
|
||||
+ assert len(state.foundations[Suit.HEARTS]) == 1
|
||||
+
|
||||
+ # Undo the move! This should revert both King of Spades and Ace of Hearts!
|
||||
+ undo_success = state.undo()
|
||||
+ assert undo_success is True
|
||||
+ assert state.tableaus == orig_tableaus
|
||||
+ assert state.freecells == orig_freecells
|
||||
+ assert state.foundations == orig_foundations
|
||||
+
|
||||
+ # Redo the move!
|
||||
+ redo_success = state.redo()
|
||||
+ assert redo_success is True
|
||||
+ assert state.freecells[0] == Card(Suit.SPADES, Rank.KING)
|
||||
+ assert len(state.tableaus[0]) == 0
|
||||
+ assert len(state.foundations[Suit.HEARTS]) == 1
|
||||
+
|
||||
+def test_win_detection():
|
||||
+ state = GameState()
|
||||
+ # Fill foundations
|
||||
+ for suit in Suit:
|
||||
+ state.foundations[suit] = [Card(suit, rank) for rank in Rank]
|
||||
+ assert state.check_win() is True
|
||||
+
|
||||
+ # Remove one card
|
||||
+ state.foundations[Suit.HEARTS].pop()
|
||||
+ assert state.check_win() is False
|
||||
6
stages/006-impl_data@1/status.json
Normal file
6
stages/006-impl_data@1/status.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"outcome": "succeeded",
|
||||
"notes": "Stage completed: impl_data",
|
||||
"failure_reason": null,
|
||||
"timestamp": "2026-06-04T18:50:05.683371Z"
|
||||
}
|
||||
27
stages/007-verify_data@1/prompt.md
Normal file
27
stages/007-verify_data@1/prompt.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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
|
||||
- **impl_setup**: succeeded
|
||||
- Model: gemini-3.5-flash, 144.1k tokens in / 6.8k 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/domain.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_domain.py, /home/daytona/workspace/fabro/status.json
|
||||
- **verify_setup**: succeeded
|
||||
- Model: gemini-3.5-flash, 185.9k 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, 179.9k tokens in / 32.0k out
|
||||
- Files: /home/daytona/workspace/fabro/card-game-app/src/card_game_tui/domain.py, /home/daytona/workspace/fabro/card-game-app/tests/test_domain.py, /home/daytona/workspace/fabro/status.json
|
||||
|
||||
|
||||
Verify the card game data structures.
|
||||
|
||||
Run:
|
||||
cd card-game-app && python3 -m pytest tests/ -v && python3 -m py_compile main.py src/card_game_tui/*.py
|
||||
|
||||
Check that the core game-state types are defined and basic operations work.
|
||||
|
||||
Write findings to .ai/verify_data.md.
|
||||
Write status.json at workspace root: outcome=succeeded if all checks pass, outcome=failed with failure_reason otherwise.
|
||||
5
stages/007-verify_data@1/provider_used.json
Normal file
5
stages/007-verify_data@1/provider_used.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"mode": "agent",
|
||||
"provider": "gemini",
|
||||
"model": "gemini-3.5-flash"
|
||||
}
|
||||
18
stages/007-verify_data@1/response.md
Normal file
18
stages/007-verify_data@1/response.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
I have completed the verification process for the card game data structures in FreeCell Solitaire.
|
||||
|
||||
### Summary of Actions Taken:
|
||||
1. **Compilation & Tests Executed:** Ran tests and compiled python files successfully.
|
||||
- `python3 -m pytest tests/ -v` -> All 13 tests passed.
|
||||
- `python3 -m py_compile main.py src/card_game_tui/*.py` -> Compiled cleanly with zero syntax/import warnings.
|
||||
2. **Analysis of Core Types:** Inspected the data structures and validated:
|
||||
- **Enums:** `Suit`, `Rank`, and `LocationType`.
|
||||
- **Entities:** `Card` (frozen/immutable), `Position` (frozen), and `MoveRecord`.
|
||||
- **State Manager:** `GameState` containing Tableaus, FreeCells, Foundations, and full validation/execution of standard FreeCell logic.
|
||||
3. **Advanced Mechanics Verified:**
|
||||
- Alternate-color and descending-rank move validations.
|
||||
- Multi-card sequential moves with correct capacity limits based on empty free cells and empty tableaus.
|
||||
- Smart, cascading auto-homing that avoids locking cards required for lower ranks of opposing colors.
|
||||
- Linear undo/redo stack managing primary moves alongside automatic cascades.
|
||||
4. **Output Generation:**
|
||||
- Detailed findings recorded in `.ai/verify_data.md`.
|
||||
- `status.json` updated with `"outcome": "succeeded"`.
|
||||
Loading…
Add table
Reference in a new issue