Files
card_game/src/klondike.rs
T
2026-05-15 11:03:45 -07:00

260 lines
6.1 KiB
Rust

use crate::Rng;
use crate::card_game::{CardValue, Game, Pile, Stack};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct KlondikeConfig {}
impl Default for KlondikeConfig {
fn default() -> Self {
KlondikeConfig {}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum KlondikePileId {
Tableau0,
Tableau1,
Tableau2,
Tableau3,
Tableau4,
Tableau5,
Tableau6,
Tableau7,
Foundation0,
Foundation1,
Foundation2,
Foundation3,
Stock,
}
impl KlondikePileId {
fn next(self) -> Option<Self> {
use KlondikePileId::*;
Some(match self {
Tableau0 => Tableau1,
Tableau1 => Tableau2,
Tableau2 => Tableau3,
Tableau3 => Tableau4,
Tableau4 => Tableau5,
Tableau5 => Tableau6,
Tableau6 => Tableau7,
Tableau7 => Foundation0,
Foundation0 => Foundation1,
Foundation1 => Foundation2,
Foundation2 => Foundation3,
Foundation3 => Stock,
Stock => return None,
})
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct KlondikeInstruction {
pub src: KlondikePileId,
pub dst: KlondikePileId,
}
impl KlondikeInstruction {
fn next(self) -> Option<Self> {
let KlondikeInstruction { src, dst } = self;
if let Some(next_dst) = dst.next() {
return Some(Self { src, dst: next_dst });
}
if let Some(next_src) = src.next() {
return Some(Self {
src: next_src,
dst: KlondikePileId::Stock,
});
}
None
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct KlondikeState {
piles: [Pile; 13],
}
impl KlondikeState {
fn pile(&self, index: KlondikePileId) -> &Pile {
&self.piles[index as usize]
}
fn pile_mut(&mut self, index: KlondikePileId) -> &mut Pile {
&mut self.piles[index as usize]
}
fn is_instruction_valid(&self, instruction: KlondikeInstruction) -> bool {
match instruction {
// Stock -> Stock draws a card or resets the stock
KlondikeInstruction {
src: KlondikePileId::Stock,
dst: KlondikePileId::Stock,
} => {
// cannot move stock when stock is empty
!self.pile(KlondikePileId::Stock).is_empty()
}
// cannot move cards to stock
KlondikeInstruction {
src: _,
dst: KlondikePileId::Stock,
} => false,
// moving to foundation has special rules
KlondikeInstruction { src, dst }
if matches!(
dst,
KlondikePileId::Foundation0
| KlondikePileId::Foundation1
| KlondikePileId::Foundation2
| KlondikePileId::Foundation3
) =>
{
// get the top cards
if let Some(src_card) = self.pile(src).face_up().last() {
match self.pile(dst).face_up().last() {
// destination card exists
Some(dst_card) => {
// suit matches?
src_card.suit() == dst_card.suit()
// value is +1?
&& dst_card.value().checked_add(1) == Some(src_card.value())
}
// only ace is allowed to go onto empty foundation
None => src_card.value() == CardValue::ACE,
}
} else {
false
}
}
// other = move to tableau
KlondikeInstruction { src, dst } => {
// get the top cards
if let Some(src_card) = self.pile(src).face_up().last()
&& let Some(dst_card) = self.pile(dst).face_up().last()
// red-ness is opposite?
&& src_card.is_red() != dst_card.is_red()
// value is -1?
&& dst_card.value().checked_sub(1) == Some(src_card.value())
{
true
} else {
false
}
}
}
}
}
pub struct KlondikeIter {
instruction: Option<KlondikeInstruction>,
}
impl KlondikeIter {
fn new() -> Self {
Self {
instruction: Some(KlondikeInstruction {
src: KlondikePileId::Tableau0,
dst: KlondikePileId::Tableau1,
}),
}
}
}
impl Iterator for KlondikeIter {
type Item = KlondikeInstruction;
fn next(&mut self) -> Option<Self::Item> {
let instruction = self.instruction;
self.instruction = instruction?.next();
instruction
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Klondike {
config: KlondikeConfig,
state: KlondikeState,
}
impl Klondike {
pub fn new_random_default() -> Self {
Self::new(Rng::default(), KlondikeConfig::default())
}
pub fn new(mut seed: Rng, config: KlondikeConfig) -> Self {
// shuffle a new deck
let mut deck = Stack::full_deck(0);
use rand::seq::SliceRandom;
deck.shuffle(&mut seed);
// generate tableaus
let [t0, t1, t2, t3, t4, t5, t6, t7] = core::array::from_fn(|i| {
let remaining = deck.split_off(i).into();
let stack = core::mem::replace(&mut deck, remaining);
let mut pile = Pile::new_face_down(stack);
pile.push(deck.pop().unwrap());
pile
});
// stock is remaining cards
let stock = Pile::new_face_down(deck);
let state = KlondikeState {
piles: [
t0,
t1,
t2,
t3,
t4,
t5,
t6,
t7,
Pile::new(),
Pile::new(),
Pile::new(),
Pile::new(),
stock,
],
};
Self { config, state }
}
#[inline]
pub fn pile(&self, index: KlondikePileId) -> &Pile {
self.state.pile(index)
}
#[inline]
fn pile_mut(&mut self, index: KlondikePileId) -> &mut Pile {
self.state.pile_mut(index)
}
}
impl Game for Klondike {
type Instruction = KlondikeInstruction;
fn possible_instructions(&self) -> impl Iterator<Item = Self::Instruction> + use<> {
let state = self.state.clone();
KlondikeIter::new().filter(move |&instruction| state.is_instruction_valid(instruction))
}
fn is_instruction_valid(&self, instruction: Self::Instruction) -> bool {
self.state.is_instruction_valid(instruction)
}
fn process_instruction(&mut self, instruction: Self::Instruction) {
match instruction {
// Reset the stock if it's empty
KlondikeInstruction {
src: KlondikePileId::Stock,
dst: KlondikePileId::Stock,
} => {
if self.pile(KlondikePileId::Stock).face_down().is_empty() {
self.pile_mut(KlondikePileId::Stock)
.flip_it_and_reverse_it();
} else {
self.pile_mut(KlondikePileId::Stock).flip_up();
}
}
KlondikeInstruction { src, dst } => {
if let Some(card) = self.pile_mut(src).pop_flip_up() {
self.pile_mut(dst).push(card);
} else {
println!("Attempted to move from an empty src");
dbg!(instruction);
}
}
}
}
fn is_win(&self) -> bool {
// assuming only valid moves, tableau empty and stock empty means win
self.state.piles[0..9].iter().all(|pile| pile.is_empty())
}
}