Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b277601ea | |||
| 418e422f12 | |||
| bc2d1b126e | |||
| 08e8656ecf | |||
| 73ffef76b0 | |||
| 0a34deb630 | |||
| bc05bbdc50 | |||
| f9012b01c4 | |||
| e18e242eae | |||
| 576489c226 | |||
| 90f8fe2e77 | |||
| 7f708df327 | |||
| 6bfa05c292 | |||
| da0dfe98c4 | |||
| b840d56725 | |||
| 37837e76c1 | |||
| e6113b6e91 | |||
| 5cb5cb9a50 | |||
| 446cd87665 | |||
| a9b93f3807 | |||
| 13bc36ce0c | |||
| 07c3dc6667 | |||
| 835a4dcc5f | |||
| ec25f11ca5 | |||
| f8dd9e008c | |||
| 90d46902ea | |||
| fd6b2a23ea |
Generated
+10
-1
@@ -22,7 +22,7 @@ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "card_game"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
@@ -132,6 +132,15 @@ dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "klondike-bench"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"card_game",
|
||||
"klondike",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "klondike-cli"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
members = [
|
||||
"card_game",
|
||||
"klondike",
|
||||
"klondike-bench",
|
||||
"klondike-cli",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.dependencies]
|
||||
card_game = { version = "0.2.0", path = "card_game", registry = "Quaternions" }
|
||||
klondike = { version = "0.1.0", path = "klondike", registry = "Quaternions" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
# unsafe_code = "forbid"
|
||||
# missing_docs = "warn"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "card_game"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
repository = "https://git.aleshym.co/Quaternions/card_game"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -10,3 +10,6 @@ keywords = ["card", "cards", "solitaire", "klondike"]
|
||||
|
||||
[dependencies]
|
||||
arrayvec = "0.7.6"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
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
|
||||
|
||||
|
||||
+93
-23
@@ -6,10 +6,10 @@ struct ReadmeDoctests;
|
||||
use core::ops::RangeBounds;
|
||||
|
||||
// TODO: pub struct ValidInstruction<I>(I);
|
||||
pub trait Game {
|
||||
type Stats;
|
||||
type Config;
|
||||
type Instruction;
|
||||
pub trait Game: Clone + core::fmt::Debug {
|
||||
type Stats: Clone + core::fmt::Debug;
|
||||
type Config: Clone + core::fmt::Debug;
|
||||
type Instruction: Clone + core::fmt::Debug;
|
||||
fn possible_instructions(&self) -> impl Iterator<Item = Self::Instruction> + use<Self>;
|
||||
fn is_instruction_valid(&self, config: &Self::Config, instruction: Self::Instruction) -> bool;
|
||||
fn process_instruction(
|
||||
@@ -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 suit ID
|
||||
/// 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)]
|
||||
pub struct Pile<const DN: usize, const UP: usize> {
|
||||
face_down: Stack<DN>,
|
||||
@@ -319,21 +321,36 @@ impl<S> SessionStats<S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Oom;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Session<G: Game> {
|
||||
stats: SessionStats<G::Stats>,
|
||||
config: G::Config,
|
||||
state: SessionState<G>,
|
||||
}
|
||||
#[derive(Clone, Eq, Hash, PartialEq)]
|
||||
pub struct SessionState<G: Game> {
|
||||
seed: G,
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StateSnapshot<G: Game> {
|
||||
state: G,
|
||||
history: Vec<G::Instruction>,
|
||||
instruction: G::Instruction,
|
||||
}
|
||||
impl<G: Game> StateSnapshot<G> {
|
||||
pub const fn state(&self) -> &G {
|
||||
&self.state
|
||||
}
|
||||
pub const fn instruction(&self) -> &G::Instruction {
|
||||
&self.instruction
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SessionState<G: Game> {
|
||||
state: G,
|
||||
history: Vec<StateSnapshot<G>>,
|
||||
}
|
||||
impl<G: Game + Clone> SessionState<G> {
|
||||
fn new(state: G) -> Self {
|
||||
Self {
|
||||
seed: state.clone(),
|
||||
state,
|
||||
history: Vec::new(),
|
||||
}
|
||||
@@ -343,6 +360,7 @@ impl<G: Game> Session<G>
|
||||
where
|
||||
G: Clone + Eq + core::hash::Hash,
|
||||
G::Stats: Clone + Default,
|
||||
G::Config: Clone,
|
||||
G::Instruction: Clone + Eq + core::hash::Hash,
|
||||
{
|
||||
pub fn new(state: G, config: G::Config) -> Self {
|
||||
@@ -367,7 +385,7 @@ where
|
||||
pub const fn config(&self) -> &G::Config {
|
||||
&self.config
|
||||
}
|
||||
pub fn history(&self) -> &[G::Instruction] {
|
||||
pub fn history(&self) -> &[StateSnapshot<G>] {
|
||||
&self.state.history
|
||||
}
|
||||
pub fn undo(&mut self) {
|
||||
@@ -387,12 +405,67 @@ where
|
||||
pub fn is_win(&self) -> bool {
|
||||
self.state.is_win()
|
||||
}
|
||||
pub fn is_winnable(&self) -> Result<Option<Vec<StateSnapshot<G>>>, Oom> {
|
||||
const HUGE_CAP: usize = 1 << 25;
|
||||
let mut state_moves = std::collections::HashMap::with_capacity(HUGE_CAP);
|
||||
let mut state = self.clone();
|
||||
while !state.is_win() {
|
||||
// don't look for empty hash map buckets when the hash map is 99% full!
|
||||
if HUGE_CAP * 127 <= state_moves.len() * 128 {
|
||||
return Err(Oom);
|
||||
}
|
||||
|
||||
// Continue existing iterator if it exists
|
||||
let it = state_moves
|
||||
.entry(state.state().clone())
|
||||
.or_insert_with(|| state.state().possible_instructions());
|
||||
|
||||
// Run one possible move
|
||||
if let Some(instruction) = it.next() {
|
||||
state.process_instruction(instruction);
|
||||
continue;
|
||||
}
|
||||
|
||||
// No more moves. If we can't undo we're done
|
||||
if state.history().is_empty() {
|
||||
return Ok(None);
|
||||
} else {
|
||||
state.undo();
|
||||
}
|
||||
}
|
||||
|
||||
// history includes cycles
|
||||
let mut state_index: std::collections::HashMap<_, _> = state
|
||||
.history()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, snapshot)| (snapshot.state().clone(), i))
|
||||
.collect();
|
||||
|
||||
// find the longest range where the start and end are the same state
|
||||
while let Some(longest_range) = state
|
||||
.history()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, snapshot)| {
|
||||
let &last_index = state_index.get(snapshot.state())?;
|
||||
let longness = last_index - index;
|
||||
(longness != 0).then_some(index..last_index)
|
||||
})
|
||||
.max_by_key(|range| range.len())
|
||||
{
|
||||
state.state.history.drain(longest_range);
|
||||
for (i, snapshot) in state.history().iter().enumerate() {
|
||||
state_index.insert(snapshot.state().clone(), i);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(state.state.history))
|
||||
}
|
||||
}
|
||||
impl<G: Game> Game for SessionState<G>
|
||||
where
|
||||
G: Clone,
|
||||
G::Stats: Default,
|
||||
G::Instruction: Clone,
|
||||
{
|
||||
type Stats = SessionStats<G::Stats>;
|
||||
type Config = G::Config;
|
||||
@@ -418,19 +491,16 @@ where
|
||||
) {
|
||||
match instruction {
|
||||
SessionInstruction::Undo => {
|
||||
// replay the entire history of the game except one move
|
||||
self.history.pop();
|
||||
let mut inner_stats = G::Stats::default();
|
||||
let mut state = self.seed.clone();
|
||||
for instruction in &self.history {
|
||||
state.process_instruction(&mut inner_stats, config, instruction.clone());
|
||||
}
|
||||
self.state = state;
|
||||
stats.inner_stats = inner_stats;
|
||||
if let Some(snapshot) = self.history.pop() {
|
||||
self.state = snapshot.state;
|
||||
stats.increment_undos();
|
||||
}
|
||||
}
|
||||
SessionInstruction::InnerInstruction(instruction) => {
|
||||
self.history.push(instruction.clone());
|
||||
self.history.push(StateSnapshot {
|
||||
state: self.state.clone(),
|
||||
instruction: instruction.clone(),
|
||||
});
|
||||
self.state
|
||||
.process_instruction(&mut stats.inner_stats, config, instruction);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -4,6 +4,9 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
card_game = { version = "0.1.0", path = "../card_game" }
|
||||
klondike = { version = "0.1.0", path = "../klondike" }
|
||||
card_game.workspace = true
|
||||
klondike.workspace = true
|
||||
rand = { version = "0.10.1", default-features = false, features = ["thread_rng"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -4,8 +4,8 @@ use klondike::{
|
||||
KlondikePile, KlondikePileStack, KlondikeStats, SkipCards, Tableau, TableauStack,
|
||||
};
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod test;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
use std::fmt::Display;
|
||||
struct Displayed<T>(T);
|
||||
@@ -212,51 +212,6 @@ fn find_valid_instruction(
|
||||
.then_some(instruction)
|
||||
}
|
||||
|
||||
fn get_good_move(state: &Klondike) -> Option<KlondikeInstruction> {
|
||||
fn useless_moves(instruction: &KlondikeInstruction) -> bool {
|
||||
!matches!(
|
||||
instruction,
|
||||
// foundation -> foundation is a useless move
|
||||
KlondikeInstruction::DstFoundation(DstFoundation {
|
||||
src: KlondikePile::Foundation(_),
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
fn instruction_priority(state: &Klondike, instruction: &KlondikeInstruction) -> usize {
|
||||
// 1 Move into foundation
|
||||
// 2 T->T Move to reveal new card (moving a non-king to reveal empty tableau also counts)
|
||||
// 3 Move from stock
|
||||
// 4 Rotate stock
|
||||
// 5 T->T Move not revealing new card
|
||||
// 6 Move from foundation
|
||||
match instruction {
|
||||
KlondikeInstruction::DstFoundation(_) => 1,
|
||||
&KlondikeInstruction::DstTableau(dst_tableau) => match dst_tableau.src {
|
||||
KlondikePileStack::Tableau(TableauStack {
|
||||
tableau,
|
||||
skip_cards: SkipCards::Skip0,
|
||||
}) if !state.state().is_tableau_face_down_empty(tableau)
|
||||
|| state
|
||||
.state()
|
||||
.stack_bottom_card(dst_tableau.src)
|
||||
.is_some_and(|card| card.rank() != Rank::King) =>
|
||||
{
|
||||
2
|
||||
}
|
||||
KlondikePileStack::Stock => 3,
|
||||
KlondikePileStack::Tableau(_) => 5,
|
||||
KlondikePileStack::Foundation(_) => 6,
|
||||
},
|
||||
KlondikeInstruction::RotateStock => 4,
|
||||
}
|
||||
}
|
||||
state
|
||||
.possible_instructions()
|
||||
.filter(useless_moves)
|
||||
.min_by_key(|ins| instruction_priority(state, ins))
|
||||
}
|
||||
|
||||
fn main() -> Result<(), std::io::Error> {
|
||||
use rand::RngExt;
|
||||
let mut rng = rand::rng();
|
||||
@@ -297,7 +252,7 @@ fn main() -> Result<(), std::io::Error> {
|
||||
}
|
||||
}
|
||||
SessionInstruction::Auto => {
|
||||
if let Some(instruction) = get_good_move(session.state()) {
|
||||
if let Some(instruction) = session.state().get_auto_move() {
|
||||
session.process_instruction(instruction);
|
||||
} else {
|
||||
println!("No valid moves!");
|
||||
|
||||
@@ -1,33 +1,15 @@
|
||||
use klondike::Klondike;
|
||||
use card_game::Session;
|
||||
use klondike::Klondike;
|
||||
#[test]
|
||||
fn test_is_winnable() {
|
||||
// is winnable
|
||||
let is_winnable = Session::new_default(Klondike::with_seed(123)).is_winnable();
|
||||
println!("is_winnable = {is_winnable:?}");
|
||||
}
|
||||
#[test]
|
||||
fn test_klondike() {
|
||||
// create game session
|
||||
let game = Klondike::with_seed(123);
|
||||
let mut session = Session::new_default(game);
|
||||
|
||||
// is winnable
|
||||
let is_winnable = session.is_winnable();
|
||||
println!("is_winnable = {is_winnable:?}");
|
||||
|
||||
// play game
|
||||
while let Some(instruction) = session.possible_instructions().next() {
|
||||
session.process_instruction(instruction);
|
||||
let is_winnable = Session::new_default(Klondike::with_seed(0)).is_winnable().unwrap();
|
||||
if let Some(win_moves) = is_winnable {
|
||||
// for (i, ins) in win_moves.into_iter().enumerate() {
|
||||
// println!("{i} = {:?}", ins.instruction());
|
||||
// }
|
||||
println!("Game is winnable with {} moves", win_moves.len());
|
||||
} else {
|
||||
println!("Game is not winnable");
|
||||
}
|
||||
|
||||
// did win
|
||||
let is_win = session.is_win();
|
||||
|
||||
// print session history
|
||||
for (i, instruction) in session.history().iter().enumerate() {
|
||||
println!("move {i} = {instruction:?}");
|
||||
}
|
||||
|
||||
println!("is_win = {is_win}");
|
||||
}
|
||||
|
||||
+4
-1
@@ -4,5 +4,8 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
card_game = { version = "0.1.0", path = "../card_game" }
|
||||
card_game.workspace = true
|
||||
rand = { version = "0.10.1", default-features = false, features = ["std_rng"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
```rust
|
||||
use card_game::{Session, Game};
|
||||
use card_game::Session;
|
||||
use klondike::Klondike;
|
||||
|
||||
// create game session
|
||||
@@ -14,7 +14,7 @@ let game = Klondike::with_seed(123);
|
||||
let mut session = Session::new_default(game);
|
||||
|
||||
// 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);
|
||||
|
||||
// quit after 1000 moves
|
||||
|
||||
+73
-4
@@ -299,6 +299,16 @@ impl KlondikeInstruction {
|
||||
Self::RotateStock => return None,
|
||||
})
|
||||
}
|
||||
/// foundation -> foundation is a useless move
|
||||
pub fn is_useless(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
KlondikeInstruction::DstFoundation(DstFoundation {
|
||||
src: KlondikePile::Foundation(_),
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const TABLEAUS: usize = 7;
|
||||
@@ -523,6 +533,10 @@ impl Iterator for KlondikeIter {
|
||||
instruction
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_klondike_iter() {
|
||||
assert_eq!(KlondikeIter::new().count(), 721);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Klondike {
|
||||
@@ -531,14 +545,14 @@ pub struct Klondike {
|
||||
impl Klondike {
|
||||
pub fn with_seed(seed: u64) -> Self {
|
||||
use rand::SeedableRng;
|
||||
let rng = Rng::seed_from_u64(seed);
|
||||
Self::with_rng(rng)
|
||||
let mut rng = Rng::seed_from_u64(seed);
|
||||
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
|
||||
let mut deck = Stack::full_deck(card_game::Deck::Deck1);
|
||||
use rand::seq::SliceRandom;
|
||||
deck.shuffle(&mut rng);
|
||||
deck.shuffle(rng);
|
||||
let mut deck = deck.into_iter();
|
||||
|
||||
// generate tableaus
|
||||
@@ -575,6 +589,61 @@ impl Klondike {
|
||||
pub const fn state(&self) -> &KlondikeState {
|
||||
&self.state
|
||||
}
|
||||
/// Check if the game should be auto-completed
|
||||
pub fn is_win_trivial(&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()
|
||||
}
|
||||
fn instruction_priority(&self, instruction: &KlondikeInstruction) -> usize {
|
||||
// 1 Move into foundation
|
||||
// 2 T->T Move to reveal new card (moving a non-king to reveal empty tableau also counts)
|
||||
// 3 Move from stock
|
||||
// 4 Rotate stock
|
||||
// 5 T->T Move not revealing new card
|
||||
// 6 Move from foundation
|
||||
match instruction {
|
||||
KlondikeInstruction::DstFoundation(_) => 1,
|
||||
&KlondikeInstruction::DstTableau(dst_tableau) => match dst_tableau.src {
|
||||
KlondikePileStack::Tableau(TableauStack {
|
||||
tableau,
|
||||
skip_cards: SkipCards::Skip0,
|
||||
}) if !self.state().is_tableau_face_down_empty(tableau)
|
||||
|| self
|
||||
.state()
|
||||
.stack_bottom_card(dst_tableau.src)
|
||||
.is_some_and(|card| card.rank() != Rank::King) =>
|
||||
{
|
||||
2
|
||||
}
|
||||
KlondikePileStack::Stock => 3,
|
||||
KlondikePileStack::Tableau(_) => 5,
|
||||
KlondikePileStack::Foundation(_) => 6,
|
||||
},
|
||||
KlondikeInstruction::RotateStock => 4,
|
||||
}
|
||||
}
|
||||
/// A single move that usually makes progress towards a winning game
|
||||
pub fn get_auto_move(&self) -> Option<KlondikeInstruction> {
|
||||
self.possible_instructions()
|
||||
.filter(|ins| !ins.is_useless())
|
||||
.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> {
|
||||
let mut useful_moves: Vec<_> = self
|
||||
.possible_instructions()
|
||||
.filter(|ins| !ins.is_useless())
|
||||
.collect();
|
||||
useful_moves.sort_by_key(|ins| self.instruction_priority(ins));
|
||||
useful_moves
|
||||
}
|
||||
}
|
||||
|
||||
impl Game for Klondike {
|
||||
|
||||
Reference in New Issue
Block a user