7 Commits

Author SHA1 Message Date
Quaternions eac07676dd remove separate trait 2026-07-08 09:34:49 -07:00
Quaternions 1245ba1947 doc 2026-07-07 19:03:01 -07:00
Quaternions b75c35be84 cleanups & docs 2026-07-07 18:59:12 -07:00
Quaternions 4cd41fbdee minimize diff 2026-07-07 18:52:01 -07:00
Quaternions 984972c126 cheat 2026-07-07 18:50:47 -07:00
Quaternions 8c64583c00 implement staging api 2026-07-07 18:47:24 -07:00
Quaternions a511679a47 add staging api 2026-07-07 18:39:22 -07:00
6 changed files with 106 additions and 53 deletions
+72 -26
View File
@@ -5,7 +5,6 @@ struct ReadmeDoctests;
use core::ops::RangeBounds; use core::ops::RangeBounds;
// TODO: pub struct ValidInstruction<I>(I);
pub trait Game: Clone { pub trait Game: Clone {
type Score: Clone + core::fmt::Debug; type Score: Clone + core::fmt::Debug;
type Stats: Clone + core::fmt::Debug; type Stats: Clone + core::fmt::Debug;
@@ -16,14 +15,51 @@ pub trait Game: Clone {
&self, &self,
config: &Self::Config, config: &Self::Config,
) -> impl Iterator<Item = Self::Instruction> + use<Self>; ) -> impl Iterator<Item = Self::Instruction> + use<Self>;
fn is_instruction_valid(&self, config: &Self::Config, instruction: Self::Instruction) -> bool; fn is_instruction_valid(&self, config: &Self::Config, instruction: &Self::Instruction) -> bool;
fn process_instruction( fn process_instruction(valid_instruction: StagedInstruction<'_, Self>);
&mut self,
stats: &mut Self::Stats,
config: &Self::Config,
instruction: Self::Instruction,
);
fn is_win(&self) -> bool; fn is_win(&self) -> bool;
/// Validate an instruction to prepare to run it.
fn stage_instruction<'a>(
&'a mut self,
stats: &'a mut Self::Stats,
config: &'a Self::Config,
instruction: Self::Instruction,
) -> Option<StagedInstruction<'a, Self>> {
self.is_instruction_valid(config, &instruction)
.then_some(StagedInstruction {
game: self,
stats,
config,
instruction,
})
}
}
/// Call .commit() to run the staged instruction.
/// Cannot be constructed other than by Game::stage_instruction.
pub struct StagedInstruction<'a, G: Game> {
game: &'a mut G,
stats: &'a mut G::Stats,
config: &'a G::Config,
instruction: G::Instruction,
}
impl<'a, G: Game> StagedInstruction<'a, G> {
/// Consume the staged instruction. Use this in your implementation of InstructionConsumer::process_instruction for your game.
pub fn consume(self) -> (&'a mut G, &'a mut G::Stats, &'a G::Config, G::Instruction) {
let StagedInstruction {
game,
stats,
config,
instruction,
} = self;
(game, stats, config, instruction)
}
}
impl<G: Game> StagedInstruction<'_, G> {
/// Process the staged instruction.
pub fn commit(self) {
G::process_instruction(self);
}
} }
/// card_game supports up to 4 identifiably separate decks. /// card_game supports up to 4 identifiably separate decks.
@@ -542,14 +578,21 @@ where
&self.state.history &self.state.history
} }
pub fn undo(&mut self) { pub fn undo(&mut self) {
if let Some(staged) =
self.state self.state
.process_instruction(&mut self.stats, &self.config, SessionInstruction::Undo) .stage_instruction(&mut self.stats, &self.config, SessionInstruction::Undo)
{
staged.commit();
}
} }
pub fn possible_instructions(&self) -> impl Iterator<Item = G::Instruction> + use<G> { pub fn possible_instructions(&self) -> impl Iterator<Item = G::Instruction> + use<G> {
self.state.state.possible_instructions(&self.config.inner) self.state.state.possible_instructions(&self.config.inner)
} }
pub fn process_instruction(&mut self, instruction: G::Instruction) { pub fn stage_instruction(
self.state.process_instruction( &mut self,
instruction: G::Instruction,
) -> Option<StagedInstruction<'_, SessionState<G>>> {
self.state.stage_instruction(
&mut self.stats, &mut self.stats,
&self.config, &self.config,
SessionInstruction::InnerInstruction(instruction), SessionInstruction::InnerInstruction(instruction),
@@ -583,7 +626,8 @@ where
// Run one possible move // Run one possible move
if let Some(instruction) = it.next() { if let Some(instruction) = it.next() {
session.process_instruction(instruction); // TODO: no unwrap
session.stage_instruction(instruction).unwrap().commit();
continue; continue;
} }
@@ -618,7 +662,7 @@ where
.possible_instructions(&config.inner) .possible_instructions(&config.inner)
.map(SessionInstruction::InnerInstruction) .map(SessionInstruction::InnerInstruction)
} }
fn is_instruction_valid(&self, config: &Self::Config, instruction: Self::Instruction) -> bool { fn is_instruction_valid(&self, config: &Self::Config, instruction: &Self::Instruction) -> bool {
match instruction { match instruction {
SessionInstruction::Undo => !self.history.is_empty(), SessionInstruction::Undo => !self.history.is_empty(),
SessionInstruction::InnerInstruction(instruction) => { SessionInstruction::InnerInstruction(instruction) => {
@@ -626,26 +670,27 @@ where
} }
} }
} }
fn process_instruction( fn process_instruction(valid_instruction: StagedInstruction<'_, Self>) {
&mut self, let (game, stats, config, instruction) = valid_instruction.consume();
stats: &mut Self::Stats,
config: &Self::Config,
instruction: Self::Instruction,
) {
match instruction { match instruction {
SessionInstruction::Undo => { SessionInstruction::Undo => {
if let Some(snapshot) = self.history.pop() { if let Some(snapshot) = game.history.pop() {
self.state = snapshot.state; game.state = snapshot.state;
stats.increment_undos(); stats.increment_undos();
} }
} }
SessionInstruction::InnerInstruction(instruction) => { SessionInstruction::InnerInstruction(instruction) => {
self.history.push(StateSnapshot { game.history.push(StateSnapshot {
state: self.state.clone(), state: game.state.clone(),
instruction: instruction.clone(), instruction: instruction.clone(),
}); });
self.state // This might be considered cheaing a bit, we're pre-approving the instruction here by accessing the inner fields.
.process_instruction(&mut stats.inner, &config.inner, instruction); G::process_instruction(StagedInstruction {
game: &mut game.state,
stats: &mut stats.inner,
config: &config.inner,
instruction,
});
} }
} }
} }
@@ -681,7 +726,8 @@ where
let serialized = SerializedSession::deserialize(deserializer)?; let serialized = SerializedSession::deserialize(deserializer)?;
let mut session = Session::new(serialized.initial_state, serialized.config); let mut session = Session::new(serialized.initial_state, serialized.config);
for instruction in serialized.instructions { for instruction in serialized.instructions {
session.process_instruction(instruction); // TODO: no unwrap
session.stage_instruction(instruction).unwrap().commit();
} }
Ok(session) Ok(session)
} }
+3 -1
View File
@@ -21,7 +21,9 @@ fn play_to_win(rng: &mut Rng) -> Option<KlondikeStats> {
return None; return None;
} }
game.process_instruction(&mut stats, &CONFIG, instruction); game.stage_instruction(&mut stats, &CONFIG, instruction)
.unwrap()
.commit();
} }
game.is_win().then_some(stats) game.is_win().then_some(stats)
} }
+8 -5
View File
@@ -210,7 +210,7 @@ fn find_valid_instruction(
}); });
let instruction = let instruction =
KlondikeInstruction::DstTableau(DstTableau { tableau, src }); KlondikeInstruction::DstTableau(DstTableau { tableau, src });
if state.is_instruction_valid(config, instruction) { if state.is_instruction_valid(config, &instruction) {
return Some(instruction); return Some(instruction);
} }
} }
@@ -228,7 +228,7 @@ fn find_valid_instruction(
_ => return None, _ => return None,
}; };
state state
.is_instruction_valid(config, instruction) .is_instruction_valid(config, &instruction)
.then_some(instruction) .then_some(instruction)
} }
@@ -277,13 +277,16 @@ fn main() -> Result<(), std::io::Error> {
.state() .state()
.get_auto_move(&session.config().inner) .get_auto_move(&session.config().inner)
{ {
session.process_instruction(instruction); session.stage_instruction(instruction).unwrap().commit();
} else { } else {
println!("No valid moves!"); println!("No valid moves!");
} }
} }
SessionInstruction::Stock => { SessionInstruction::Stock => {
session.process_instruction(KlondikeInstruction::RotateStock) session
.stage_instruction(KlondikeInstruction::RotateStock)
.unwrap()
.commit();
} }
SessionInstruction::Klondike(naive_instruction) => { SessionInstruction::Klondike(naive_instruction) => {
if let Some(instruction) = find_valid_instruction( if let Some(instruction) = find_valid_instruction(
@@ -291,7 +294,7 @@ fn main() -> Result<(), std::io::Error> {
session.state().state(), session.state().state(),
naive_instruction, naive_instruction,
) { ) {
session.process_instruction(instruction); session.stage_instruction(instruction).unwrap().commit();
} else { } else {
println!("Invalid move!"); println!("Invalid move!");
} }
+1 -1
View File
@@ -16,7 +16,7 @@ let mut session = Session::new_default(game);
// play game a bit // play game a bit
while let Some(instruction) = session.state().state().get_auto_move(&config) { while let Some(instruction) = session.state().state().get_auto_move(&config) {
session.process_instruction(instruction); session.stage_instruction(instruction).unwrap().commit();
// quit after 200 moves or win // quit after 200 moves or win
if session.is_win() || 200 < session.stats().stats().moves() { if session.is_win() || 200 < session.stats().stats().moves() {
+13 -17
View File
@@ -1,6 +1,6 @@
pub type Rng = rand::rngs::StdRng; pub type Rng = rand::rngs::StdRng;
use card_game::{Card, Game, Pile, Rank, Stack}; use card_game::{Card, Game, Pile, Rank, Stack, StagedInstruction};
#[cfg(test)] #[cfg(test)]
mod test; mod test;
@@ -621,7 +621,7 @@ impl KlondikeState {
pub fn is_instruction_valid( pub fn is_instruction_valid(
&self, &self,
config: &KlondikeConfig, config: &KlondikeConfig,
instruction: KlondikeInstruction, instruction: &KlondikeInstruction,
) -> bool { ) -> bool {
match instruction { match instruction {
// Stock -> Stock draws a card or resets the stock // Stock -> Stock draws a card or resets the stock
@@ -830,38 +830,34 @@ impl Game for Klondike {
let state = self.state.clone(); let state = self.state.clone();
let config = config.clone(); let config = config.clone();
KlondikeIter::new() KlondikeIter::new()
.filter(move |&instruction| state.is_instruction_valid(&config, instruction)) .filter(move |instruction| state.is_instruction_valid(&config, instruction))
} }
fn is_instruction_valid(&self, config: &Self::Config, instruction: Self::Instruction) -> bool { fn is_instruction_valid(&self, config: &Self::Config, instruction: &Self::Instruction) -> bool {
self.state.is_instruction_valid(config, instruction) self.state.is_instruction_valid(config, instruction)
} }
fn process_instruction( fn process_instruction(valid_instruction: StagedInstruction<'_, Self>) {
&mut self, let (game, stats, config, instruction) = valid_instruction.consume();
stats: &mut Self::Stats,
config: &Self::Config,
instruction: Self::Instruction,
) {
stats.increment_moves(); stats.increment_moves();
match instruction { match instruction {
// Reset the stock if it's empty // Reset the stock if it's empty
KlondikeInstruction::RotateStock => { KlondikeInstruction::RotateStock => {
if self.state.stock.face_down().is_empty() { if game.state.stock.face_down().is_empty() {
self.state.stock.flip_it_and_reverse_it(); game.state.stock.flip_it_and_reverse_it();
stats.increment_recycle_count(); stats.increment_recycle_count();
} else { } else {
for _ in 0..config.draw_stock as usize { for _ in 0..config.draw_stock as usize {
self.state.stock.flip_up(); game.state.stock.flip_up();
} }
} }
} }
// Move a card from anywhere to a foundation // Move a card from anywhere to a foundation
KlondikeInstruction::DstFoundation(DstFoundation { src, foundation }) => { KlondikeInstruction::DstFoundation(DstFoundation { src, foundation }) => {
stats.increment_move_to_foundation(); stats.increment_move_to_foundation();
let (card, did_flip_up) = self.state.take_top_card_flip_up(src); let (card, did_flip_up) = game.state.take_top_card_flip_up(src);
if did_flip_up { if did_flip_up {
stats.increment_flip_up_bonus(); stats.increment_flip_up_bonus();
} }
self.state.extend_foundation(foundation, card); game.state.extend_foundation(foundation, card);
} }
// Move a stack of cards from anywhere to a tableau // Move a stack of cards from anywhere to a tableau
KlondikeInstruction::DstTableau(DstTableau { src, tableau }) => { KlondikeInstruction::DstTableau(DstTableau { src, tableau }) => {
@@ -870,11 +866,11 @@ impl Game for Klondike {
KlondikePileStack::Foundation(_) => stats.increment_move_from_foundation(), KlondikePileStack::Foundation(_) => stats.increment_move_from_foundation(),
KlondikePileStack::Tableau(_) => {} KlondikePileStack::Tableau(_) => {}
} }
let (cards, did_flip_up) = self.state.take_stack_flip_up(src); let (cards, did_flip_up) = game.state.take_stack_flip_up(src);
if did_flip_up { if did_flip_up {
stats.increment_flip_up_bonus(); stats.increment_flip_up_bonus();
} }
self.state.extend_tableau(tableau, cards); game.state.extend_tableau(tableau, cards);
} }
} }
} }
+8 -2
View File
@@ -23,7 +23,10 @@ fn test_json() {
let solution_result = session.solve(); let solution_result = session.solve();
if let Ok(Some(solution)) = solution_result { if let Ok(Some(solution)) = solution_result {
for snapshot in solution.clean_solution() { for snapshot in solution.clean_solution() {
session.process_instruction(snapshot.instruction().clone()); session
.stage_instruction(snapshot.instruction().clone())
.unwrap()
.commit();
} }
} }
let serialized = serde_json::to_string(&session).unwrap(); let serialized = serde_json::to_string(&session).unwrap();
@@ -42,7 +45,10 @@ fn test_rmp() {
let solution_result = session.solve(); let solution_result = session.solve();
if let Ok(Some(solution)) = solution_result { if let Ok(Some(solution)) = solution_result {
for snapshot in solution.clean_solution() { for snapshot in solution.clean_solution() {
session.process_instruction(snapshot.instruction().clone()); session
.stage_instruction(snapshot.instruction().clone())
.unwrap()
.commit();
} }
} }
let serialized = rmp_serde::to_vec(&session).unwrap(); let serialized = rmp_serde::to_vec(&session).unwrap();