feat(core): Step 2 — replace pile management with Session<Klondike>
Build and Deploy / build-and-push (push) Failing after 29s

- Delete rules.rs (228 lines) — move validation now handled by klondike engine
- Delete SolverState DFS from solver.rs (~900 lines) — replaced by session.solve()
- Rewrite GameState::new_with_mode() using Klondike::with_seed() (removes deck.rs dep)
- Rewrite move_cards/draw/undo to use Session<Klondike> as move executor
- Remove internal undo_stack (VecDeque<StateSnapshot>) — session owns history
- Sync piles from KlondikeState after each move via sync_piles_from_session()
- Update engine layer (game_plugin, input_plugin, card_plugin, etc.) to new API
- Net: 821 insertions, 3872 deletions (-3051 lines)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
funman300
2026-05-29 17:31:09 -07:00
parent d4796fa252
commit 6496e130f3
11 changed files with 840 additions and 3891 deletions
File diff suppressed because it is too large Load Diff
-1
View File
@@ -5,6 +5,5 @@ pub mod error;
pub mod game_state;
pub mod klondike_adapter;
pub mod pile;
pub mod rules;
pub mod scoring;
pub mod solver;
-228
View File
@@ -1,228 +0,0 @@
use crate::card::{Card, Rank};
use crate::pile::Pile;
/// Returns `true` if `card` can be placed on the foundation `pile`.
///
/// Foundation rules:
/// - When the pile is empty, any Ace is accepted; the placed Ace's suit
/// becomes the pile's claimed suit (derived from the bottom card via
/// [`Pile::claimed_suit`](crate::pile::Pile::claimed_suit)).
/// - When the pile is non-empty, the next card must match the top card's
/// suit and be exactly one rank higher.
#[must_use]
pub fn can_place_on_foundation(card: &Card, pile: &Pile) -> bool {
match pile.cards.last() {
None => card.rank == Rank::Ace,
Some(top) => card.suit == top.suit && card.rank.checked_sub(1) == Some(top.rank),
}
}
/// Returns `true` if `card` (or the bottom card of a sequence) can be placed on `pile` in the tableau.
///
/// Tableau rules: Kings go on empty piles; otherwise alternating colour, one rank lower.
#[must_use]
pub fn can_place_on_tableau(card: &Card, pile: &Pile) -> bool {
match pile.cards.last() {
None => card.rank == Rank::King,
Some(top) => {
top.face_up
&& card.rank.checked_add(1) == Some(top.rank)
&& card.suit.is_red() != top.suit.is_red()
}
}
}
/// Returns `true` if `cards` is a legal tableau run on its own — every
/// adjacent pair descends by one rank and alternates colour. A single
/// card is trivially valid. The destination check is separate; this
/// only validates the sequence's *internal* structure, which the tableau
/// move path must enforce so a player can't smuggle an arbitrary stack
/// onto another column when the bottom card happens to land legally.
#[must_use]
pub fn is_valid_tableau_sequence(cards: &[Card]) -> bool {
cards.windows(2).all(|w| {
w[0].rank.checked_sub(1) == Some(w[1].rank) && w[0].suit.is_red() != w[1].suit.is_red()
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::card::{Card, Rank, Suit};
use crate::pile::{Pile, PileType};
fn card(suit: Suit, rank: Rank) -> Card {
Card {
id: 0,
suit,
rank,
face_up: true,
}
}
fn pile_with(pile_type: PileType, cards: Vec<Card>) -> Pile {
Pile { pile_type, cards }
}
// Foundation tests
#[test]
fn foundation_ace_on_empty_is_valid() {
// Every suit's Ace must land on an empty foundation slot regardless of
// its slot index; the slot claims the suit only after the Ace lands.
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let c = card(suit, Rank::Ace);
let p = Pile::new(PileType::Foundation(0));
assert!(
can_place_on_foundation(&c, &p),
"Ace of {suit:?} must land on empty slot 0",
);
}
}
#[test]
fn foundation_non_ace_on_empty_is_invalid() {
let c = card(Suit::Hearts, Rank::Two);
let p = Pile::new(PileType::Foundation(0));
assert!(!can_place_on_foundation(&c, &p));
}
#[test]
fn foundation_two_on_ace_same_suit_is_valid() {
let c = card(Suit::Clubs, Rank::Two);
let p = pile_with(PileType::Foundation(0), vec![card(Suit::Clubs, Rank::Ace)]);
assert!(can_place_on_foundation(&c, &p));
}
#[test]
fn foundation_second_card_must_match_claimed_suit() {
// Place Ace of Hearts on slot 0, then attempt 2 of Spades — rejected
// because the slot's claimed suit is Hearts after the Ace lands.
let p = pile_with(PileType::Foundation(0), vec![card(Suit::Hearts, Rank::Ace)]);
let c = card(Suit::Spades, Rank::Two);
assert!(!can_place_on_foundation(&c, &p));
}
#[test]
fn foundation_skipping_rank_is_invalid() {
let c = card(Suit::Diamonds, Rank::Three);
let p = pile_with(
PileType::Foundation(0),
vec![card(Suit::Diamonds, Rank::Ace)],
);
assert!(!can_place_on_foundation(&c, &p));
}
// Tableau tests
#[test]
fn tableau_king_on_empty_is_valid() {
let c = card(Suit::Hearts, Rank::King);
let p = Pile::new(PileType::Tableau(0));
assert!(can_place_on_tableau(&c, &p));
}
#[test]
fn tableau_non_king_on_empty_is_invalid() {
let c = card(Suit::Hearts, Rank::Queen);
let p = Pile::new(PileType::Tableau(0));
assert!(!can_place_on_tableau(&c, &p));
}
#[test]
fn tableau_red_on_black_one_lower_is_valid() {
let c = card(Suit::Hearts, Rank::Nine);
let p = pile_with(PileType::Tableau(0), vec![card(Suit::Spades, Rank::Ten)]);
assert!(can_place_on_tableau(&c, &p));
}
#[test]
fn tableau_same_color_is_invalid() {
let c = card(Suit::Clubs, Rank::Nine);
let p = pile_with(PileType::Tableau(0), vec![card(Suit::Spades, Rank::Ten)]);
assert!(!can_place_on_tableau(&c, &p));
}
#[test]
fn tableau_wrong_rank_difference_is_invalid() {
let c = card(Suit::Hearts, Rank::Eight);
let p = pile_with(PileType::Tableau(0), vec![card(Suit::Spades, Rank::Ten)]);
assert!(!can_place_on_tableau(&c, &p));
}
#[test]
fn tableau_black_on_red_one_lower_is_valid() {
let c = card(Suit::Clubs, Rank::Six);
let p = pile_with(PileType::Tableau(0), vec![card(Suit::Hearts, Rank::Seven)]);
assert!(can_place_on_tableau(&c, &p));
}
#[test]
fn foundation_king_on_queen_completes_suit() {
// The last card placed to complete a foundation is always King on Queen.
let c = card(Suit::Spades, Rank::King);
let p = pile_with(
PileType::Foundation(0),
vec![card(Suit::Spades, Rank::Queen)],
);
assert!(can_place_on_foundation(&c, &p));
}
#[test]
fn foundation_king_wrong_suit_is_invalid() {
// King of Hearts cannot go on a Spades-claimed foundation even if rank matches.
let c = card(Suit::Hearts, Rank::King);
let p = pile_with(
PileType::Foundation(0),
vec![card(Suit::Spades, Rank::Queen)],
);
assert!(!can_place_on_foundation(&c, &p));
}
#[test]
fn tableau_ace_on_two_different_color_is_valid() {
// Ace (rank 1) can be placed on a Two of the opposite colour in the tableau.
// rank check: Ace.value() + 1 = 2 == Two.value() — passes.
let c = card(Suit::Hearts, Rank::Ace);
let p = pile_with(PileType::Tableau(0), vec![card(Suit::Spades, Rank::Two)]);
assert!(can_place_on_tableau(&c, &p));
}
#[test]
fn tableau_same_rank_different_color_is_invalid() {
// Two cards of the same rank cannot be stacked regardless of colour.
let c = card(Suit::Hearts, Rank::Nine);
let p = pile_with(PileType::Tableau(0), vec![card(Suit::Spades, Rank::Nine)]);
assert!(!can_place_on_tableau(&c, &p));
}
#[test]
fn tableau_face_down_destination_top_is_invalid() {
// A face-down top card must never be a valid placement target.
let c = card(Suit::Hearts, Rank::Nine);
let mut top = card(Suit::Spades, Rank::Ten);
top.face_up = false;
let p = pile_with(PileType::Tableau(0), vec![top]);
assert!(!can_place_on_tableau(&c, &p));
}
#[test]
fn tableau_sequence_validation() {
// Single card is trivially a valid sequence.
assert!(is_valid_tableau_sequence(&[card(Suit::Hearts, Rank::Five)]));
// Valid descending alternating-colour run K♠ Q♥ J♣.
assert!(is_valid_tableau_sequence(&[
card(Suit::Spades, Rank::King),
card(Suit::Hearts, Rank::Queen),
card(Suit::Clubs, Rank::Jack),
]));
// Same colour twice (Q♠ on K♠) — invalid.
assert!(!is_valid_tableau_sequence(&[
card(Suit::Spades, Rank::King),
card(Suit::Spades, Rank::Queen),
]));
// Rank gap (K♠ → J♥) — invalid.
assert!(!is_valid_tableau_sequence(&[
card(Suit::Spades, Rank::King),
card(Suit::Hearts, Rank::Jack),
]));
}
}
+154 -1338
View File
File diff suppressed because it is too large Load Diff