Files
card_game/src/klondike.rs
T

591 lines
17 KiB
Rust

use crate::Rng;
use crate::card_game::{Card, CardValue, Game, Pile, Stack};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct KlondikeConfig {}
impl Default for KlondikeConfig {
fn default() -> Self {
KlondikeConfig {}
}
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum KlondikePileId {
Tableau1,
Tableau2,
Tableau3,
Tableau4,
Tableau5,
Tableau6,
Tableau7,
Foundation1,
Foundation2,
Foundation3,
Foundation4,
Stock,
}
impl KlondikePileId {
const fn next(self) -> Option<Self> {
use KlondikePileId::*;
Some(match self {
Tableau1 => Tableau2,
Tableau2 => Tableau3,
Tableau3 => Tableau4,
Tableau4 => Tableau5,
Tableau5 => Tableau6,
Tableau6 => Tableau7,
Tableau7 => Foundation1,
Foundation1 => Foundation2,
Foundation2 => Foundation3,
Foundation3 => Foundation4,
Foundation4 => Stock,
Stock => return None,
})
}
}
/// high four bits for stack depth, low four bits for Pile Id
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct InstructionSrc(u8);
impl InstructionSrc {
const STOCK: Self = InstructionSrc::new(KlondikePileStack::Stock);
pub const fn new(src: KlondikePileStack) -> Self {
match src {
KlondikePileStack::Tableau1(skip_cards) => {
Self(KlondikePileId::Tableau1 as u8 + ((skip_cards as u8) << 4))
}
KlondikePileStack::Tableau2(skip_cards) => {
Self(KlondikePileId::Tableau2 as u8 + ((skip_cards as u8) << 4))
}
KlondikePileStack::Tableau3(skip_cards) => {
Self(KlondikePileId::Tableau3 as u8 + ((skip_cards as u8) << 4))
}
KlondikePileStack::Tableau4(skip_cards) => {
Self(KlondikePileId::Tableau4 as u8 + ((skip_cards as u8) << 4))
}
KlondikePileStack::Tableau5(skip_cards) => {
Self(KlondikePileId::Tableau5 as u8 + ((skip_cards as u8) << 4))
}
KlondikePileStack::Tableau6(skip_cards) => {
Self(KlondikePileId::Tableau6 as u8 + ((skip_cards as u8) << 4))
}
KlondikePileStack::Tableau7(skip_cards) => {
Self(KlondikePileId::Tableau7 as u8 + ((skip_cards as u8) << 4))
}
KlondikePileStack::Foundation1 => Self(KlondikePileId::Foundation1 as u8),
KlondikePileStack::Foundation2 => Self(KlondikePileId::Foundation2 as u8),
KlondikePileStack::Foundation3 => Self(KlondikePileId::Foundation3 as u8),
KlondikePileStack::Foundation4 => Self(KlondikePileId::Foundation4 as u8),
KlondikePileStack::Stock => Self(KlondikePileId::Stock as u8),
}
}
const fn into_spec(self) -> KlondikePileStack {
// SAFETY: there is no way to construct an invalid InstructionSrc
let pile = unsafe { core::mem::transmute(self.0 & 0b1111) };
match pile {
KlondikePileId::Tableau1 => {
KlondikePileStack::Tableau1(unsafe { core::mem::transmute(self.0 >> 4) })
}
KlondikePileId::Tableau2 => {
KlondikePileStack::Tableau2(unsafe { core::mem::transmute(self.0 >> 4) })
}
KlondikePileId::Tableau3 => {
KlondikePileStack::Tableau3(unsafe { core::mem::transmute(self.0 >> 4) })
}
KlondikePileId::Tableau4 => {
KlondikePileStack::Tableau4(unsafe { core::mem::transmute(self.0 >> 4) })
}
KlondikePileId::Tableau5 => {
KlondikePileStack::Tableau5(unsafe { core::mem::transmute(self.0 >> 4) })
}
KlondikePileId::Tableau6 => {
KlondikePileStack::Tableau6(unsafe { core::mem::transmute(self.0 >> 4) })
}
KlondikePileId::Tableau7 => {
KlondikePileStack::Tableau7(unsafe { core::mem::transmute(self.0 >> 4) })
}
KlondikePileId::Foundation1 => KlondikePileStack::Foundation1,
KlondikePileId::Foundation2 => KlondikePileStack::Foundation2,
KlondikePileId::Foundation3 => KlondikePileStack::Foundation3,
KlondikePileId::Foundation4 => KlondikePileStack::Foundation4,
KlondikePileId::Stock => KlondikePileStack::Stock,
}
}
const fn next(self) -> Option<Self> {
match self.into_spec().next() {
Some(s) => Some(Self::new(s)),
None => None,
}
}
}
impl From<InstructionSrc> for KlondikePileId {
fn from(value: InstructionSrc) -> Self {
match value.into_spec() {
KlondikePileStack::Tableau1(_) => KlondikePileId::Tableau1,
KlondikePileStack::Tableau2(_) => KlondikePileId::Tableau2,
KlondikePileStack::Tableau3(_) => KlondikePileId::Tableau3,
KlondikePileStack::Tableau4(_) => KlondikePileId::Tableau4,
KlondikePileStack::Tableau5(_) => KlondikePileId::Tableau5,
KlondikePileStack::Tableau6(_) => KlondikePileId::Tableau6,
KlondikePileStack::Tableau7(_) => KlondikePileId::Tableau7,
KlondikePileStack::Foundation1 => KlondikePileId::Foundation1,
KlondikePileStack::Foundation2 => KlondikePileId::Foundation2,
KlondikePileStack::Foundation3 => KlondikePileId::Foundation3,
KlondikePileStack::Foundation4 => KlondikePileId::Foundation4,
KlondikePileStack::Stock => KlondikePileId::Stock,
}
}
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum SkipCards {
Zero,
One,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
Eleven,
Twelve,
Thirteen,
}
impl SkipCards {
const fn next(self) -> Option<Self> {
use SkipCards::*;
Some(match self {
Zero => One,
One => Two,
Two => Three,
Three => Four,
Four => Five,
Five => Six,
Six => Seven,
Seven => Eight,
Eight => Nine,
Nine => Ten,
Ten => Eleven,
Eleven => Twelve,
Twelve => Thirteen,
Thirteen => return None,
})
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum KlondikePileStack {
Tableau1(SkipCards),
Tableau2(SkipCards),
Tableau3(SkipCards),
Tableau4(SkipCards),
Tableau5(SkipCards),
Tableau6(SkipCards),
Tableau7(SkipCards),
Foundation1,
Foundation2,
Foundation3,
Foundation4,
Stock,
}
impl KlondikePileStack {
const fn next(self) -> Option<Self> {
use KlondikePileStack::*;
Some(match self {
Tableau1(skip) => match skip.next() {
Some(next) => Tableau1(next),
None => Tableau2(SkipCards::Zero),
},
Tableau2(skip) => match skip.next() {
Some(next) => Tableau2(next),
None => Tableau3(SkipCards::Zero),
},
Tableau3(skip) => match skip.next() {
Some(next) => Tableau3(next),
None => Tableau4(SkipCards::Zero),
},
Tableau4(skip) => match skip.next() {
Some(next) => Tableau4(next),
None => Tableau5(SkipCards::Zero),
},
Tableau5(skip) => match skip.next() {
Some(next) => Tableau5(next),
None => Tableau6(SkipCards::Zero),
},
Tableau6(skip) => match skip.next() {
Some(next) => Tableau6(next),
None => Tableau7(SkipCards::Zero),
},
Tableau7(skip) => match skip.next() {
Some(next) => Tableau7(next),
None => Foundation1,
},
Foundation1 => Foundation2,
Foundation2 => Foundation3,
Foundation3 => Foundation4,
Foundation4 => Stock,
Stock => return None,
})
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct KlondikeInstruction {
pub src: InstructionSrc,
pub dst: KlondikePileId,
}
impl KlondikeInstruction {
pub const fn stock() -> Self {
Self {
src: InstructionSrc::STOCK,
dst: KlondikePileId::Stock,
}
}
const 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::Tableau1,
});
}
None
}
}
const TABLEAUS: usize = 7;
const fn sum(n: usize) -> usize {
n * (n + 1) / 2
}
const MAX_STACK: usize = 52 - sum(TABLEAUS);
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct KlondikeState {
stock: Pile<MAX_STACK, MAX_STACK>,
foundations: [Stack<13>; 4],
tableau1: Pile<0, 13>,
tableau2: Pile<1, 13>,
tableau3: Pile<2, 13>,
tableau4: Pile<3, 13>,
tableau5: Pile<4, 13>,
tableau6: Pile<5, 13>,
tableau7: Pile<6, 13>,
}
impl KlondikeState {
pub const fn stock(&self) -> &Pile<MAX_STACK, MAX_STACK> {
&self.stock
}
pub const fn foundation1(&self) -> &Stack<13> {
&self.foundations[1 - 1]
}
pub const fn foundation2(&self) -> &Stack<13> {
&self.foundations[2 - 1]
}
pub const fn foundation3(&self) -> &Stack<13> {
&self.foundations[3 - 1]
}
pub const fn foundation4(&self) -> &Stack<13> {
&self.foundations[4 - 1]
}
pub const fn tableau1(&self) -> &Pile<0, 13> {
&self.tableau1
}
pub const fn tableau2(&self) -> &Pile<1, 13> {
&self.tableau2
}
pub const fn tableau3(&self) -> &Pile<2, 13> {
&self.tableau3
}
pub const fn tableau4(&self) -> &Pile<3, 13> {
&self.tableau4
}
pub const fn tableau5(&self) -> &Pile<4, 13> {
&self.tableau5
}
pub const fn tableau6(&self) -> &Pile<5, 13> {
&self.tableau6
}
pub const fn tableau7(&self) -> &Pile<6, 13> {
&self.tableau7
}
fn src_card(&self, src: InstructionSrc) -> Option<&Card> {
match src.into_spec() {
KlondikePileStack::Tableau1(skip_cards) => {
self.tableau1.face_up().get(skip_cards as usize)
}
KlondikePileStack::Tableau2(skip_cards) => {
self.tableau2.face_up().get(skip_cards as usize)
}
KlondikePileStack::Tableau3(skip_cards) => {
self.tableau3.face_up().get(skip_cards as usize)
}
KlondikePileStack::Tableau4(skip_cards) => {
self.tableau4.face_up().get(skip_cards as usize)
}
KlondikePileStack::Tableau5(skip_cards) => {
self.tableau5.face_up().get(skip_cards as usize)
}
KlondikePileStack::Tableau6(skip_cards) => {
self.tableau6.face_up().get(skip_cards as usize)
}
KlondikePileStack::Tableau7(skip_cards) => {
self.tableau7.face_up().get(skip_cards as usize)
}
KlondikePileStack::Foundation1 => self.foundations[1 - 1].last(),
KlondikePileStack::Foundation2 => self.foundations[2 - 1].last(),
KlondikePileStack::Foundation3 => self.foundations[3 - 1].last(),
KlondikePileStack::Foundation4 => self.foundations[4 - 1].last(),
KlondikePileStack::Stock => self.stock.face_up().last(),
}
}
fn take_src_cards(&mut self, src: InstructionSrc) -> Stack<13> {
match src.into_spec() {
KlondikePileStack::Tableau1(skip_cards) => {
self.tableau1.take_range_flip_up(skip_cards as usize..)
}
KlondikePileStack::Tableau2(skip_cards) => {
self.tableau2.take_range_flip_up(skip_cards as usize..)
}
KlondikePileStack::Tableau3(skip_cards) => {
self.tableau3.take_range_flip_up(skip_cards as usize..)
}
KlondikePileStack::Tableau4(skip_cards) => {
self.tableau4.take_range_flip_up(skip_cards as usize..)
}
KlondikePileStack::Tableau5(skip_cards) => {
self.tableau5.take_range_flip_up(skip_cards as usize..)
}
KlondikePileStack::Tableau6(skip_cards) => {
self.tableau6.take_range_flip_up(skip_cards as usize..)
}
KlondikePileStack::Tableau7(skip_cards) => {
self.tableau7.take_range_flip_up(skip_cards as usize..)
}
KlondikePileStack::Foundation1 => Stack::from_iter(self.foundations[1 - 1].pop()),
KlondikePileStack::Foundation2 => Stack::from_iter(self.foundations[2 - 1].pop()),
KlondikePileStack::Foundation3 => Stack::from_iter(self.foundations[3 - 1].pop()),
KlondikePileStack::Foundation4 => Stack::from_iter(self.foundations[4 - 1].pop()),
KlondikePileStack::Stock => Stack::from_iter(self.stock.pop()),
}
}
fn dst_card(&self, dst: KlondikePileId) -> Option<&Card> {
match dst {
KlondikePileId::Tableau1 => self.tableau1.face_up().last(),
KlondikePileId::Tableau2 => self.tableau2.face_up().last(),
KlondikePileId::Tableau3 => self.tableau3.face_up().last(),
KlondikePileId::Tableau4 => self.tableau4.face_up().last(),
KlondikePileId::Tableau5 => self.tableau5.face_up().last(),
KlondikePileId::Tableau6 => self.tableau6.face_up().last(),
KlondikePileId::Tableau7 => self.tableau7.face_up().last(),
KlondikePileId::Foundation1 => self.foundations[1 - 1].last(),
KlondikePileId::Foundation2 => self.foundations[2 - 1].last(),
KlondikePileId::Foundation3 => self.foundations[3 - 1].last(),
KlondikePileId::Foundation4 => self.foundations[4 - 1].last(),
KlondikePileId::Stock => None,
}
}
fn extend_dst_pile(&mut self, dst: KlondikePileId, cards: Stack<13>) {
match dst {
KlondikePileId::Tableau1 => self.tableau1.extend(cards),
KlondikePileId::Tableau2 => self.tableau2.extend(cards),
KlondikePileId::Tableau3 => self.tableau3.extend(cards),
KlondikePileId::Tableau4 => self.tableau4.extend(cards),
KlondikePileId::Tableau5 => self.tableau5.extend(cards),
KlondikePileId::Tableau6 => self.tableau6.extend(cards),
KlondikePileId::Tableau7 => self.tableau7.extend(cards),
KlondikePileId::Foundation1 => self.foundations[1 - 1].extend(cards),
KlondikePileId::Foundation2 => self.foundations[2 - 1].extend(cards),
KlondikePileId::Foundation3 => self.foundations[3 - 1].extend(cards),
KlondikePileId::Foundation4 => self.foundations[4 - 1].extend(cards),
KlondikePileId::Stock => (),
}
}
fn is_instruction_valid(&self, instruction: KlondikeInstruction) -> bool {
match instruction {
// Stock -> Stock draws a card or resets the stock
KlondikeInstruction {
src: InstructionSrc::STOCK,
dst: KlondikePileId::Stock,
} => {
// cannot move stock when stock is empty
!self.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::Foundation1
| KlondikePileId::Foundation2
| KlondikePileId::Foundation3
| KlondikePileId::Foundation4
) =>
{
// get the top cards
if let Some(src_card) = self.src_card(src) {
match self.dst_card(dst) {
// 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.src_card(src) {
match self.dst_card(dst) {
// destination card exists
Some(dst_card) => {
// 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())
}
// only king is allowed to go onto empty tableau
None => src_card.value() == CardValue::KING,
}
} else {
false
}
}
}
}
}
pub struct KlondikeIter {
instruction: Option<KlondikeInstruction>,
}
impl KlondikeIter {
const fn new() -> Self {
Self {
instruction: Some(KlondikeInstruction {
src: InstructionSrc::new(KlondikePileStack::Tableau1(SkipCards::Zero)),
dst: KlondikePileId::Tableau2,
}),
}
}
}
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);
let mut deck = deck.into_iter();
// generate tableaus
fn pile<const DN: usize>(deck: &mut arrayvec::IntoIter<Card, 52>) -> Pile<DN, 13> {
let stack = arrayvec::ArrayVec::from_iter(deck.take(DN)).into();
let mut pile = Pile::new_face_down(stack);
pile.push(deck.next().unwrap());
pile
}
let tableau1 = pile(&mut deck);
let tableau2 = pile(&mut deck);
let tableau3 = pile(&mut deck);
let tableau4 = pile(&mut deck);
let tableau5 = pile(&mut deck);
let tableau6 = pile(&mut deck);
let tableau7 = pile(&mut deck);
// stock is remaining cards
let stock = Pile::new_face_down(arrayvec::ArrayVec::from_iter(deck).into());
let state = KlondikeState {
stock,
foundations: core::array::from_fn(|_| Stack::new()),
tableau1,
tableau2,
tableau3,
tableau4,
tableau5,
tableau6,
tableau7,
};
Self { config, state }
}
pub const fn state(&self) -> &KlondikeState {
&self.state
}
}
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: InstructionSrc::STOCK,
dst: KlondikePileId::Stock,
} => {
if self.state.stock.face_down().is_empty() {
self.state.stock.flip_it_and_reverse_it();
} else {
self.state.stock.flip_up();
}
}
KlondikeInstruction { src, dst } => {
let cards = self.state.take_src_cards(src);
self.state.extend_dst_pile(dst, cards);
}
}
}
fn is_win(&self) -> bool {
// all face down cards empty means win
self.state.stock.face_down().is_empty()
&& self.state.tableau1.face_down().is_empty()
&& self.state.tableau2.face_down().is_empty()
&& self.state.tableau3.face_down().is_empty()
&& self.state.tableau4.face_down().is_empty()
&& self.state.tableau5.face_down().is_empty()
&& self.state.tableau6.face_down().is_empty()
&& self.state.tableau7.face_down().is_empty()
}
}