mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
fabro(01KT9YT14FHDYA4VTV9DSFG0FY): impl_data (succeeded)
Fabro-Run: 01KT9YT14FHDYA4VTV9DSFG0FY
Fabro-Completed: 6
Fabro-Checkpoint: 8b71e8ffe1
⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
parent
b10c3badae
commit
f2b42d6439
2 changed files with 580 additions and 2 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue