From a511679a47f4928cf187b3686a951bf2b6e17c8f Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 7 Jul 2026 18:39:22 -0700 Subject: [PATCH] add staging api --- card_game/src/lib.rs | 117 ++++++++++++++++++++++++++++++++----------- 1 file changed, 87 insertions(+), 30 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 9da45c4..8cb1887 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -5,25 +5,67 @@ struct ReadmeDoctests; use core::ops::RangeBounds; -// TODO: pub struct ValidInstruction(I); pub trait Game: Clone { type Score: Clone + core::fmt::Debug; type Stats: Clone + core::fmt::Debug; type Config: Clone + core::fmt::Debug; type Instruction: Clone + core::fmt::Debug; fn score(&self, stats: &Self::Stats, config: &Self::Config) -> Self::Score; + fn is_instruction_valid(&self, config: &Self::Config, instruction: &Self::Instruction) -> bool; fn possible_instructions( &self, config: &Self::Config, ) -> impl Iterator + use; - fn is_instruction_valid(&self, config: &Self::Config, instruction: Self::Instruction) -> bool; - fn process_instruction( - &mut self, - stats: &mut Self::Stats, - config: &Self::Config, - instruction: Self::Instruction, - ); fn is_win(&self) -> bool; + fn stage_instruction<'a>( + &'a mut self, + stats: &'a mut Self::Stats, + config: &'a Self::Config, + instruction: Self::Instruction, + ) -> Option> { + if self.is_instruction_valid(config, &instruction) { + Some(StagedInstruction { + game: self, + stats, + config, + instruction, + }) + } else { + None + } + } +} + +/// Call .commit() to run the staged 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> { + 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 StagedInstruction<'_, G> +where + G: InstructionConsumer, +{ + pub fn commit(self) { + G::process_instruction(self); + } +} + +/// Apply an atomic state update +pub trait InstructionConsumer: Game { + fn process_instruction(valid_instruction: StagedInstruction<'_, Self>); } /// card_game supports up to 4 identifiably separate decks. @@ -515,6 +557,7 @@ where G: Eq + core::hash::Hash, G::Stats: Default, G::Instruction: Eq + core::hash::Hash, + G: InstructionConsumer, { pub fn new(state: G, config: SessionConfig) -> Self { Self { @@ -542,14 +585,21 @@ where &self.state.history } pub fn undo(&mut self) { - self.state - .process_instruction(&mut self.stats, &self.config, SessionInstruction::Undo) + if let Some(staged) = + self.state + .stage_instruction(&mut self.stats, &self.config, SessionInstruction::Undo) + { + staged.commit(); + } } pub fn possible_instructions(&self) -> impl Iterator + use { self.state.state.possible_instructions(&self.config.inner) } - pub fn process_instruction(&mut self, instruction: G::Instruction) { - self.state.process_instruction( + pub fn stage_instruction( + &mut self, + instruction: G::Instruction, + ) -> Option>> { + self.state.stage_instruction( &mut self.stats, &self.config, SessionInstruction::InnerInstruction(instruction), @@ -583,7 +633,8 @@ where // Run one possible move if let Some(instruction) = it.next() { - session.process_instruction(instruction); + // TODO: no unwrap + session.stage_instruction(instruction).unwrap().commit(); continue; } @@ -618,7 +669,7 @@ where .possible_instructions(&config.inner) .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 { SessionInstruction::Undo => !self.history.is_empty(), SessionInstruction::InnerInstruction(instruction) => { @@ -626,32 +677,36 @@ where } } } - fn process_instruction( - &mut self, - stats: &mut Self::Stats, - config: &Self::Config, - instruction: Self::Instruction, - ) { + fn is_win(&self) -> bool { + self.state.is_win() + } +} +impl InstructionConsumer for SessionState +where + G: Game, + G: InstructionConsumer, +{ + fn process_instruction(valid_instruction: StagedInstruction<'_, Self>) { + let (game, stats, config, instruction) = valid_instruction.consume(); match instruction { SessionInstruction::Undo => { - if let Some(snapshot) = self.history.pop() { - self.state = snapshot.state; + if let Some(snapshot) = game.history.pop() { + game.state = snapshot.state; stats.increment_undos(); } } SessionInstruction::InnerInstruction(instruction) => { - self.history.push(StateSnapshot { - state: self.state.clone(), + game.history.push(StateSnapshot { + state: game.state.clone(), instruction: instruction.clone(), }); - self.state - .process_instruction(&mut stats.inner, &config.inner, instruction); + game.state + .stage_instruction(&mut stats.inner, &config.inner, instruction) + .unwrap() + .commit(); } } } - fn is_win(&self) -> bool { - self.state.is_win() - } } #[cfg(feature = "serde")] @@ -660,6 +715,7 @@ where G: serde::Deserialize<'de> + Clone + Eq + core::hash::Hash, G::Stats: Default, G::Instruction: serde::Deserialize<'de> + Eq + core::hash::Hash, + G: InstructionConsumer, SessionConfig: serde::Deserialize<'de>, Vec: serde::Deserialize<'de>, { @@ -681,7 +737,8 @@ where let serialized = SerializedSession::deserialize(deserializer)?; let mut session = Session::new(serialized.initial_state, serialized.config); for instruction in serialized.instructions { - session.process_instruction(instruction); + // TODO: no unwrap + session.stage_instruction(instruction).unwrap().commit(); } Ok(session) }