From 60728060a31939fe1b6fc977b4563ea8f2e896bd Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sat, 30 May 2026 09:59:58 -0700 Subject: [PATCH 01/25] serde for Card, Stack, Pile --- Cargo.lock | 2 ++ card_game/Cargo.toml | 6 ++++++ card_game/src/lib.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 343cacc..f91f9e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,6 +25,8 @@ name = "card_game" version = "0.4.0" dependencies = [ "arrayvec", + "serde", + "serde_derive", ] [[package]] diff --git a/card_game/Cargo.toml b/card_game/Cargo.toml index 051d596..64feed5 100644 --- a/card_game/Cargo.toml +++ b/card_game/Cargo.toml @@ -10,6 +10,12 @@ keywords = ["card", "cards", "solitaire", "klondike"] [dependencies] arrayvec = { version = "0.7.6", registry = "Quaternions", features = ["len_u8"], default-features = false } +serde = { version = "1.0.228", default-features = false, optional = true } +serde_derive = { version = "1.0.228", default-features = false, optional = true } [lints] workspace = true + +[features] +default = ["serde"] +serde = ["dep:serde", "dep:serde_derive"] diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index ab698e0..70f16a0 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -146,6 +146,11 @@ impl Rank { /// 2 bits for suit ID /// 4 bits for card Value #[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize), + serde(transparent) +)] pub struct Card(core::num::NonZeroU8); impl Card { pub const fn new(deck: Deck, suit: Suit, rank: Rank) -> Self { @@ -229,9 +234,54 @@ impl IntoIterator for Stack { self.0.into_iter() } } +#[cfg(feature = "serde")] +impl<'de, const CAP: usize> serde::Deserialize<'de> for Stack { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct StackVisitor; + impl<'de, const CAP: usize> serde::de::Visitor<'de> for StackVisitor { + type Value = Stack; + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Card Stack") + } + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut stack = Stack::new(); + while let Some(card) = seq.next_element()? { + // TODO: Error + stack.try_push(card).unwrap(); + } + Ok(stack) + } + } + deserializer.deserialize_seq(StackVisitor) + } +} +#[cfg(feature = "serde")] +impl serde::Serialize for Stack { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(self.len()))?; + for card in self.as_slice() { + seq.serialize_element(card)?; + } + seq.end() + } +} /// A pile is a stack of face down cards and a stack of face up cards. #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize) +)] pub struct Pile { face_down: Stack, face_up: Stack, -- 2.47.3 From e256e60e91d0b282ccc38565b66fcad8e7d3100c Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sat, 30 May 2026 10:00:06 -0700 Subject: [PATCH 02/25] serde for Session --- card_game/src/lib.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 70f16a0..0dd30bb 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -422,6 +422,10 @@ pub enum SessionInstruction { } #[derive(Clone, Debug, Default)] +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize) +)] pub struct SessionStats { inner: S, undos: u32, @@ -438,6 +442,10 @@ impl SessionStats { } } #[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize) +)] pub struct SessionConfig { pub inner: C, pub undo_penalty: i32, @@ -461,12 +469,26 @@ impl Default for SessionConfig { } #[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize), + serde(bound = " + G: serde::Serialize + for <'a> serde::Deserialize<'a>, + G::Stats: serde::Serialize + for <'a> serde::Deserialize<'a>, + G::Config: serde::Serialize + for <'a> serde::Deserialize<'a>, + Vec>: serde::Serialize + for <'a> serde::Deserialize<'a>, + ") +)] pub struct Session { stats: SessionStats, config: SessionConfig, state: SessionState, } #[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize) +)] pub struct StateSnapshot { state: G, instruction: G::Instruction, @@ -480,6 +502,14 @@ impl StateSnapshot { } } #[derive(Clone, Debug)] +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize), + serde(bound = " + G: serde::Serialize + for <'a> serde::Deserialize<'a>, + Vec>: serde::Serialize + for <'a> serde::Deserialize<'a>, + ") +)] pub struct SessionState { state: G, history: Vec>, -- 2.47.3 From cc4824732a229b17c352b7071f003eecb54c2869 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sat, 30 May 2026 10:13:40 -0700 Subject: [PATCH 03/25] a --- card_game/src/lib.rs | 54 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 0dd30bb..6239339 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -476,7 +476,6 @@ impl Default for SessionConfig { G: serde::Serialize + for <'a> serde::Deserialize<'a>, G::Stats: serde::Serialize + for <'a> serde::Deserialize<'a>, G::Config: serde::Serialize + for <'a> serde::Deserialize<'a>, - Vec>: serde::Serialize + for <'a> serde::Deserialize<'a>, ") )] pub struct Session { @@ -502,14 +501,6 @@ impl StateSnapshot { } } #[derive(Clone, Debug)] -#[cfg_attr( - feature = "serde", - derive(serde_derive::Deserialize, serde_derive::Serialize), - serde(bound = " - G: serde::Serialize + for <'a> serde::Deserialize<'a>, - Vec>: serde::Serialize + for <'a> serde::Deserialize<'a>, - ") -)] pub struct SessionState { state: G, history: Vec>, @@ -670,3 +661,48 @@ where self.state.is_win() } } + +#[cfg(feature = "serde")] +impl<'de, G: Game> serde::Deserialize<'de> for SessionState +where + G: serde::Serialize + for<'a> serde::Deserialize<'a>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SessionStateVisitor(core::marker::PhantomData); + impl<'de, G: Game> serde::de::Visitor<'de> for SessionStateVisitor { + type Value = SessionState; + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "State History") + } + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut stack = Stack::new(); + while let Some(card) = seq.next_element()? { + // TODO: Error + stack.try_push(card).unwrap(); + } + Ok(stack) + } + } + deserializer.deserialize_seq(SessionStateVisitor(core::marker::PhantomData)) + } +} +#[cfg(feature = "serde")] +impl serde::Serialize for SessionState { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(self.len()))?; + for card in self.as_slice() { + seq.serialize_element(card)?; + } + seq.end() + } +} -- 2.47.3 From fadefe93c27f4c00b16a0b3c9b308180cea02ee2 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sat, 30 May 2026 10:24:24 -0700 Subject: [PATCH 04/25] what is up with serde moving the deserializer --- card_game/src/lib.rs | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 6239339..0c2db52 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -665,13 +665,16 @@ where #[cfg(feature = "serde")] impl<'de, G: Game> serde::Deserialize<'de> for SessionState where - G: serde::Serialize + for<'a> serde::Deserialize<'a>, + G: serde::Deserialize<'de>, + G::Instruction: serde::Deserialize<'de>, { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - struct SessionStateVisitor(core::marker::PhantomData); + struct SessionStateVisitor { + state: G, + } impl<'de, G: Game> serde::de::Visitor<'de> for SessionStateVisitor { type Value = SessionState; fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -681,27 +684,44 @@ where where A: serde::de::SeqAccess<'de>, { - let mut stack = Stack::new(); - while let Some(card) = seq.next_element()? { - // TODO: Error - stack.try_push(card).unwrap(); + let mut state = self.state.clone(); + let mut history = match seq.size_hint() { + Some(capacity) => Vec::with_capacity(capacity), + None => Vec::new(), + }; + while let Some(instruction) = seq.next_element()? { + history.push(StateSnapshot { + state: state.clone(), + instruction: instruction.clone(), + }); + state.process_instruction(stats, config, instruction); } - Ok(stack) + Ok(SessionState { state, history }) } } - deserializer.deserialize_seq(SessionStateVisitor(core::marker::PhantomData)) + let state = G::deserialize(deserializer)?; + deserializer.deserialize_seq(SessionStateVisitor { state }) } } #[cfg(feature = "serde")] -impl serde::Serialize for SessionState { +impl serde::Serialize for SessionState +where + G: serde::Serialize, + G::Instruction: serde::Serialize, +{ fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { + if let Some(state) = self.history.first() { + state.serialize(serializer)?; + }else{ + self.state.serialize(serializer)?; + } use serde::ser::SerializeSeq; - let mut seq = serializer.serialize_seq(Some(self.len()))?; - for card in self.as_slice() { - seq.serialize_element(card)?; + let mut seq = serializer.serialize_seq(Some(self.history.len()))?; + for snapshot in &self.history { + seq.serialize_element(&snapshot.instruction)?; } seq.end() } -- 2.47.3 From b6e9d9fbdd24095c72644273e574f576d84b3ae8 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Sat, 30 May 2026 10:44:33 -0700 Subject: [PATCH 05/25] wag --- card_game/src/lib.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 0c2db52..1f1e44f 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -663,19 +663,24 @@ where } #[cfg(feature = "serde")] -impl<'de, G: Game> serde::Deserialize<'de> for SessionState +impl<'de, G: Game> serde::de::DeserializeSeed<'de> for Session where G: serde::Deserialize<'de>, G::Instruction: serde::Deserialize<'de>, { - fn deserialize(deserializer: D) -> Result + type Value = SessionState; + fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct SessionStateVisitor { state: G, } - impl<'de, G: Game> serde::de::Visitor<'de> for SessionStateVisitor { + impl<'de, G: Game> serde::de::Visitor<'de> for SessionStateVisitor + where + G: serde::Deserialize<'de>, + G::Instruction: serde::Deserialize<'de>, + { type Value = SessionState; fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "State History") @@ -689,7 +694,7 @@ where Some(capacity) => Vec::with_capacity(capacity), None => Vec::new(), }; - while let Some(instruction) = seq.next_element()? { + while let Some(instruction) = seq.next_element::()? { history.push(StateSnapshot { state: state.clone(), instruction: instruction.clone(), @@ -715,7 +720,7 @@ where { if let Some(state) = self.history.first() { state.serialize(serializer)?; - }else{ + } else { self.state.serialize(serializer)?; } use serde::ser::SerializeSeq; -- 2.47.3 From 0477ef5c52478cfd75ecde2babc76c9a0097c5e0 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 21:45:58 -0700 Subject: [PATCH 06/25] no derive for Session --- card_game/src/lib.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 1f1e44f..15bbe1a 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -469,15 +469,6 @@ impl Default for SessionConfig { } #[derive(Clone, Debug)] -#[cfg_attr( - feature = "serde", - derive(serde_derive::Deserialize, serde_derive::Serialize), - serde(bound = " - G: serde::Serialize + for <'a> serde::Deserialize<'a>, - G::Stats: serde::Serialize + for <'a> serde::Deserialize<'a>, - G::Config: serde::Serialize + for <'a> serde::Deserialize<'a>, - ") -)] pub struct Session { stats: SessionStats, config: SessionConfig, -- 2.47.3 From 2a844add363b0b496f111c3ec1c1fbf7ec51976d Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 21:45:28 -0700 Subject: [PATCH 07/25] deserialize seed work --- card_game/src/lib.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 15bbe1a..91751d8 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -656,27 +656,29 @@ where #[cfg(feature = "serde")] impl<'de, G: Game> serde::de::DeserializeSeed<'de> for Session where - G: serde::Deserialize<'de>, + G: serde::Deserialize<'de> + Clone, G::Instruction: serde::Deserialize<'de>, { type Value = SessionState; - fn deserialize(deserializer: D) -> Result + fn deserialize(self, deserializer: D) -> Result where D: serde::Deserializer<'de>, { - struct SessionStateVisitor { + struct SessionStateVisitor { state: G, + stats: SessionStats, + config: SessionConfig, } impl<'de, G: Game> serde::de::Visitor<'de> for SessionStateVisitor where - G: serde::Deserialize<'de>, + G: serde::Deserialize<'de> + Clone, G::Instruction: serde::Deserialize<'de>, { type Value = SessionState; fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "State History") } - fn visit_seq(self, mut seq: A) -> Result + fn visit_seq(mut self, mut seq: A) -> Result where A: serde::de::SeqAccess<'de>, { @@ -690,13 +692,21 @@ where state: state.clone(), instruction: instruction.clone(), }); - state.process_instruction(stats, config, instruction); + state.process_instruction( + &mut self.stats.inner, + &self.config.inner, + instruction, + ); } Ok(SessionState { state, history }) } } let state = G::deserialize(deserializer)?; - deserializer.deserialize_seq(SessionStateVisitor { state }) + deserializer.deserialize_seq(SessionStateVisitor { + state, + stats: self.stats, + config: self.config, + }) } } #[cfg(feature = "serde")] -- 2.47.3 From ea066706ee9f4d7f333aae6c6f5d0c2d91bf56cf Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 22:25:15 -0700 Subject: [PATCH 08/25] serialize impl --- card_game/src/lib.rs | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 91751d8..f011c16 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -719,16 +719,38 @@ where where S: serde::Serializer, { - if let Some(state) = self.history.first() { - state.serialize(serializer)?; + struct History<'a, G: Game>(&'a [StateSnapshot]); + impl serde::Serialize for History<'_, G> + where + G: serde::Serialize, + G::Instruction: serde::Serialize, + { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let &History(history) = self; + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(history.len()))?; + for snapshot in history { + seq.serialize_element(&snapshot.instruction)?; + } + seq.end() + } + } + + use serde::ser::SerializeMap; + let mut map = serializer.serialize_map(Some(2))?; + // serialize the initial state of the game. + // if there is history, it is the first snapshot's state, + // otherwise it is the current game state since there are no moves. + let state = if let Some(snapshot) = self.history.first() { + snapshot.state() } else { - self.state.serialize(serializer)?; - } - use serde::ser::SerializeSeq; - let mut seq = serializer.serialize_seq(Some(self.history.len()))?; - for snapshot in &self.history { - seq.serialize_element(&snapshot.instruction)?; - } - seq.end() + &self.state + }; + map.serialize_entry("state", state)?; + map.serialize_entry("history", &History(&self.history))?; + map.end() } } -- 2.47.3 From 9ab2eb0668ba0321a44dadb2bcaa3b18f197cfc2 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 22:41:45 -0700 Subject: [PATCH 09/25] serde --- card_game/src/lib.rs | 83 +++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 51 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index f011c16..7aea343 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -653,66 +653,46 @@ where } } +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize), + serde(bound = " + G: serde::Deserialize<'de>, + SessionConfig: serde::Deserialize<'de>, + Vec: serde::Deserialize<'de>, + ") +)] +struct SerializedSession { + config: SessionConfig, + state: G, + history: Vec, +} #[cfg(feature = "serde")] -impl<'de, G: Game> serde::de::DeserializeSeed<'de> for Session +impl<'de, G: Game> serde::de::Deserialize<'de> for Session where - G: serde::Deserialize<'de> + Clone, - G::Instruction: serde::Deserialize<'de>, + G: serde::Deserialize<'de> + Clone + Eq + core::hash::Hash, + G::Stats: Default, + G::Instruction: serde::Deserialize<'de> + Eq + core::hash::Hash, + SessionConfig: serde::Deserialize<'de>, + Vec: serde::Deserialize<'de>, { - type Value = SessionState; - fn deserialize(self, deserializer: D) -> Result + fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - struct SessionStateVisitor { - state: G, - stats: SessionStats, - config: SessionConfig, + let serialized = SerializedSession::deserialize(deserializer)?; + let mut session = Session::new(serialized.state, serialized.config); + for instruction in serialized.history { + session.process_instruction(instruction); } - impl<'de, G: Game> serde::de::Visitor<'de> for SessionStateVisitor - where - G: serde::Deserialize<'de> + Clone, - G::Instruction: serde::Deserialize<'de>, - { - type Value = SessionState; - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "State History") - } - fn visit_seq(mut self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let mut state = self.state.clone(); - let mut history = match seq.size_hint() { - Some(capacity) => Vec::with_capacity(capacity), - None => Vec::new(), - }; - while let Some(instruction) = seq.next_element::()? { - history.push(StateSnapshot { - state: state.clone(), - instruction: instruction.clone(), - }); - state.process_instruction( - &mut self.stats.inner, - &self.config.inner, - instruction, - ); - } - Ok(SessionState { state, history }) - } - } - let state = G::deserialize(deserializer)?; - deserializer.deserialize_seq(SessionStateVisitor { - state, - stats: self.stats, - config: self.config, - }) + Ok(session) } } #[cfg(feature = "serde")] -impl serde::Serialize for SessionState +impl serde::Serialize for Session where G: serde::Serialize, + G::Config: serde::Serialize, G::Instruction: serde::Serialize, { fn serialize(&self, serializer: S) -> Result @@ -744,13 +724,14 @@ where // serialize the initial state of the game. // if there is history, it is the first snapshot's state, // otherwise it is the current game state since there are no moves. - let state = if let Some(snapshot) = self.history.first() { + let state = if let Some(snapshot) = self.state.history.first() { snapshot.state() } else { - &self.state + &self.state.state }; + map.serialize_entry("config", &self.config)?; map.serialize_entry("state", state)?; - map.serialize_entry("history", &History(&self.history))?; + map.serialize_entry("history", &History(&self.state.history))?; map.end() } } -- 2.47.3 From c06451156ee2d9272d97a26570856802dfd4f813 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 23:00:15 -0700 Subject: [PATCH 10/25] klondike serde --- Cargo.lock | 2 ++ klondike/Cargo.toml | 6 ++++++ klondike/src/lib.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index f91f9e5..f30e0af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,6 +132,8 @@ version = "0.3.0" dependencies = [ "card_game", "rand", + "serde", + "serde_derive", ] [[package]] diff --git a/klondike/Cargo.toml b/klondike/Cargo.toml index 1c55cb4..557c118 100644 --- a/klondike/Cargo.toml +++ b/klondike/Cargo.toml @@ -6,6 +6,12 @@ edition = "2024" [dependencies] card_game.workspace = true rand = { version = "0.10.1", default-features = false, features = ["std_rng"] } +serde = { version = "1.0.228", default-features = false, optional = true } +serde_derive = { version = "1.0.228", default-features = false, optional = true } [lints] workspace = true + +[features] +default = ["serde"] +serde = ["dep:serde", "dep:serde_derive"] diff --git a/klondike/src/lib.rs b/klondike/src/lib.rs index 48b491d..922b52a 100644 --- a/klondike/src/lib.rs +++ b/klondike/src/lib.rs @@ -7,6 +7,10 @@ use card_game::{Card, Game, Pile, Rank, Stack}; #[cfg(doctest)] struct ReadmeDoctests; +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize) +)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum DrawStockConfig { #[default] @@ -14,6 +18,10 @@ pub enum DrawStockConfig { DrawThree = 3, } +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize) +)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum MoveFromFoundationConfig { #[default] @@ -21,6 +29,10 @@ pub enum MoveFromFoundationConfig { Disallowed, } +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize) +)] #[derive(Clone, Copy, Debug)] pub struct ScoringConfig { pub move_to_foundation: i32, @@ -44,6 +56,10 @@ impl Default for ScoringConfig { } } +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize) +)] #[derive(Clone, Debug, Default)] pub struct KlondikeConfig { pub draw_stock: DrawStockConfig, @@ -379,6 +395,10 @@ const fn sum(n: usize) -> usize { const STOCK: usize = 52 - sum(TABLEAUS); const NUM_RANKS: usize = Rank::RANKS.len(); +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize) +)] #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct KlondikeState { stock: Pile, @@ -613,6 +633,10 @@ fn test_klondike_iter() { assert_eq!(KlondikeIter::new().count(), 721); } +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize, serde_derive::Serialize) +)] #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct Klondike { state: KlondikeState, @@ -797,3 +821,28 @@ impl Game for Klondike { }) } } + +#[cfg(feature = "serde")] +impl serde::Serialize for KlondikeInstruction { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + // There is 721 klondike instructions + let instruction_id = KlondikeIter::new() + .position(|instruction| &instruction == self) + .unwrap(); + serializer.serialize_u16(instruction_id as u16) + } +} +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for KlondikeInstruction { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let instruction_id = u16::deserialize(deserializer)?; + let instruction = KlondikeIter::new().nth(instruction_id as usize).unwrap(); + Ok(instruction) + } +} -- 2.47.3 From ea7e12240431e338efbd49f47c62b92554eaaf9a Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 23:18:18 -0700 Subject: [PATCH 11/25] write bound as explicitly deserialize --- card_game/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 7aea343..e35647e 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -656,11 +656,11 @@ where #[cfg_attr( feature = "serde", derive(serde_derive::Deserialize), - serde(bound = " + serde(bound(deserialize = " G: serde::Deserialize<'de>, SessionConfig: serde::Deserialize<'de>, Vec: serde::Deserialize<'de>, - ") + ")) )] struct SerializedSession { config: SessionConfig, -- 2.47.3 From 206361b0858edd1025c1f5255595f211e7403f8b Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 23:13:59 -0700 Subject: [PATCH 12/25] smoke test --- Cargo.lock | 6 ++++-- klondike-cli/Cargo.toml | 4 ++++ klondike-cli/src/test.rs | 13 +++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f30e0af..2009491 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,6 +152,8 @@ dependencies = [ "card_game", "klondike", "rand", + "serde", + "serde_json", ] [[package]] @@ -266,9 +268,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", diff --git a/klondike-cli/Cargo.toml b/klondike-cli/Cargo.toml index a4769cd..5e1399f 100644 --- a/klondike-cli/Cargo.toml +++ b/klondike-cli/Cargo.toml @@ -8,5 +8,9 @@ card_game.workspace = true klondike.workspace = true rand = { version = "0.10.1", default-features = false, features = ["thread_rng"] } +[dev-dependencies] +serde = { version = "1.0.228", default-features = false } +serde_json = "1.0.150" + [lints] workspace = true diff --git a/klondike-cli/src/test.rs b/klondike-cli/src/test.rs index 28c6471..68ca0e0 100644 --- a/klondike-cli/src/test.rs +++ b/klondike-cli/src/test.rs @@ -14,3 +14,16 @@ fn test_is_winnable() { println!("Game is not winnable"); } } + +#[test] +fn test_serde() { + let mut session = Session::new_default(Klondike::with_seed(124)); + let solution_result = session.solve(); + if let Ok(Some(solution)) = solution_result { + for snapshot in solution.clean_solution() { + session.process_instruction(snapshot.instruction().clone()); + } + } + let serialized = serde_json::to_string(&session).unwrap(); + println!("serialized = {serialized}"); +} -- 2.47.3 From 6b68b12ea99215658c2dc925b607600c1080a2bd Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 23:21:59 -0700 Subject: [PATCH 13/25] round trip --- klondike-cli/src/test.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/klondike-cli/src/test.rs b/klondike-cli/src/test.rs index 68ca0e0..a99b634 100644 --- a/klondike-cli/src/test.rs +++ b/klondike-cli/src/test.rs @@ -26,4 +26,7 @@ fn test_serde() { } let serialized = serde_json::to_string(&session).unwrap(); println!("serialized = {serialized}"); + let round_trip_session: Session = serde_json::from_str(&serialized).unwrap(); + let serialized2 = serde_json::to_string(&round_trip_session).unwrap(); + assert_eq!(serialized, serialized2); } -- 2.47.3 From db558dfec3a8ed741ab4096dc353ee03fdd1334d Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 23:24:39 -0700 Subject: [PATCH 14/25] no --- Cargo.lock | 4 ++-- klondike-cli/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2009491..7b5f6a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,9 +268,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", diff --git a/klondike-cli/Cargo.toml b/klondike-cli/Cargo.toml index 5e1399f..7736d8a 100644 --- a/klondike-cli/Cargo.toml +++ b/klondike-cli/Cargo.toml @@ -10,7 +10,7 @@ rand = { version = "0.10.1", default-features = false, features = ["thread_rng"] [dev-dependencies] serde = { version = "1.0.228", default-features = false } -serde_json = "1.0.150" +serde_json = "1.0.149" [lints] workspace = true -- 2.47.3 From e3afbc5cad7608082a2498cae0abd73a110b39fd Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 23:27:27 -0700 Subject: [PATCH 15/25] transparent --- klondike/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/klondike/src/lib.rs b/klondike/src/lib.rs index 922b52a..4e1dd50 100644 --- a/klondike/src/lib.rs +++ b/klondike/src/lib.rs @@ -635,7 +635,8 @@ fn test_klondike_iter() { #[cfg_attr( feature = "serde", - derive(serde_derive::Deserialize, serde_derive::Serialize) + derive(serde_derive::Deserialize, serde_derive::Serialize), + serde(transparent) )] #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct Klondike { -- 2.47.3 From 3020c21bca603f36384bf7cb7f1af4df70d0cd6b Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 23:34:34 -0700 Subject: [PATCH 16/25] change serialize_map to serialize_struct --- card_game/src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index e35647e..a5e55c9 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -719,8 +719,8 @@ where } } - use serde::ser::SerializeMap; - let mut map = serializer.serialize_map(Some(2))?; + use serde::ser::SerializeStruct; + let mut map = serializer.serialize_struct("Session", 3)?; // serialize the initial state of the game. // if there is history, it is the first snapshot's state, // otherwise it is the current game state since there are no moves. @@ -729,9 +729,9 @@ where } else { &self.state.state }; - map.serialize_entry("config", &self.config)?; - map.serialize_entry("state", state)?; - map.serialize_entry("history", &History(&self.state.history))?; + map.serialize_field("config", &self.config)?; + map.serialize_field("state", state)?; + map.serialize_field("history", &History(&self.state.history))?; map.end() } } -- 2.47.3 From 8fc6854118b68a81d170344dc5720207c70c08d2 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 23:34:47 -0700 Subject: [PATCH 17/25] change test name --- klondike-cli/src/test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klondike-cli/src/test.rs b/klondike-cli/src/test.rs index a99b634..196c31d 100644 --- a/klondike-cli/src/test.rs +++ b/klondike-cli/src/test.rs @@ -16,7 +16,7 @@ fn test_is_winnable() { } #[test] -fn test_serde() { +fn test_json() { let mut session = Session::new_default(Klondike::with_seed(124)); let solution_result = session.solve(); if let Ok(Some(solution)) = solution_result { -- 2.47.3 From dd2e4ac3ae46bbb597d8a9e11e89b91f58d593a5 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Mon, 8 Jun 2026 23:34:56 -0700 Subject: [PATCH 18/25] test rmp_serde --- Cargo.lock | 35 +++++++++++++++++++++++++++++++++++ klondike-cli/Cargo.toml | 1 + klondike-cli/src/test.rs | 16 ++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 7b5f6a4..467c99c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,12 @@ version = "0.7.6" source = "sparse+https://git.aleshym.co/api/packages/Quaternions/cargo/" checksum = "813440870d646c57c222c1d713dc4e3ddcb2919c3801564d767d85d7bf2afee4" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "bitflags" version = "2.11.1" @@ -152,6 +158,7 @@ dependencies = [ "card_game", "klondike", "rand", + "rmp-serde", "serde", "serde_json", ] @@ -180,6 +187,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -231,6 +247,25 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + [[package]] name = "semver" version = "1.0.28" diff --git a/klondike-cli/Cargo.toml b/klondike-cli/Cargo.toml index 7736d8a..032e985 100644 --- a/klondike-cli/Cargo.toml +++ b/klondike-cli/Cargo.toml @@ -9,6 +9,7 @@ klondike.workspace = true rand = { version = "0.10.1", default-features = false, features = ["thread_rng"] } [dev-dependencies] +rmp-serde = "1.3.1" serde = { version = "1.0.228", default-features = false } serde_json = "1.0.149" diff --git a/klondike-cli/src/test.rs b/klondike-cli/src/test.rs index 196c31d..25b6617 100644 --- a/klondike-cli/src/test.rs +++ b/klondike-cli/src/test.rs @@ -30,3 +30,19 @@ fn test_json() { let serialized2 = serde_json::to_string(&round_trip_session).unwrap(); assert_eq!(serialized, serialized2); } + +#[test] +fn test_rmp() { + let mut session = Session::new_default(Klondike::with_seed(124)); + let solution_result = session.solve(); + if let Ok(Some(solution)) = solution_result { + for snapshot in solution.clean_solution() { + session.process_instruction(snapshot.instruction().clone()); + } + } + let serialized = rmp_serde::to_vec(&session).unwrap(); + println!("serialized.len() = {}", serialized.len()); + let round_trip_session: Session = rmp_serde::from_slice(&serialized).unwrap(); + let serialized2 = rmp_serde::to_vec(&round_trip_session).unwrap(); + assert_eq!(serialized, serialized2); +} -- 2.47.3 From f13d08a0a875477e041ed393cce3fb34761f9649 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 9 Jun 2026 09:46:44 -0700 Subject: [PATCH 19/25] move struct into deserialize impl --- card_game/src/lib.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index a5e55c9..94cf754 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -653,20 +653,6 @@ where } } -#[cfg_attr( - feature = "serde", - derive(serde_derive::Deserialize), - serde(bound(deserialize = " - G: serde::Deserialize<'de>, - SessionConfig: serde::Deserialize<'de>, - Vec: serde::Deserialize<'de>, - ")) -)] -struct SerializedSession { - config: SessionConfig, - state: G, - history: Vec, -} #[cfg(feature = "serde")] impl<'de, G: Game> serde::de::Deserialize<'de> for Session where @@ -680,6 +666,17 @@ where where D: serde::Deserializer<'de>, { + #[derive(serde_derive::Deserialize)] + #[serde(bound(deserialize = " + G: serde::Deserialize<'de>, + SessionConfig: serde::Deserialize<'de>, + Vec: serde::Deserialize<'de>, + "))] + struct SerializedSession { + config: SessionConfig, + state: G, + history: Vec, + } let serialized = SerializedSession::deserialize(deserializer)?; let mut session = Session::new(serialized.state, serialized.config); for instruction in serialized.history { -- 2.47.3 From 9cc223c16046bd01a5a5928bef0b96a1cc576fda Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 9 Jun 2026 09:46:54 -0700 Subject: [PATCH 20/25] use struct in serialize impl --- card_game/src/lib.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 94cf754..862bd65 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -716,8 +716,18 @@ where } } - use serde::ser::SerializeStruct; - let mut map = serializer.serialize_struct("Session", 3)?; + #[derive(serde_derive::Serialize)] + #[serde(bound(serialize = " + G: serde::Serialize, + SessionConfig: serde::Serialize, + G::Instruction: serde::Serialize, + "))] + struct SerializedSession<'a, G: Game> { + config: &'a SessionConfig, + state: &'a G, + history: History<'a, G>, + } + // serialize the initial state of the game. // if there is history, it is the first snapshot's state, // otherwise it is the current game state since there are no moves. @@ -726,9 +736,11 @@ where } else { &self.state.state }; - map.serialize_field("config", &self.config)?; - map.serialize_field("state", state)?; - map.serialize_field("history", &History(&self.state.history))?; - map.end() + let session = SerializedSession { + config: &self.config, + state, + history: History(&self.state.history), + }; + session.serialize(serializer) } } -- 2.47.3 From 96e77df387e95f1a3be2dd636e44b1f2e5a4d829 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 9 Jun 2026 09:49:32 -0700 Subject: [PATCH 21/25] move tests to klondike --- Cargo.lock | 5 ++--- klondike-cli/Cargo.toml | 5 ----- klondike-cli/src/main.rs | 3 --- klondike/Cargo.toml | 5 +++++ klondike/src/lib.rs | 3 +++ {klondike-cli => klondike}/src/test.rs | 4 +++- 6 files changed, 13 insertions(+), 12 deletions(-) rename {klondike-cli => klondike}/src/test.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index 467c99c..f9e3990 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,8 +138,10 @@ version = "0.3.0" dependencies = [ "card_game", "rand", + "rmp-serde", "serde", "serde_derive", + "serde_json", ] [[package]] @@ -158,9 +160,6 @@ dependencies = [ "card_game", "klondike", "rand", - "rmp-serde", - "serde", - "serde_json", ] [[package]] diff --git a/klondike-cli/Cargo.toml b/klondike-cli/Cargo.toml index 032e985..a4769cd 100644 --- a/klondike-cli/Cargo.toml +++ b/klondike-cli/Cargo.toml @@ -8,10 +8,5 @@ card_game.workspace = true klondike.workspace = true rand = { version = "0.10.1", default-features = false, features = ["thread_rng"] } -[dev-dependencies] -rmp-serde = "1.3.1" -serde = { version = "1.0.228", default-features = false } -serde_json = "1.0.149" - [lints] workspace = true diff --git a/klondike-cli/src/main.rs b/klondike-cli/src/main.rs index 22ba410..60c3740 100644 --- a/klondike-cli/src/main.rs +++ b/klondike-cli/src/main.rs @@ -4,9 +4,6 @@ use klondike::{ KlondikePile, KlondikePileStack, SkipCards, Tableau, TableauStack, }; -#[cfg(test)] -mod test; - use std::fmt::Display; struct Displayed(T); diff --git a/klondike/Cargo.toml b/klondike/Cargo.toml index 557c118..56c2787 100644 --- a/klondike/Cargo.toml +++ b/klondike/Cargo.toml @@ -9,6 +9,11 @@ rand = { version = "0.10.1", default-features = false, features = ["std_rng"] } serde = { version = "1.0.228", default-features = false, optional = true } serde_derive = { version = "1.0.228", default-features = false, optional = true } +[dev-dependencies] +rmp-serde = "1.3.1" +serde = { version = "1.0.228", default-features = false } +serde_json = "1.0.149" + [lints] workspace = true diff --git a/klondike/src/lib.rs b/klondike/src/lib.rs index 4e1dd50..425201f 100644 --- a/klondike/src/lib.rs +++ b/klondike/src/lib.rs @@ -2,6 +2,9 @@ pub type Rng = rand::rngs::StdRng; use card_game::{Card, Game, Pile, Rank, Stack}; +#[cfg(test)] +mod test; + // test readme #[doc = include_str!("../README.md")] #[cfg(doctest)] diff --git a/klondike-cli/src/test.rs b/klondike/src/test.rs similarity index 98% rename from klondike-cli/src/test.rs rename to klondike/src/test.rs index 25b6617..d7931e1 100644 --- a/klondike-cli/src/test.rs +++ b/klondike/src/test.rs @@ -1,5 +1,6 @@ use card_game::Session; -use klondike::Klondike; +use crate::Klondike; + #[test] fn test_is_winnable() { // is winnable @@ -15,6 +16,7 @@ fn test_is_winnable() { } } + #[test] fn test_json() { let mut session = Session::new_default(Klondike::with_seed(124)); -- 2.47.3 From f58e56ade9c107910d7a7926354a8ea33a3f1de4 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 9 Jun 2026 09:53:44 -0700 Subject: [PATCH 22/25] fixup serde feature --- klondike/Cargo.toml | 2 +- klondike/src/test.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/klondike/Cargo.toml b/klondike/Cargo.toml index 56c2787..ff4927d 100644 --- a/klondike/Cargo.toml +++ b/klondike/Cargo.toml @@ -19,4 +19,4 @@ workspace = true [features] default = ["serde"] -serde = ["dep:serde", "dep:serde_derive"] +serde = ["dep:serde", "dep:serde_derive", "card_game/serde"] diff --git a/klondike/src/test.rs b/klondike/src/test.rs index d7931e1..9550cf2 100644 --- a/klondike/src/test.rs +++ b/klondike/src/test.rs @@ -17,6 +17,7 @@ fn test_is_winnable() { } +#[cfg(feature = "serde")] #[test] fn test_json() { let mut session = Session::new_default(Klondike::with_seed(124)); @@ -33,6 +34,7 @@ fn test_json() { assert_eq!(serialized, serialized2); } +#[cfg(feature = "serde")] #[test] fn test_rmp() { let mut session = Session::new_default(Klondike::with_seed(124)); -- 2.47.3 From 12e57cb33d802b8a1a35f663ab983326658519b2 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 9 Jun 2026 09:57:27 -0700 Subject: [PATCH 23/25] snapshot tests --- Cargo.lock | 105 ++++++++++++++++++ klondike/Cargo.toml | 1 + .../src/snapshots/klondike__test__json.snap | 5 + .../snapshots/klondike__test__save_rmp.snap | 6 + .../klondike__test__save_rmp.snap.bin | Bin 0 -> 459 bytes klondike/src/test.rs | 7 +- 6 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 klondike/src/snapshots/klondike__test__json.snap create mode 100644 klondike/src/snapshots/klondike__test__save_rmp.snap create mode 100644 klondike/src/snapshots/klondike__test__save_rmp.snap.bin diff --git a/Cargo.lock b/Cargo.lock index f9e3990..ffa87bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,17 @@ dependencies = [ "rand_core", ] +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -61,12 +72,34 @@ dependencies = [ "libc", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "foldhash" version = "0.1.5" @@ -126,6 +159,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "insta" +version = "1.47.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +dependencies = [ + "console", + "once_cell", + "similar", + "tempfile", +] + [[package]] name = "itoa" version = "1.0.18" @@ -137,6 +182,7 @@ name = "klondike" version = "0.3.0" dependencies = [ "card_game", + "insta", "rand", "rmp-serde", "serde", @@ -174,6 +220,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "log" version = "0.4.29" @@ -195,6 +247,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "prettyplease" version = "0.2.37" @@ -265,6 +323,19 @@ dependencies = [ "serde", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "semver" version = "1.0.28" @@ -313,6 +384,12 @@ dependencies = [ "zmij", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "syn" version = "2.0.117" @@ -324,6 +401,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -388,6 +478,21 @@ dependencies = [ "semver", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/klondike/Cargo.toml b/klondike/Cargo.toml index ff4927d..bdaec6b 100644 --- a/klondike/Cargo.toml +++ b/klondike/Cargo.toml @@ -10,6 +10,7 @@ serde = { version = "1.0.228", default-features = false, optional = true } serde_derive = { version = "1.0.228", default-features = false, optional = true } [dev-dependencies] +insta = "1.47.2" rmp-serde = "1.3.1" serde = { version = "1.0.228", default-features = false } serde_json = "1.0.149" diff --git a/klondike/src/snapshots/klondike__test__json.snap b/klondike/src/snapshots/klondike__test__json.snap new file mode 100644 index 0000000..5014cb4 --- /dev/null +++ b/klondike/src/snapshots/klondike__test__json.snap @@ -0,0 +1,5 @@ +--- +source: klondike/src/test.rs +expression: serialized +--- +{"config":{"inner":{"draw_stock":"DrawOne","move_from_foundation":"Allowed","scoring":{"move_to_foundation":10,"flip_up_bonus":5,"move_to_tableau":5,"move_from_foundation":-15,"recycle":0}},"undo_penalty":-15,"solve_moves_budget":100000,"solve_states_budget":100000},"state":{"stock":{"face_down":[52,51,21,59,36,45,49,50,22,3,58,39,41,61,13,57,34,56,28,1,19,12,17,5],"face_up":[]},"foundations":[[],[],[],[]],"tableau1":{"face_down":[],"face_up":[40]},"tableau2":{"face_down":[9],"face_up":[4]},"tableau3":{"face_down":[35,8],"face_up":[60]},"tableau4":{"face_down":[11,27,44],"face_up":[25]},"tableau5":{"face_down":[6,7,23,18],"face_up":[29]},"tableau6":{"face_down":[2,55,10,42,24],"face_up":[54]},"tableau7":{"face_down":[26,43,38,53,37,33],"face_up":[20]}},"history":[336,100,720,619,606,6,720,19,16,388,497,580,388,720,139,720,19,720,17,31,720,720,720,7,720,720,523,458,720,720,720,720,720,720,720,720,43,43,720,720,720,139,720,19,720,43,720,43,720,720,720,720,720,720,720,720,720,623,619,720,720,720,720,720,720,720,720,720,720,693,281,333,253,209,113,87,720,720,720,720,720,720,720,720,75,223,619,533,74,2,720,116,331,279,279,305,305,29,25,24,12,37,25,222,24,42,126,471,720,720,720,362,292,331,253,720,19,720,183,151,12,67,336,61,93,235,720,7,2,0,7,38,26,14,27,15,26,36,0,7,3,6,18,43,36,24,12,0,1,19,38,26,39,27,15,38,26,36]} diff --git a/klondike/src/snapshots/klondike__test__save_rmp.snap b/klondike/src/snapshots/klondike__test__save_rmp.snap new file mode 100644 index 0000000..a7ace5b --- /dev/null +++ b/klondike/src/snapshots/klondike__test__save_rmp.snap @@ -0,0 +1,6 @@ +--- +source: klondike/src/test.rs +expression: serialized +extension: bin +snapshot_kind: binary +--- diff --git a/klondike/src/snapshots/klondike__test__save_rmp.snap.bin b/klondike/src/snapshots/klondike__test__save_rmp.snap.bin new file mode 100644 index 0000000000000000000000000000000000000000..0de4f97393e687c2c7b8c2bd48b3da0aefd99b07 GIT binary patch literal 459 zcmbO{W%6>DqQr9lywv56IXU^|sVP&rSXn3c6WH3V>2u&oWA$ zW%NDs_sqq@a8xOTM(;EC!A6OCF#(m7oMrSr%P0;;hG!WKWhEteR3*>clhCR=%Xl4O z24mJ)MirpiGk<|%AYY%^er7t4GccHJW6!(>^4Xaf*wv)?r1_;(7}%NFgtS#8co-Ok M)uhy=`PHOU0FGL`p8x;= literal 0 HcmV?d00001 diff --git a/klondike/src/test.rs b/klondike/src/test.rs index 9550cf2..ed8168f 100644 --- a/klondike/src/test.rs +++ b/klondike/src/test.rs @@ -1,5 +1,5 @@ -use card_game::Session; use crate::Klondike; +use card_game::Session; #[test] fn test_is_winnable() { @@ -16,7 +16,6 @@ fn test_is_winnable() { } } - #[cfg(feature = "serde")] #[test] fn test_json() { @@ -32,6 +31,8 @@ fn test_json() { let round_trip_session: Session = serde_json::from_str(&serialized).unwrap(); let serialized2 = serde_json::to_string(&round_trip_session).unwrap(); assert_eq!(serialized, serialized2); + + insta::assert_snapshot!(serialized); } #[cfg(feature = "serde")] @@ -49,4 +50,6 @@ fn test_rmp() { let round_trip_session: Session = rmp_serde::from_slice(&serialized).unwrap(); let serialized2 = rmp_serde::to_vec(&round_trip_session).unwrap(); assert_eq!(serialized, serialized2); + + insta::assert_binary_snapshot!("save_rmp.bin", serialized); } -- 2.47.3 From 51892029988693a9eb6d2aefe72b670fba43f26a Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 9 Jun 2026 10:05:29 -0700 Subject: [PATCH 24/25] stack error --- card_game/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index 862bd65..ae8fc92 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -234,6 +234,7 @@ impl IntoIterator for Stack { self.0.into_iter() } } + #[cfg(feature = "serde")] impl<'de, const CAP: usize> serde::Deserialize<'de> for Stack { fn deserialize(deserializer: D) -> Result @@ -250,10 +251,10 @@ impl<'de, const CAP: usize> serde::Deserialize<'de> for Stack { where A: serde::de::SeqAccess<'de>, { + use serde::de::Error; let mut stack = Stack::new(); while let Some(card) = seq.next_element()? { - // TODO: Error - stack.try_push(card).unwrap(); + stack.try_push(card).map_err(A::Error::custom)?; } Ok(stack) } -- 2.47.3 From 76284e4fd323cc0ee28900ccadd02b6aa17644d9 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Tue, 9 Jun 2026 10:11:29 -0700 Subject: [PATCH 25/25] change serde field names to make more sense --- card_game/src/lib.rs | 18 +++++++++--------- .../src/snapshots/klondike__test__json.snap | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/card_game/src/lib.rs b/card_game/src/lib.rs index ae8fc92..9da45c4 100644 --- a/card_game/src/lib.rs +++ b/card_game/src/lib.rs @@ -675,12 +675,12 @@ where "))] struct SerializedSession { config: SessionConfig, - state: G, - history: Vec, + initial_state: G, + instructions: Vec, } let serialized = SerializedSession::deserialize(deserializer)?; - let mut session = Session::new(serialized.state, serialized.config); - for instruction in serialized.history { + let mut session = Session::new(serialized.initial_state, serialized.config); + for instruction in serialized.instructions { session.process_instruction(instruction); } Ok(session) @@ -725,22 +725,22 @@ where "))] struct SerializedSession<'a, G: Game> { config: &'a SessionConfig, - state: &'a G, - history: History<'a, G>, + initial_state: &'a G, + instructions: History<'a, G>, } // serialize the initial state of the game. // if there is history, it is the first snapshot's state, // otherwise it is the current game state since there are no moves. - let state = if let Some(snapshot) = self.state.history.first() { + let initial_state = if let Some(snapshot) = self.state.history.first() { snapshot.state() } else { &self.state.state }; let session = SerializedSession { config: &self.config, - state, - history: History(&self.state.history), + initial_state, + instructions: History(&self.state.history), }; session.serialize(serializer) } diff --git a/klondike/src/snapshots/klondike__test__json.snap b/klondike/src/snapshots/klondike__test__json.snap index 5014cb4..949eccd 100644 --- a/klondike/src/snapshots/klondike__test__json.snap +++ b/klondike/src/snapshots/klondike__test__json.snap @@ -2,4 +2,4 @@ source: klondike/src/test.rs expression: serialized --- -{"config":{"inner":{"draw_stock":"DrawOne","move_from_foundation":"Allowed","scoring":{"move_to_foundation":10,"flip_up_bonus":5,"move_to_tableau":5,"move_from_foundation":-15,"recycle":0}},"undo_penalty":-15,"solve_moves_budget":100000,"solve_states_budget":100000},"state":{"stock":{"face_down":[52,51,21,59,36,45,49,50,22,3,58,39,41,61,13,57,34,56,28,1,19,12,17,5],"face_up":[]},"foundations":[[],[],[],[]],"tableau1":{"face_down":[],"face_up":[40]},"tableau2":{"face_down":[9],"face_up":[4]},"tableau3":{"face_down":[35,8],"face_up":[60]},"tableau4":{"face_down":[11,27,44],"face_up":[25]},"tableau5":{"face_down":[6,7,23,18],"face_up":[29]},"tableau6":{"face_down":[2,55,10,42,24],"face_up":[54]},"tableau7":{"face_down":[26,43,38,53,37,33],"face_up":[20]}},"history":[336,100,720,619,606,6,720,19,16,388,497,580,388,720,139,720,19,720,17,31,720,720,720,7,720,720,523,458,720,720,720,720,720,720,720,720,43,43,720,720,720,139,720,19,720,43,720,43,720,720,720,720,720,720,720,720,720,623,619,720,720,720,720,720,720,720,720,720,720,693,281,333,253,209,113,87,720,720,720,720,720,720,720,720,75,223,619,533,74,2,720,116,331,279,279,305,305,29,25,24,12,37,25,222,24,42,126,471,720,720,720,362,292,331,253,720,19,720,183,151,12,67,336,61,93,235,720,7,2,0,7,38,26,14,27,15,26,36,0,7,3,6,18,43,36,24,12,0,1,19,38,26,39,27,15,38,26,36]} +{"config":{"inner":{"draw_stock":"DrawOne","move_from_foundation":"Allowed","scoring":{"move_to_foundation":10,"flip_up_bonus":5,"move_to_tableau":5,"move_from_foundation":-15,"recycle":0}},"undo_penalty":-15,"solve_moves_budget":100000,"solve_states_budget":100000},"initial_state":{"stock":{"face_down":[52,51,21,59,36,45,49,50,22,3,58,39,41,61,13,57,34,56,28,1,19,12,17,5],"face_up":[]},"foundations":[[],[],[],[]],"tableau1":{"face_down":[],"face_up":[40]},"tableau2":{"face_down":[9],"face_up":[4]},"tableau3":{"face_down":[35,8],"face_up":[60]},"tableau4":{"face_down":[11,27,44],"face_up":[25]},"tableau5":{"face_down":[6,7,23,18],"face_up":[29]},"tableau6":{"face_down":[2,55,10,42,24],"face_up":[54]},"tableau7":{"face_down":[26,43,38,53,37,33],"face_up":[20]}},"instructions":[336,100,720,619,606,6,720,19,16,388,497,580,388,720,139,720,19,720,17,31,720,720,720,7,720,720,523,458,720,720,720,720,720,720,720,720,43,43,720,720,720,139,720,19,720,43,720,43,720,720,720,720,720,720,720,720,720,623,619,720,720,720,720,720,720,720,720,720,720,693,281,333,253,209,113,87,720,720,720,720,720,720,720,720,75,223,619,533,74,2,720,116,331,279,279,305,305,29,25,24,12,37,25,222,24,42,126,471,720,720,720,362,292,331,253,720,19,720,183,151,12,67,336,61,93,235,720,7,2,0,7,38,26,14,27,15,26,36,0,7,3,6,18,43,36,24,12,0,1,19,38,26,39,27,15,38,26,36]} -- 2.47.3