chore(core): delete deck.rs and scoring.rs
Build and Deploy / build-and-push (push) Failing after 1m18s
Build and Deploy / build-and-push (push) Failing after 1m18s
- deck.rs (193 lines) — Deck/deal_klondike replaced by Klondike::with_seed() - scoring.rs (152 lines) — scoring fns superseded by KlondikeAdapter; move compute_time_bonus to klondike_adapter.rs, update win_summary_plugin import - Remove rand dep from solitaire_core (only used by deck.rs) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,193 +0,0 @@
|
||||
use crate::card::{Card, Rank, Suit};
|
||||
use crate::pile::{Pile, PileType};
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{SeedableRng, seq::SliceRandom};
|
||||
|
||||
const ALL_SUITS: [Suit; 4] = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades];
|
||||
const ALL_RANKS: [Rank; 13] = [
|
||||
Rank::Ace,
|
||||
Rank::Two,
|
||||
Rank::Three,
|
||||
Rank::Four,
|
||||
Rank::Five,
|
||||
Rank::Six,
|
||||
Rank::Seven,
|
||||
Rank::Eight,
|
||||
Rank::Nine,
|
||||
Rank::Ten,
|
||||
Rank::Jack,
|
||||
Rank::Queen,
|
||||
Rank::King,
|
||||
];
|
||||
|
||||
/// A standard 52-card deck.
|
||||
pub struct Deck {
|
||||
/// All 52 cards in the deck, in deal order.
|
||||
pub cards: Vec<Card>,
|
||||
}
|
||||
|
||||
impl Deck {
|
||||
/// Creates an unshuffled deck with all 52 unique cards (id 0–51).
|
||||
pub fn new() -> Self {
|
||||
let mut cards = Vec::with_capacity(52);
|
||||
let mut id = 0u32;
|
||||
for &suit in &ALL_SUITS {
|
||||
for &rank in &ALL_RANKS {
|
||||
cards.push(Card {
|
||||
id,
|
||||
suit,
|
||||
rank,
|
||||
face_up: false,
|
||||
});
|
||||
id += 1;
|
||||
}
|
||||
}
|
||||
Self { cards }
|
||||
}
|
||||
|
||||
/// Shuffles the deck in-place using Fisher-Yates with a seeded `StdRng`.
|
||||
/// The same seed always produces the same order on any platform.
|
||||
pub fn shuffle(&mut self, seed: u64) {
|
||||
let mut rng = StdRng::seed_from_u64(seed);
|
||||
self.cards.shuffle(&mut rng);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Deck {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Deals a standard Klondike layout from a pre-shuffled deck.
|
||||
///
|
||||
/// Returns 7 tableau piles and the remaining stock pile.
|
||||
/// Column `i` contains `i + 1` cards; only the top card is face-up.
|
||||
/// Stock receives the remaining 24 cards, all face-down.
|
||||
pub fn deal_klondike(deck: Deck) -> ([Pile; 7], Pile) {
|
||||
debug_assert_eq!(
|
||||
deck.cards.len(),
|
||||
52,
|
||||
"deal_klondike requires a full 52-card deck"
|
||||
);
|
||||
let mut tableau: [Pile; 7] = core::array::from_fn(|i| Pile::new(PileType::Tableau(i)));
|
||||
// Safety: the debug_assert above documents the 52-card contract; index arithmetic is bounded.
|
||||
let mut idx = 0usize;
|
||||
|
||||
for (col, pile) in tableau.iter_mut().enumerate() {
|
||||
for row in 0..=col {
|
||||
let mut card = deck.cards[idx].clone();
|
||||
card.face_up = row == col;
|
||||
pile.cards.push(card);
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut stock = Pile::new(PileType::Stock);
|
||||
stock.cards.extend(deck.cards.into_iter().skip(idx));
|
||||
(tableau, stock)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deck_new_has_52_cards() {
|
||||
assert_eq!(Deck::new().cards.len(), 52);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deck_new_has_unique_ids() {
|
||||
let deck = Deck::new();
|
||||
let mut ids: Vec<u32> = deck.cards.iter().map(|c| c.id).collect();
|
||||
ids.sort_unstable();
|
||||
ids.dedup();
|
||||
assert_eq!(ids.len(), 52);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deck_new_has_all_suits_and_ranks() {
|
||||
let deck = Deck::new();
|
||||
for suit in ALL_SUITS {
|
||||
for rank in ALL_RANKS {
|
||||
assert!(
|
||||
deck.cards.iter().any(|c| c.suit == suit && c.rank == rank),
|
||||
"missing {rank:?} {suit:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_seed_produces_same_order() {
|
||||
let mut d1 = Deck::new();
|
||||
d1.shuffle(42);
|
||||
let mut d2 = Deck::new();
|
||||
d2.shuffle(42);
|
||||
assert_eq!(d1.cards, d2.cards);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_seeds_produce_different_orders() {
|
||||
let mut d1 = Deck::new();
|
||||
d1.shuffle(1);
|
||||
let mut d2 = Deck::new();
|
||||
d2.shuffle(2);
|
||||
assert_ne!(d1.cards, d2.cards);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deal_klondike_correct_tableau_sizes() {
|
||||
let mut deck = Deck::new();
|
||||
deck.shuffle(0);
|
||||
let (tableau, stock) = deal_klondike(deck);
|
||||
for (i, pile) in tableau.iter().enumerate() {
|
||||
assert_eq!(pile.cards.len(), i + 1, "col {i} wrong size");
|
||||
}
|
||||
assert_eq!(stock.cards.len(), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deal_klondike_top_cards_are_face_up() {
|
||||
let mut deck = Deck::new();
|
||||
deck.shuffle(0);
|
||||
let (tableau, _) = deal_klondike(deck);
|
||||
for pile in &tableau {
|
||||
assert!(pile.cards.last().unwrap().face_up);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deal_klondike_non_top_cards_are_face_down() {
|
||||
let mut deck = Deck::new();
|
||||
deck.shuffle(0);
|
||||
let (tableau, _) = deal_klondike(deck);
|
||||
for pile in &tableau {
|
||||
for card in &pile.cards[..pile.cards.len().saturating_sub(1)] {
|
||||
assert!(!card.face_up);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deal_klondike_stock_is_face_down() {
|
||||
let mut deck = Deck::new();
|
||||
deck.shuffle(0);
|
||||
let (_, stock) = deal_klondike(deck);
|
||||
assert!(stock.cards.iter().all(|c| !c.face_up));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deal_klondike_all_52_cards_present() {
|
||||
let mut deck = Deck::new();
|
||||
deck.shuffle(99);
|
||||
let (tableau, stock) = deal_klondike(deck);
|
||||
let mut ids: Vec<u32> = stock.cards.iter().map(|c| c.id).collect();
|
||||
for pile in &tableau {
|
||||
ids.extend(pile.cards.iter().map(|c| c.id));
|
||||
}
|
||||
ids.sort_unstable();
|
||||
assert_eq!(ids, (0u32..52).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::card::{Card, Rank};
|
||||
use crate::error::MoveError;
|
||||
use crate::klondike_adapter::{card_from_kl, KlondikeAdapter, SavedInstruction};
|
||||
use crate::klondike_adapter::{card_from_kl, compute_time_bonus as scoring_time_bonus, KlondikeAdapter, SavedInstruction};
|
||||
use crate::pile::{Pile, PileType};
|
||||
use crate::scoring::compute_time_bonus as scoring_time_bonus;
|
||||
use card_game::{Game, Session, SessionConfig};
|
||||
use klondike::{
|
||||
DstFoundation, DstTableau, Foundation, Klondike, KlondikeConfig, KlondikeInstruction,
|
||||
|
||||
@@ -237,8 +237,7 @@ pub fn card_to_kl(card: &crate::card::Card) -> KlCard {
|
||||
/// 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.
|
||||
/// The id 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());
|
||||
@@ -518,3 +517,12 @@ impl TryFrom<SavedInstruction> for KlondikeInstruction {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Time bonus added to the score on a win: `700_000 / elapsed_seconds`.
|
||||
/// Returns 0 when `elapsed_seconds` is 0 to avoid division by zero.
|
||||
pub fn compute_time_bonus(elapsed_seconds: u64) -> i32 {
|
||||
if elapsed_seconds == 0 {
|
||||
return 0;
|
||||
}
|
||||
(700_000u64 / elapsed_seconds).min(i32::MAX as u64) as i32
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
pub mod achievement;
|
||||
pub mod card;
|
||||
pub mod deck;
|
||||
pub mod error;
|
||||
pub mod game_state;
|
||||
pub mod klondike_adapter;
|
||||
pub mod pile;
|
||||
pub mod scoring;
|
||||
pub mod solver;
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
use crate::pile::PileType;
|
||||
|
||||
/// Score delta for moving cards from `from` to `to`.
|
||||
///
|
||||
/// Windows XP Standard scoring:
|
||||
/// - +10 for any card reaching a foundation pile
|
||||
/// - +5 for a waste → tableau move
|
||||
/// - -15 for a foundation → tableau (take-from-foundation) move
|
||||
/// - 0 for all other moves
|
||||
///
|
||||
/// Note: the +5 flip bonus for exposing a face-down tableau card is applied
|
||||
/// separately in `game_state::move_cards` because it depends on post-move state.
|
||||
pub fn score_move(from: &PileType, to: &PileType) -> i32 {
|
||||
match to {
|
||||
PileType::Foundation(_) => 10,
|
||||
PileType::Tableau(_) => match from {
|
||||
PileType::Waste => 5,
|
||||
PileType::Foundation(_) => -15,
|
||||
_ => 0,
|
||||
},
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Score penalty applied when the player uses undo: -15.
|
||||
pub fn score_undo() -> i32 {
|
||||
-15
|
||||
}
|
||||
|
||||
/// Score bonus awarded when a face-down tableau card is flipped face-up: +5.
|
||||
pub fn score_flip() -> i32 {
|
||||
5
|
||||
}
|
||||
|
||||
/// Score penalty for recycling the waste pile back to stock.
|
||||
///
|
||||
/// Windows standard: the first N recycles are free (N=1 for Draw-1, N=3 for Draw-3).
|
||||
/// Subsequent recycles cost -100 (Draw-1) or -20 (Draw-3).
|
||||
/// `recycle_count` is the new total count **after** this recycle.
|
||||
pub fn score_recycle(recycle_count: u32, is_draw_three: bool) -> i32 {
|
||||
let (free, penalty) = if is_draw_three {
|
||||
(3_u32, -20_i32)
|
||||
} else {
|
||||
(1_u32, -100_i32)
|
||||
};
|
||||
if recycle_count > free { penalty } else { 0 }
|
||||
}
|
||||
|
||||
/// Time bonus added to the score on a win: `700_000 / elapsed_seconds`.
|
||||
/// Returns 0 when `elapsed_seconds` is 0 to avoid division by zero.
|
||||
pub fn compute_time_bonus(elapsed_seconds: u64) -> i32 {
|
||||
if elapsed_seconds == 0 {
|
||||
return 0;
|
||||
}
|
||||
(700_000u64 / elapsed_seconds).min(i32::MAX as u64) as i32
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn move_to_foundation_scores_ten() {
|
||||
assert_eq!(score_move(&PileType::Waste, &PileType::Foundation(2)), 10);
|
||||
assert_eq!(
|
||||
score_move(&PileType::Tableau(0), &PileType::Foundation(0)),
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn waste_to_tableau_scores_five() {
|
||||
assert_eq!(score_move(&PileType::Waste, &PileType::Tableau(3)), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tableau_to_tableau_scores_zero() {
|
||||
assert_eq!(score_move(&PileType::Tableau(0), &PileType::Tableau(1)), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_penalty_is_negative_fifteen() {
|
||||
assert_eq!(score_undo(), -15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_bonus_at_100_seconds() {
|
||||
assert_eq!(compute_time_bonus(100), 7000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_bonus_at_zero_is_zero() {
|
||||
assert_eq!(compute_time_bonus(0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_bonus_at_one_second() {
|
||||
assert_eq!(compute_time_bonus(1), 700_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foundation_to_tableau_penalises_fifteen() {
|
||||
// Moving a card back off a foundation (take_from_foundation rule) costs -15.
|
||||
assert_eq!(
|
||||
score_move(&PileType::Foundation(0), &PileType::Tableau(0)),
|
||||
-15
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_to_stock_or_waste_scores_zero() {
|
||||
// These destinations are illegal moves in practice, but the function
|
||||
// must not panic and should return 0.
|
||||
assert_eq!(score_move(&PileType::Waste, &PileType::Stock), 0);
|
||||
assert_eq!(score_move(&PileType::Waste, &PileType::Waste), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_bonus_is_capped_at_i32_max_for_huge_values() {
|
||||
// Very short elapsed time would overflow without the .min() guard.
|
||||
let bonus = compute_time_bonus(1);
|
||||
assert!(
|
||||
bonus >= 0,
|
||||
"time bonus must be non-negative after u64→i32 cast"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flip_bonus_is_five() {
|
||||
assert_eq!(score_flip(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recycle_draw1_first_pass_free() {
|
||||
assert_eq!(score_recycle(1, false), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recycle_draw1_second_pass_penalised() {
|
||||
assert_eq!(score_recycle(2, false), -100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recycle_draw3_third_pass_free() {
|
||||
assert_eq!(score_recycle(3, true), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recycle_draw3_fourth_pass_penalised() {
|
||||
assert_eq!(score_recycle(4, true), -20);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user