18 Commits

Author SHA1 Message Date
Quaternions 5ab1b8bac7 stock face down does not matter for trivial win 2026-05-26 22:39:46 -07:00
Quaternions a80f2ae3c9 tableau_face_down_cards tableau_face_up_cards functions 2026-05-26 22:34:14 -07:00
Quaternions 555daf5ec3 use custom arrayvec 2026-05-24 10:36:15 -07:00
Quaternions 555e7ef11c update readme 2026-05-19 16:46:32 -07:00
Quaternions f49721e415 no repr 2026-05-19 07:42:34 -07:00
Quaternions 90f8fe2e77 1000 games 2026-05-18 16:45:49 -07:00
Quaternions 7f708df327 stats histogram 2026-05-18 16:43:45 -07:00
Quaternions 6bfa05c292 add benchmark 2026-05-18 15:57:53 -07:00
Quaternions da0dfe98c4 update readme 2026-05-18 15:51:17 -07:00
Quaternions b840d56725 use workspace lints 2026-05-18 15:43:32 -07:00
Quaternions 37837e76c1 use get_auto_move in readme 2026-05-18 14:04:58 -07:00
Quaternions e6113b6e91 tweak doc 2026-05-18 13:57:47 -07:00
Quaternions 5cb5cb9a50 tweak doc 2026-05-18 13:43:39 -07:00
Quaternions 446cd87665 update readme 2026-05-18 13:34:03 -07:00
Quaternions a9b93f3807 update readme 2026-05-18 13:29:59 -07:00
Quaternions 13bc36ce0c tweak with_rng signature to allow Rng reuse 2026-05-18 13:28:40 -07:00
Quaternions 07c3dc6667 fix doc 2026-05-18 13:26:08 -07:00
Quaternions 835a4dcc5f fix registry 2026-05-18 13:15:11 -07:00
11 changed files with 122 additions and 48 deletions
Generated
+11 -2
View File
@@ -11,8 +11,8 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "sparse+https://git.aleshym.co/api/packages/Quaternions/cargo/"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "813440870d646c57c222c1d713dc4e3ddcb2919c3801564d767d85d7bf2afee4"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
@@ -132,6 +132,15 @@ dependencies = [
"rand", "rand",
] ]
[[package]]
name = "klondike-bench"
version = "0.1.0"
dependencies = [
"card_game",
"klondike",
"rand",
]
[[package]] [[package]]
name = "klondike-cli" name = "klondike-cli"
version = "0.1.0" version = "0.1.0"
+3 -2
View File
@@ -2,13 +2,14 @@
members = [ members = [
"card_game", "card_game",
"klondike", "klondike",
"klondike-bench",
"klondike-cli", "klondike-cli",
] ]
resolver = "3" resolver = "3"
[workspace.dependencies] [workspace.dependencies]
card_game = { version = "0.2.0", path = "card_game" } card_game = { version = "0.2.0", path = "card_game", registry = "Quaternions" }
klondike = { version = "0.1.0", path = "klondike" } klondike = { version = "0.1.0", path = "klondike", registry = "Quaternions" }
[workspace.lints.rust] [workspace.lints.rust]
# unsafe_code = "forbid" # unsafe_code = "forbid"
+4 -1
View File
@@ -9,4 +9,7 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
keywords = ["card", "cards", "solitaire", "klondike"] keywords = ["card", "cards", "solitaire", "klondike"]
[dependencies] [dependencies]
arrayvec = "0.7.6" arrayvec = { version = "0.7.6", registry = "Quaternions", features = ["len_u8"], default-features = false }
[lints]
workspace = true
+1 -1
View File
@@ -1,7 +1,7 @@
Card Game Card Game
========= =========
`card_game` is a library to implement card games. Mainly interesting for the `Game` trait and the `Session` type. `card_game` is a collection of algorithms, structs, and enums which are useful to implement card games.
## Example ## Example
+3 -1
View File
@@ -135,7 +135,8 @@ impl Rank {
} }
} }
} }
/// An identifier which specifies the deck id, suit, and card value.
/// A card which specifies the deck id, suit, and card value.
/// 2 bits for deck ID /// 2 bits for deck ID
/// 2 bits for suit ID /// 2 bits for suit ID
/// 4 bits for card Value /// 4 bits for card Value
@@ -224,6 +225,7 @@ impl<const CAP: usize> IntoIterator for Stack<CAP> {
} }
} }
/// A pile is a stack of face down cards and a stack of face up cards.
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct Pile<const DN: usize, const UP: usize> { pub struct Pile<const DN: usize, const UP: usize> {
face_down: Stack<DN>, face_down: Stack<DN>,
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "klondike-bench"
version = "0.1.0"
edition = "2024"
[dependencies]
card_game.workspace = true
klondike.workspace = true
rand = { version = "0.10.1", default-features = false }
[lints]
workspace = true
+46
View File
@@ -0,0 +1,46 @@
use card_game::Game;
use klondike::{Klondike, KlondikeConfig, KlondikeStats, Rng};
const MAX_MOVES: usize = 250;
fn play_to_win(rng: &mut Rng) -> Option<KlondikeStats> {
// create game session
let mut game = Klondike::with_rng(rng);
let mut stats = KlondikeStats::new();
const CONFIG: KlondikeConfig = KlondikeConfig {
draw_stock: klondike::DrawStockConfig::DrawOne,
};
// play game a bit
while let Some(instruction) = game.get_auto_move()
&& !game.is_win()
{
// quit before 250 moves
if MAX_MOVES < stats.moves() + 1 {
return None;
}
game.process_instruction(&mut stats, &CONFIG, instruction);
}
game.is_win().then_some(stats)
}
fn main() {
use rand::SeedableRng;
let mut rng = Rng::seed_from_u64(0);
const GAMES: u32 = 1000;
let mut wins = 0;
let mut score_tally = [0usize; MAX_MOVES * 10 / 5];
let mut recycle_tally = [0usize; MAX_MOVES];
let mut moves_tally = [0usize; MAX_MOVES];
for _ in 0..GAMES {
if let Some(stats) = play_to_win(&mut rng) {
wins += 1;
score_tally[stats.score() / 5] += 1;
recycle_tally[stats.recycle_count()] += 1;
moves_tally[stats.moves()] += 1;
}
}
println!("score_tally={score_tally:?}");
println!("recycle_tally={recycle_tally:?}");
println!("moves_tally={moves_tally:?}");
println!("wins = {wins}/{GAMES} win_rate = {}%", wins * 100 / GAMES);
}
+3
View File
@@ -7,3 +7,6 @@ edition = "2024"
card_game.workspace = true card_game.workspace = true
klondike.workspace = true klondike.workspace = true
rand = { version = "0.10.1", default-features = false, features = ["thread_rng"] } rand = { version = "0.10.1", default-features = false, features = ["thread_rng"] }
[lints]
workspace = true
+3
View File
@@ -6,3 +6,6 @@ edition = "2024"
[dependencies] [dependencies]
card_game.workspace = true card_game.workspace = true
rand = { version = "0.10.1", default-features = false, features = ["std_rng"] } rand = { version = "0.10.1", default-features = false, features = ["std_rng"] }
[lints]
workspace = true
+7 -7
View File
@@ -1,12 +1,12 @@
Card Game Klondike
========= ========
`card_game` is a library to implement card games. Mainly interesting for the `Game` trait and the `Session` type. Contains klondike as the reference implementation. `klondike` is a pure-logic implementation of Klondike using `card_game`. Graphics not included.
## Example ## Example
```rust ```rust
use card_game::{Session, Game}; use card_game::Session;
use klondike::Klondike; use klondike::Klondike;
// create game session // create game session
@@ -14,11 +14,11 @@ let game = Klondike::with_seed(123);
let mut session = Session::new_default(game); let mut session = Session::new_default(game);
// play game a bit // play game a bit
while let Some(instruction) = session.possible_instructions().next() { while let Some(instruction) = session.state().get_auto_move() {
session.process_instruction(instruction); session.process_instruction(instruction);
// quit after 1000 moves // quit after 200 moves or win
if 1000 < session.stats().stats().moves() { if session.is_win() || 200 < session.stats().stats().moves() {
break; break;
} }
} }
+29 -34
View File
@@ -137,7 +137,6 @@ impl From<Foundation> for KlondikePile {
} }
} }
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum SkipCards { pub enum SkipCards {
Skip0, Skip0,
@@ -366,15 +365,26 @@ impl KlondikeState {
pub const fn tableau7(&self) -> &Pile<6, 13> { pub const fn tableau7(&self) -> &Pile<6, 13> {
&self.tableau7 &self.tableau7
} }
pub fn is_tableau_face_down_empty(&self, tableau: Tableau) -> bool { pub fn tableau_face_down_cards(&self, tableau: Tableau) -> &[Card] {
match tableau { match tableau {
Tableau::Tableau1 => self.tableau1.face_down().is_empty(), Tableau::Tableau1 => self.tableau1.face_down(),
Tableau::Tableau2 => self.tableau2.face_down().is_empty(), Tableau::Tableau2 => self.tableau2.face_down(),
Tableau::Tableau3 => self.tableau3.face_down().is_empty(), Tableau::Tableau3 => self.tableau3.face_down(),
Tableau::Tableau4 => self.tableau4.face_down().is_empty(), Tableau::Tableau4 => self.tableau4.face_down(),
Tableau::Tableau5 => self.tableau5.face_down().is_empty(), Tableau::Tableau5 => self.tableau5.face_down(),
Tableau::Tableau6 => self.tableau6.face_down().is_empty(), Tableau::Tableau6 => self.tableau6.face_down(),
Tableau::Tableau7 => self.tableau7.face_down().is_empty(), Tableau::Tableau7 => self.tableau7.face_down(),
}
}
pub fn tableau_face_up_cards(&self, tableau: Tableau) -> &[Card] {
match tableau {
Tableau::Tableau1 => self.tableau1.face_up(),
Tableau::Tableau2 => self.tableau2.face_up(),
Tableau::Tableau3 => self.tableau3.face_up(),
Tableau::Tableau4 => self.tableau4.face_up(),
Tableau::Tableau5 => self.tableau5.face_up(),
Tableau::Tableau6 => self.tableau6.face_up(),
Tableau::Tableau7 => self.tableau7.face_up(),
} }
} }
pub fn stack_bottom_card(&self, src: KlondikePileStack) -> Option<&Card> { pub fn stack_bottom_card(&self, src: KlondikePileStack) -> Option<&Card> {
@@ -382,15 +392,7 @@ impl KlondikeState {
KlondikePileStack::Tableau(TableauStack { KlondikePileStack::Tableau(TableauStack {
tableau, tableau,
skip_cards, skip_cards,
}) => match tableau { }) => self.tableau_face_up_cards(tableau).get(skip_cards as usize),
Tableau::Tableau1 => self.tableau1.face_up().get(skip_cards as usize),
Tableau::Tableau2 => self.tableau2.face_up().get(skip_cards as usize),
Tableau::Tableau3 => self.tableau3.face_up().get(skip_cards as usize),
Tableau::Tableau4 => self.tableau4.face_up().get(skip_cards as usize),
Tableau::Tableau5 => self.tableau5.face_up().get(skip_cards as usize),
Tableau::Tableau6 => self.tableau6.face_up().get(skip_cards as usize),
Tableau::Tableau7 => self.tableau7.face_up().get(skip_cards as usize),
},
KlondikePileStack::Foundation(foundation) => { KlondikePileStack::Foundation(foundation) => {
self.foundations[foundation as usize].last() self.foundations[foundation as usize].last()
} }
@@ -399,15 +401,7 @@ impl KlondikeState {
} }
pub fn top_card<S: Into<KlondikePile>>(&self, src: S) -> Option<&Card> { pub fn top_card<S: Into<KlondikePile>>(&self, src: S) -> Option<&Card> {
match src.into() { match src.into() {
KlondikePile::Tableau(tableau) => match tableau { KlondikePile::Tableau(tableau) => self.tableau_face_up_cards(tableau).last(),
Tableau::Tableau1 => self.tableau1.face_up().last(),
Tableau::Tableau2 => self.tableau2.face_up().last(),
Tableau::Tableau3 => self.tableau3.face_up().last(),
Tableau::Tableau4 => self.tableau4.face_up().last(),
Tableau::Tableau5 => self.tableau5.face_up().last(),
Tableau::Tableau6 => self.tableau6.face_up().last(),
Tableau::Tableau7 => self.tableau7.face_up().last(),
},
KlondikePile::Foundation(foundation) => self.foundations[foundation as usize].last(), KlondikePile::Foundation(foundation) => self.foundations[foundation as usize].last(),
KlondikePile::Stock => self.stock.face_up().last(), KlondikePile::Stock => self.stock.face_up().last(),
} }
@@ -541,14 +535,14 @@ pub struct Klondike {
impl Klondike { impl Klondike {
pub fn with_seed(seed: u64) -> Self { pub fn with_seed(seed: u64) -> Self {
use rand::SeedableRng; use rand::SeedableRng;
let rng = Rng::seed_from_u64(seed); let mut rng = Rng::seed_from_u64(seed);
Self::with_rng(rng) Self::with_rng(&mut rng)
} }
pub fn with_rng(mut rng: Rng) -> Self { pub fn with_rng(rng: &mut Rng) -> Self {
// shuffle a new deck // shuffle a new deck
let mut deck = Stack::full_deck(card_game::Deck::Deck1); let mut deck = Stack::full_deck(card_game::Deck::Deck1);
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
deck.shuffle(&mut rng); deck.shuffle(rng);
let mut deck = deck.into_iter(); let mut deck = deck.into_iter();
// generate tableaus // generate tableaus
@@ -588,7 +582,7 @@ impl Klondike {
/// Check if the game should be auto-completed /// Check if the game should be auto-completed
pub fn is_win_trivial(&self) -> bool { pub fn is_win_trivial(&self) -> bool {
// all face down cards empty means win // all face down cards empty means win
self.state.stock.face_down().is_empty() self.state.stock.is_empty()
&& self.state.tableau1.face_down().is_empty() && self.state.tableau1.face_down().is_empty()
&& self.state.tableau2.face_down().is_empty() && self.state.tableau2.face_down().is_empty()
&& self.state.tableau3.face_down().is_empty() && self.state.tableau3.face_down().is_empty()
@@ -610,7 +604,7 @@ impl Klondike {
KlondikePileStack::Tableau(TableauStack { KlondikePileStack::Tableau(TableauStack {
tableau, tableau,
skip_cards: SkipCards::Skip0, skip_cards: SkipCards::Skip0,
}) if !self.state().is_tableau_face_down_empty(tableau) }) if !self.state().tableau_face_down_cards(tableau).is_empty()
|| self || self
.state() .state()
.stack_bottom_card(dst_tableau.src) .stack_bottom_card(dst_tableau.src)
@@ -625,12 +619,13 @@ impl Klondike {
KlondikeInstruction::RotateStock => 4, KlondikeInstruction::RotateStock => 4,
} }
} }
/// A list of possible moves sorted by a simple prioirty function /// A single move that usually makes progress towards a winning game
pub fn get_auto_move(&self) -> Option<KlondikeInstruction> { pub fn get_auto_move(&self) -> Option<KlondikeInstruction> {
self.possible_instructions() self.possible_instructions()
.filter(|ins| !ins.is_useless()) .filter(|ins| !ins.is_useless())
.min_by_key(|ins| self.instruction_priority(ins)) .min_by_key(|ins| self.instruction_priority(ins))
} }
/// A list of possible moves with useless moves filtered out and sorted by a simple priority function
pub fn get_sorted_moves(&self) -> Vec<KlondikeInstruction> { pub fn get_sorted_moves(&self) -> Vec<KlondikeInstruction> {
let mut useful_moves: Vec<_> = self let mut useful_moves: Vec<_> = self
.possible_instructions() .possible_instructions()