feat(core): card/pile conversion utils and GameMode-aware scoring (steps 2-prep, 5)
Build and Deploy / build-and-push (push) Failing after 55s
Build and Deploy / build-and-push (push) Failing after 55s
Step 2 prep — card_game dep + type-conversion utilities: - Add card_game = "0.3.0" (registry Quaternions) to workspace + core - suit_to_kl / suit_from_kl, rank_to_kl / rank_from_kl - card_to_kl (drops id, Deck1), card_from_kl (reconstructs stable id from Clubs-first suit×13+rank ordering matching deck.rs) - Ready to wire into KlondikeState pile projection once upstream adds KlondikeState::from_piles() Step 5 — GameMode-aware scoring in the adapter: - score_for_move_with_mode, score_for_flip_with_mode (return 0 in Zen) - apply_undo_score (static, handles Zen + −15 penalty + clamp) - score_for_recycle_with_mode (return 0 in Zen) - game_state.rs: all inline GameMode::Zen checks replaced with adapter calls; adapter is now the single source of truth for "what score does this action give in this mode" 192 tests pass; clippy -D warnings clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,9 +13,10 @@
|
||||
//! - Move validation via klondike's rule engine (step 2).
|
||||
//! - DFS solver via [`klondike::KlondikeState`] (step 6).
|
||||
|
||||
use card_game::{Card as KlCard, Deck as KlDeck, Rank as KlRank, Suit as KlSuit};
|
||||
use klondike::{DrawStockConfig, KlondikeConfig, MoveFromFoundationConfig, ScoringConfig};
|
||||
|
||||
use crate::game_state::DrawMode;
|
||||
use crate::game_state::{DrawMode, GameMode};
|
||||
use crate::pile::PileType;
|
||||
|
||||
/// Bridges `solitaire_core` game config and scoring to the upstream `klondike` crate.
|
||||
@@ -136,4 +137,110 @@ impl KlondikeAdapter {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Score delta for a card move, accounting for game mode.
|
||||
///
|
||||
/// Returns 0 in [`GameMode::Zen`] (all scoring suppressed).
|
||||
pub fn score_for_move_with_mode(&self, from: &PileType, to: &PileType, mode: GameMode) -> i32 {
|
||||
if mode == GameMode::Zen { 0 } else { self.score_for_move(from, to) }
|
||||
}
|
||||
|
||||
/// Score delta for exposing a face-down card, accounting for game mode.
|
||||
///
|
||||
/// Returns 0 in [`GameMode::Zen`].
|
||||
pub fn score_for_flip_with_mode(&self, mode: GameMode) -> i32 {
|
||||
if mode == GameMode::Zen { 0 } else { self.score_for_flip() }
|
||||
}
|
||||
|
||||
/// Compute the new score after an undo, accounting for game mode.
|
||||
///
|
||||
/// In [`GameMode::Zen`] the score is always 0. Otherwise applies the
|
||||
/// −15 undo penalty and clamps to 0 via [`Self::score_for_undo`].
|
||||
pub fn apply_undo_score(snapshot_score: i32, mode: GameMode) -> i32 {
|
||||
if mode == GameMode::Zen {
|
||||
0
|
||||
} else {
|
||||
(snapshot_score + Self::score_for_undo()).max(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Score delta for recycling, accounting for game mode.
|
||||
///
|
||||
/// Returns 0 in [`GameMode::Zen`].
|
||||
pub fn score_for_recycle_with_mode(
|
||||
recycle_count: u32,
|
||||
is_draw_three: bool,
|
||||
mode: GameMode,
|
||||
) -> i32 {
|
||||
if mode == GameMode::Zen {
|
||||
0
|
||||
} else {
|
||||
Self::score_for_recycle(recycle_count, is_draw_three)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Type-conversion utilities (Step 2 — pile mapping) ────────────────────
|
||||
//
|
||||
// These are used to translate between solitaire_core's Card/PileType and the
|
||||
// card_game / klondike types when projecting KlondikeState into the pile
|
||||
// snapshot that the engine reads. A live KlondikeState shadow requires an
|
||||
// upstream `KlondikeState::from_piles()` constructor; these utilities are
|
||||
// ready to wire in once that's available.
|
||||
|
||||
/// Convert our [`crate::card::Suit`] to [`card_game::Suit`].
|
||||
pub fn suit_to_kl(suit: crate::card::Suit) -> KlSuit {
|
||||
match suit {
|
||||
crate::card::Suit::Clubs => KlSuit::Clubs,
|
||||
crate::card::Suit::Diamonds => KlSuit::Diamonds,
|
||||
crate::card::Suit::Hearts => KlSuit::Hearts,
|
||||
crate::card::Suit::Spades => KlSuit::Spades,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert [`card_game::Suit`] back to our [`crate::card::Suit`].
|
||||
pub fn suit_from_kl(suit: KlSuit) -> crate::card::Suit {
|
||||
match suit {
|
||||
KlSuit::Clubs => crate::card::Suit::Clubs,
|
||||
KlSuit::Diamonds => crate::card::Suit::Diamonds,
|
||||
KlSuit::Hearts => crate::card::Suit::Hearts,
|
||||
KlSuit::Spades => crate::card::Suit::Spades,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert our [`crate::card::Rank`] to [`card_game::Rank`].
|
||||
pub fn rank_to_kl(rank: crate::card::Rank) -> KlRank {
|
||||
KlRank::new(rank.value()).expect("rank value 1-13 always maps to a valid KlRank")
|
||||
}
|
||||
|
||||
/// Convert [`card_game::Rank`] back to our [`crate::card::Rank`].
|
||||
pub fn rank_from_kl(rank: KlRank) -> crate::card::Rank {
|
||||
crate::card::Rank::RANKS
|
||||
.into_iter()
|
||||
.find(|r| r.value() == rank as u8)
|
||||
.expect("KlRank 1-13 always maps to a valid Rank")
|
||||
}
|
||||
|
||||
/// Convert our [`crate::card::Card`] to a [`card_game::Card`] (Deck1, same suit/rank).
|
||||
///
|
||||
/// The `id` field is dropped; use [`card_to_kl`] only when the klondike engine
|
||||
/// needs to evaluate the card's logical identity, not its animation entity.
|
||||
pub fn card_to_kl(card: &crate::card::Card) -> KlCard {
|
||||
KlCard::new(KlDeck::Deck1, suit_to_kl(card.suit), rank_to_kl(card.rank))
|
||||
}
|
||||
|
||||
/// Convert a [`card_game::Card`] back to our [`crate::card::Card`], assigning
|
||||
/// a stable `id` derived from the suit and rank (0–51, Clubs-first ordering).
|
||||
///
|
||||
/// This id matches the id assigned in [`crate::deck::Deck::new`] and is
|
||||
/// consistent for the same logical card across all reconstructions.
|
||||
pub fn card_from_kl(card: &KlCard) -> crate::card::Card {
|
||||
let suit = suit_from_kl(card.suit());
|
||||
let rank = rank_from_kl(card.rank());
|
||||
let suit_index = crate::card::Suit::SUITS
|
||||
.iter()
|
||||
.position(|s| *s == suit)
|
||||
.expect("suit always in SUITS") as u32;
|
||||
let id = suit_index * 13 + (rank.value() as u32 - 1);
|
||||
crate::card::Card { id, suit, rank, face_up: false }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user