10 Commits

Author SHA1 Message Date
Quaternions 24b94e8dfa fix stock flip up and stock recycle 2026-05-15 10:55:58 -07:00
Quaternions 31025964ef try tableau moves first in KlondikeIter 2026-05-15 10:43:07 -07:00
Quaternions a8d9798001 fix bug 2026-05-15 10:36:19 -07:00
Quaternions a2f0a489c1 add undo to cli 2026-05-15 10:35:34 -07:00
Quaternions f1805826bb write cli repl 2026-05-15 10:28:52 -07:00
Quaternions c1ce3aadca test better 2026-05-15 09:43:00 -07:00
Quaternions 82b5020da4 implement is_winnable 2026-05-15 09:10:52 -07:00
Quaternions 3ca568131e fix Stock -> Stock instruction not appearing in KlondikeIter 2026-05-15 09:02:53 -07:00
Quaternions ab3cf9b3f3 fix ace to foundation 2026-05-15 09:01:03 -07:00
Quaternions f18e8b9b1a new_random_default 2026-05-15 08:56:24 -07:00
6 changed files with 297 additions and 51 deletions
+1 -2
View File
@@ -11,8 +11,7 @@ use card_game::card_game::{Session, Game};
use card_game::klondike::Klondike;
// create game session
let seed = Rng::default();
let game = Klondike::new(seed.clone(), Default::default());
let game = Klondike::new_random_default();
let mut session = Session::new(game);
// is winnable
+80 -11
View File
@@ -1,5 +1,3 @@
use crate::Rng;
// TODO: pub struct ValidInstruction<I>(I);
pub trait Game {
type Instruction;
@@ -31,6 +29,22 @@ impl Suit {
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CardValue(u8);
impl CardValue {
pub const ACE: Self = CardValue(1);
pub const TWO: Self = CardValue(2);
pub const THREE: Self = CardValue(3);
pub const FOUR: Self = CardValue(4);
pub const FIVE: Self = CardValue(5);
pub const SIX: Self = CardValue(6);
pub const SEVEN: Self = CardValue(7);
pub const EIGHT: Self = CardValue(8);
pub const NINE: Self = CardValue(9);
pub const TEN: Self = CardValue(10);
pub const JACK: Self = CardValue(11);
pub const QUEEN: Self = CardValue(12);
pub const KING: Self = CardValue(13);
pub fn get(self) -> u8 {
self.0
}
pub fn checked_add(self, offset: u8) -> Option<CardValue> {
let new_value = self.0.checked_add(offset)?;
if 13 < new_value {
@@ -86,7 +100,7 @@ impl Card {
}
}
#[derive(Clone, Debug, Hash)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Stack(Vec<Card>);
impl Stack {
pub fn new() -> Self {
@@ -120,7 +134,7 @@ impl std::ops::DerefMut for Stack {
}
}
#[derive(Clone, Debug, Hash)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Pile {
face_down: Stack,
face_up: Stack,
@@ -138,18 +152,28 @@ impl Pile {
face_up: Stack::new(),
}
}
pub fn flip_it_and_reverse_it(&mut self) {
self.swap_up_down();
self.face_up.reverse();
}
pub fn swap_up_down(&mut self) {
core::mem::swap(&mut self.face_up, &mut self.face_down);
}
pub fn flip_up(&mut self) {
if let Some(card) = self.face_down.pop() {
self.face_up.push(card);
}
}
pub fn is_empty(&self) -> bool {
self.face_down.is_empty() && self.face_up.is_empty()
}
pub fn pop(&mut self) -> Option<Card> {
let card = self.face_up.pop()?;
self.face_up.pop()
}
pub fn pop_flip_up(&mut self) -> Option<Card> {
let card = self.pop()?;
if self.face_up.is_empty() {
if let Some(card) = self.face_down.pop() {
self.face_up.push(card);
}
self.flip_up();
}
Some(card)
}
@@ -164,14 +188,15 @@ impl Pile {
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Session<G: Game> {
seed: G,
state: G,
history: Vec<G::Instruction>,
}
impl<G: Game + Clone> Session<G>
impl<G: Game + Clone + Eq + core::hash::Hash> Session<G>
where
G::Instruction: Clone,
G::Instruction: Clone + Eq + core::hash::Hash,
{
pub fn new(state: G) -> Self {
Self {
@@ -180,11 +205,55 @@ where
history: Vec::new(),
}
}
pub fn state(&self) -> &G {
&self.state
}
pub fn history(&self) -> &[G::Instruction] {
&self.history
}
pub fn is_winnable(&self) -> Option<Vec<G::Instruction>> {
None
let mut observed = std::collections::HashSet::new();
struct StateMachine<G, P, I> {
state: G,
possible_instructions_iter: P,
instruction: I,
}
let state = self.state.clone();
let mut state = StateMachine {
possible_instructions_iter: state.possible_instructions(),
state,
instruction: None,
};
let mut history = Vec::new();
'outer: while !state.state.is_win() {
observed.insert(state.state.clone());
for instruction in &mut state.possible_instructions_iter {
let mut next_state = state.state.clone();
next_state.process_instruction(instruction.clone());
if !observed.contains(&next_state) {
let it = next_state.possible_instructions();
history.push(core::mem::replace(
&mut state,
StateMachine {
state: next_state,
possible_instructions_iter: it,
instruction: Some(instruction),
},
));
continue 'outer;
}
}
let Some(last_state) = history.pop() else {
return None;
};
state = last_state;
}
Some(
history
.into_iter()
.filter_map(|state| state.instruction)
.collect(),
)
}
pub fn undo(&mut self) {
// replay the entire history of the game except one move
+43 -27
View File
@@ -1,7 +1,7 @@
use crate::Rng;
use crate::card_game::{Game, Pile, Stack};
use crate::card_game::{CardValue, Game, Pile, Stack};
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct KlondikeConfig {}
impl Default for KlondikeConfig {
fn default() -> Self {
@@ -11,7 +11,6 @@ impl Default for KlondikeConfig {
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum KlondikePileId {
Stock,
Tableau0,
Tableau1,
Tableau2,
@@ -24,12 +23,12 @@ pub enum KlondikePileId {
Foundation1,
Foundation2,
Foundation3,
Stock,
}
impl KlondikePileId {
fn next(self) -> Option<Self> {
use KlondikePileId::*;
Some(match self {
Stock => Tableau0,
Tableau0 => Tableau1,
Tableau1 => Tableau2,
Tableau2 => Tableau3,
@@ -41,7 +40,8 @@ impl KlondikePileId {
Foundation0 => Foundation1,
Foundation1 => Foundation2,
Foundation2 => Foundation3,
Foundation3 => return None,
Foundation3 => Stock,
Stock => return None,
})
}
}
@@ -67,7 +67,7 @@ impl KlondikeInstruction {
}
}
#[derive(Clone, Debug, Hash)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct KlondikeState {
piles: [Pile; 13],
}
@@ -106,14 +106,18 @@ impl KlondikeState {
) =>
{
// 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()
// suit matches?
&& src_card.suit() == dst_card.suit()
// value is +1?
&& dst_card.value().checked_add(1) == Some(src_card.value())
{
true
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
}
@@ -144,8 +148,8 @@ impl KlondikeIter {
fn new() -> Self {
Self {
instruction: Some(KlondikeInstruction {
src: KlondikePileId::Stock,
dst: KlondikePileId::Stock,
src: KlondikePileId::Tableau0,
dst: KlondikePileId::Tableau1,
}),
}
}
@@ -153,17 +157,21 @@ impl KlondikeIter {
impl Iterator for KlondikeIter {
type Item = KlondikeInstruction;
fn next(&mut self) -> Option<Self::Item> {
self.instruction = self.instruction?.next();
self.instruction
let instruction = self.instruction;
self.instruction = instruction?.next();
instruction
}
}
#[derive(Clone, Debug)]
#[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);
@@ -192,11 +200,11 @@ impl Klondike {
t5,
t6,
t7,
Pile::new(),
Pile::new(),
Pile::new(),
Pile::new(),
stock,
Pile::new(),
Pile::new(),
Pile::new(),
Pile::new(),
],
};
Self { config, state }
@@ -226,12 +234,20 @@ impl Game for Klondike {
KlondikeInstruction {
src: KlondikePileId::Stock,
dst: KlondikePileId::Stock,
} if self.pile(KlondikePileId::Stock).is_empty() => {
self.pile_mut(KlondikePileId::Stock).swap_up_down();
} => {
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 } => {
let card = self.pile_mut(src).pop().unwrap();
self.pile_mut(dst).push(card);
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);
}
}
}
}
+4 -4
View File
@@ -6,7 +6,7 @@ mod test;
pub type Rng = rand::rngs::ThreadRng;
// test readme
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
struct ReadmeDoctests;
// // test readme
// #[doc = include_str!("../README.md")]
// #[cfg(doctest)]
// struct ReadmeDoctests;
+158
View File
@@ -0,0 +1,158 @@
mod card_game;
mod klondike;
pub type Rng = rand::rngs::ThreadRng;
use card_game::{Card, Game, Session, Suit};
use klondike::{Klondike, KlondikeInstruction, KlondikePileId};
use std::fmt::Display;
impl Display for Card {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.value().get() {
1 => write!(f, "A"),
11 => write!(f, "J"),
12 => write!(f, "Q"),
13 => write!(f, "K"),
other => write!(f, "{other}"),
}?;
match self.suit() {
Suit::Spades => write!(f, ""),
Suit::Hearts => write!(f, "♥️"),
Suit::Clubs => write!(f, ""),
Suit::Diamonds => write!(f, ""),
}
}
}
struct OptionalCard<'a>(Option<&'a Card>);
impl Display for OptionalCard<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OptionalCard(Some(card)) => write!(f, "{card}"),
OptionalCard(None) => write!(f, "None"),
}
}
}
impl Display for Klondike {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Stock
let stock_count = self.pile(KlondikePileId::Stock).face_down().len();
writeln!(f, "Stock: {stock_count}")?;
// Hand
let hand = self.pile(KlondikePileId::Stock).face_up().last();
writeln!(f, "Hand: {}", OptionalCard(hand))?;
// Foundations
write!(
f,
"Foundations: {} {} {} {}",
OptionalCard(self.pile(KlondikePileId::Foundation0).face_up().last()),
OptionalCard(self.pile(KlondikePileId::Foundation1).face_up().last()),
OptionalCard(self.pile(KlondikePileId::Foundation2).face_up().last()),
OptionalCard(self.pile(KlondikePileId::Foundation3).face_up().last())
)?;
writeln!(f)?;
for (i, tableau) in [
KlondikePileId::Tableau0,
KlondikePileId::Tableau1,
KlondikePileId::Tableau2,
KlondikePileId::Tableau3,
KlondikePileId::Tableau4,
KlondikePileId::Tableau5,
KlondikePileId::Tableau6,
KlondikePileId::Tableau7,
]
.into_iter()
.enumerate()
{
write!(f, "T{i} ")?;
let pile = self.pile(tableau);
for _ in pile.face_down() {
write!(f, "]")?;
}
for card in pile.face_up() {
write!(f, "{card}")?;
}
writeln!(f)?;
}
Ok(())
}
}
#[derive(Debug)]
struct Invalid;
struct Parsed<T>(T);
impl core::str::FromStr for Parsed<KlondikeInstruction> {
type Err = Invalid;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Parsed(src) = s.get(0..2).ok_or(Invalid)?.parse()?;
let Parsed(dst) = s.get(3..5).ok_or(Invalid)?.parse()?;
Ok(Parsed(KlondikeInstruction { src, dst }))
}
}
impl core::str::FromStr for Parsed<KlondikePileId> {
type Err = Invalid;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Parsed(match s {
"ST" => KlondikePileId::Stock,
"T0" => KlondikePileId::Tableau0,
"T1" => KlondikePileId::Tableau1,
"T2" => KlondikePileId::Tableau2,
"T3" => KlondikePileId::Tableau3,
"T4" => KlondikePileId::Tableau4,
"T5" => KlondikePileId::Tableau5,
"T6" => KlondikePileId::Tableau6,
"T7" => KlondikePileId::Tableau7,
"F0" => KlondikePileId::Foundation0,
"F1" => KlondikePileId::Foundation1,
"F2" => KlondikePileId::Foundation2,
"F3" => KlondikePileId::Foundation3,
_ => return Err(Invalid),
}))
}
}
enum SessionInstruction {
Undo,
Klondike(KlondikeInstruction),
}
impl core::str::FromStr for SessionInstruction {
type Err = Invalid;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"UNDO" => Self::Undo,
other => {
let Parsed(ki) = other.parse()?;
Self::Klondike(ki)
}
})
}
}
fn main() -> Result<(), std::io::Error> {
let mut session = Session::new(Klondike::new_random_default());
loop {
// display game
println!("{}", session.state());
// parse input
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let Ok(instruction) = input.trim().parse() else {
println!("Invalid move!");
continue;
};
// run game
match instruction {
SessionInstruction::Undo => session.undo(),
SessionInstruction::Klondike(instruction) => session.process_instruction(instruction),
}
}
}
+11 -7
View File
@@ -1,15 +1,20 @@
use crate::card_game::{Game, Session};
use crate::klondike::Klondike;
#[test]
fn test_is_winnable() {
// is winnable
let is_winnable = Session::new(Klondike::new_random_default()).is_winnable();
println!("is_winnable = {is_winnable:?}");
}
#[test]
fn test_klondike() {
use crate::Rng;
use crate::card_game::{Game, Session};
use crate::klondike::Klondike;
// create game session
let game = Klondike::new(Rng::default(), Default::default());
let game = Klondike::new_random_default();
let mut session = Session::new(game);
// is winnable
let is_winnable = session.is_winnable().is_some();
let is_winnable = session.is_winnable();
println!("is_winnable = {is_winnable:?}");
// play game
while let Some(instruction) = session.possible_instructions().next() {
@@ -24,6 +29,5 @@ fn test_klondike() {
println!("move {i} = {instruction:?}");
}
println!("is_winnable = {is_winnable}");
println!("is_win = {is_win}");
}