Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd2e4ac3ae | |||
| 8fc6854118 | |||
| 3020c21bca | |||
| e3afbc5cad | |||
| db558dfec3 | |||
| 6b68b12ea9 | |||
| 206361b085 | |||
| ea7e122404 | |||
| c06451156e | |||
| 9ab2eb0668 | |||
| ea066706ee | |||
| 2a844add36 | |||
| 0477ef5c52 | |||
| b6e9d9fbdd | |||
| fadefe93c2 | |||
| cc4824732a | |||
| e256e60e91 | |||
| 60728060a3 |
Generated
+5
-109
@@ -28,7 +28,7 @@ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "card_game"
|
name = "card_game"
|
||||||
version = "0.4.1"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -52,17 +52,6 @@ dependencies = [
|
|||||||
"rand_core",
|
"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]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -72,34 +61,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "encode_unicode"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
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]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -159,18 +126,6 @@ dependencies = [
|
|||||||
"serde_core",
|
"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]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -179,15 +134,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "klondike"
|
name = "klondike"
|
||||||
version = "0.4.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"card_game",
|
"card_game",
|
||||||
"insta",
|
|
||||||
"rand",
|
"rand",
|
||||||
"rmp-serde",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -206,6 +158,9 @@ dependencies = [
|
|||||||
"card_game",
|
"card_game",
|
||||||
"klondike",
|
"klondike",
|
||||||
"rand",
|
"rand",
|
||||||
|
"rmp-serde",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -220,12 +175,6 @@ version = "0.2.186"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "linux-raw-sys"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.29"
|
||||||
@@ -247,12 +196,6 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell"
|
|
||||||
version = "1.21.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.37"
|
version = "0.2.37"
|
||||||
@@ -323,19 +266,6 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.28"
|
version = "1.0.28"
|
||||||
@@ -384,12 +314,6 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "similar"
|
|
||||||
version = "2.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
@@ -401,19 +325,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@@ -478,21 +389,6 @@ dependencies = [
|
|||||||
"semver",
|
"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]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.51.0"
|
version = "0.51.0"
|
||||||
|
|||||||
+2
-2
@@ -8,8 +8,8 @@ members = [
|
|||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
card_game = { version = "0.4.1", path = "card_game", registry = "Quaternions" }
|
card_game = { version = "0.4.0", path = "card_game", registry = "Quaternions" }
|
||||||
klondike = { version = "0.4.0", path = "klondike", registry = "Quaternions" }
|
klondike = { version = "0.3.0", path = "klondike", registry = "Quaternions" }
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
# unsafe_code = "forbid"
|
# unsafe_code = "forbid"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "card_game"
|
name = "card_game"
|
||||||
version = "0.4.1"
|
version = "0.4.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
repository = "https://git.aleshym.co/Quaternions/card_game"
|
repository = "https://git.aleshym.co/Quaternions/card_game"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|||||||
+52
-108
@@ -5,6 +5,7 @@ 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;
|
||||||
@@ -15,51 +16,14 @@ 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(valid_instruction: StagedInstruction<'_, Self>);
|
fn process_instruction(
|
||||||
fn is_win(&self) -> bool;
|
&mut self,
|
||||||
/// Validate an instruction to prepare to run it.
|
stats: &mut Self::Stats,
|
||||||
fn stage_instruction<'a>(
|
config: &Self::Config,
|
||||||
&'a mut self,
|
|
||||||
stats: &'a mut Self::Stats,
|
|
||||||
config: &'a Self::Config,
|
|
||||||
instruction: Self::Instruction,
|
instruction: Self::Instruction,
|
||||||
) -> Option<StagedInstruction<'a, Self>> {
|
);
|
||||||
self.is_instruction_valid(config, &instruction)
|
fn is_win(&self) -> bool;
|
||||||
.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.
|
||||||
@@ -270,7 +234,6 @@ impl<const CAP: usize> IntoIterator for Stack<CAP> {
|
|||||||
self.0.into_iter()
|
self.0.into_iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
impl<'de, const CAP: usize> serde::Deserialize<'de> for Stack<CAP> {
|
impl<'de, const CAP: usize> serde::Deserialize<'de> for Stack<CAP> {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
@@ -287,10 +250,10 @@ impl<'de, const CAP: usize> serde::Deserialize<'de> for Stack<CAP> {
|
|||||||
where
|
where
|
||||||
A: serde::de::SeqAccess<'de>,
|
A: serde::de::SeqAccess<'de>,
|
||||||
{
|
{
|
||||||
use serde::de::Error;
|
|
||||||
let mut stack = Stack::new();
|
let mut stack = Stack::new();
|
||||||
while let Some(card) = seq.next_element()? {
|
while let Some(card) = seq.next_element()? {
|
||||||
stack.try_push(card).map_err(A::Error::custom)?;
|
// TODO: Error
|
||||||
|
stack.try_push(card).unwrap();
|
||||||
}
|
}
|
||||||
Ok(stack)
|
Ok(stack)
|
||||||
}
|
}
|
||||||
@@ -578,21 +541,14 @@ 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 stage_instruction(
|
pub fn process_instruction(&mut self, instruction: G::Instruction) {
|
||||||
&mut self,
|
self.state.process_instruction(
|
||||||
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),
|
||||||
@@ -626,8 +582,7 @@ where
|
|||||||
|
|
||||||
// Run one possible move
|
// Run one possible move
|
||||||
if let Some(instruction) = it.next() {
|
if let Some(instruction) = it.next() {
|
||||||
// TODO: no unwrap
|
session.process_instruction(instruction);
|
||||||
session.stage_instruction(instruction).unwrap().commit();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,7 +617,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) => {
|
||||||
@@ -670,27 +625,26 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn process_instruction(valid_instruction: StagedInstruction<'_, Self>) {
|
fn process_instruction(
|
||||||
let (game, stats, config, instruction) = valid_instruction.consume();
|
&mut self,
|
||||||
|
stats: &mut Self::Stats,
|
||||||
|
config: &Self::Config,
|
||||||
|
instruction: Self::Instruction,
|
||||||
|
) {
|
||||||
match instruction {
|
match instruction {
|
||||||
SessionInstruction::Undo => {
|
SessionInstruction::Undo => {
|
||||||
if let Some(snapshot) = game.history.pop() {
|
if let Some(snapshot) = self.history.pop() {
|
||||||
game.state = snapshot.state;
|
self.state = snapshot.state;
|
||||||
stats.increment_undos();
|
stats.increment_undos();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SessionInstruction::InnerInstruction(instruction) => {
|
SessionInstruction::InnerInstruction(instruction) => {
|
||||||
game.history.push(StateSnapshot {
|
self.history.push(StateSnapshot {
|
||||||
state: game.state.clone(),
|
state: self.state.clone(),
|
||||||
instruction: instruction.clone(),
|
instruction: instruction.clone(),
|
||||||
});
|
});
|
||||||
// This might be considered cheaing a bit, we're pre-approving the instruction here by accessing the inner fields.
|
self.state
|
||||||
G::process_instruction(StagedInstruction {
|
.process_instruction(&mut stats.inner, &config.inner, instruction);
|
||||||
game: &mut game.state,
|
|
||||||
stats: &mut stats.inner,
|
|
||||||
config: &config.inner,
|
|
||||||
instruction,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -699,6 +653,20 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serde",
|
||||||
|
derive(serde_derive::Deserialize),
|
||||||
|
serde(bound(deserialize = "
|
||||||
|
G: serde::Deserialize<'de>,
|
||||||
|
SessionConfig<G::Config>: serde::Deserialize<'de>,
|
||||||
|
Vec<G::Instruction>: serde::Deserialize<'de>,
|
||||||
|
"))
|
||||||
|
)]
|
||||||
|
struct SerializedSession<G: Game> {
|
||||||
|
config: SessionConfig<G::Config>,
|
||||||
|
state: G,
|
||||||
|
history: Vec<G::Instruction>,
|
||||||
|
}
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
impl<'de, G: Game<Score = i32>> serde::de::Deserialize<'de> for Session<G>
|
impl<'de, G: Game<Score = i32>> serde::de::Deserialize<'de> for Session<G>
|
||||||
where
|
where
|
||||||
@@ -712,22 +680,10 @@ where
|
|||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
#[derive(serde_derive::Deserialize)]
|
|
||||||
#[serde(bound(deserialize = "
|
|
||||||
G: serde::Deserialize<'de>,
|
|
||||||
SessionConfig<G::Config>: serde::Deserialize<'de>,
|
|
||||||
Vec<G::Instruction>: serde::Deserialize<'de>,
|
|
||||||
"))]
|
|
||||||
struct SerializedSession<G: Game> {
|
|
||||||
config: SessionConfig<G::Config>,
|
|
||||||
initial_state: G,
|
|
||||||
instructions: Vec<G::Instruction>,
|
|
||||||
}
|
|
||||||
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.state, serialized.config);
|
||||||
for instruction in serialized.instructions {
|
for instruction in serialized.history {
|
||||||
// TODO: no unwrap
|
session.process_instruction(instruction);
|
||||||
session.stage_instruction(instruction).unwrap().commit();
|
|
||||||
}
|
}
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
@@ -763,31 +719,19 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde_derive::Serialize)]
|
use serde::ser::SerializeStruct;
|
||||||
#[serde(bound(serialize = "
|
let mut map = serializer.serialize_struct("Session", 3)?;
|
||||||
G: serde::Serialize,
|
|
||||||
SessionConfig<G::Config>: serde::Serialize,
|
|
||||||
G::Instruction: serde::Serialize,
|
|
||||||
"))]
|
|
||||||
struct SerializedSession<'a, G: Game> {
|
|
||||||
config: &'a SessionConfig<G::Config>,
|
|
||||||
initial_state: &'a G,
|
|
||||||
instructions: History<'a, G>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// serialize the initial state of the game.
|
// serialize the initial state of the game.
|
||||||
// if there is history, it is the first snapshot's state,
|
// if there is history, it is the first snapshot's state,
|
||||||
// otherwise it is the current game state since there are no moves.
|
// otherwise it is the current game state since there are no moves.
|
||||||
let initial_state = if let Some(snapshot) = self.state.history.first() {
|
let state = if let Some(snapshot) = self.state.history.first() {
|
||||||
snapshot.state()
|
snapshot.state()
|
||||||
} else {
|
} else {
|
||||||
&self.state.state
|
&self.state.state
|
||||||
};
|
};
|
||||||
let session = SerializedSession {
|
map.serialize_field("config", &self.config)?;
|
||||||
config: &self.config,
|
map.serialize_field("state", state)?;
|
||||||
initial_state,
|
map.serialize_field("history", &History(&self.state.history))?;
|
||||||
instructions: History(&self.state.history),
|
map.end()
|
||||||
};
|
|
||||||
session.serialize(serializer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ fn play_to_win(rng: &mut Rng) -> Option<KlondikeStats> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
game.stage_instruction(&mut stats, &CONFIG, instruction)
|
game.process_instruction(&mut stats, &CONFIG, instruction);
|
||||||
.unwrap()
|
|
||||||
.commit();
|
|
||||||
}
|
}
|
||||||
game.is_win().then_some(stats)
|
game.is_win().then_some(stats)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,10 @@ card_game.workspace = true
|
|||||||
klondike.workspace = true
|
klondike.workspace = true
|
||||||
rand = { version = "0.10.1", default-features = false, features = ["thread_rng"] }
|
rand = { version = "0.10.1", default-features = false, features = ["thread_rng"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rmp-serde = "1.3.1"
|
||||||
|
serde = { version = "1.0.228", default-features = false }
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ use klondike::{
|
|||||||
KlondikePile, KlondikePileStack, SkipCards, Tableau, TableauStack,
|
KlondikePile, KlondikePileStack, SkipCards, Tableau, TableauStack,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
struct Displayed<T>(T);
|
struct Displayed<T>(T);
|
||||||
|
|
||||||
@@ -210,7 +213,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 +231,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,16 +280,13 @@ fn main() -> Result<(), std::io::Error> {
|
|||||||
.state()
|
.state()
|
||||||
.get_auto_move(&session.config().inner)
|
.get_auto_move(&session.config().inner)
|
||||||
{
|
{
|
||||||
session.stage_instruction(instruction).unwrap().commit();
|
session.process_instruction(instruction);
|
||||||
} else {
|
} else {
|
||||||
println!("No valid moves!");
|
println!("No valid moves!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SessionInstruction::Stock => {
|
SessionInstruction::Stock => {
|
||||||
session
|
session.process_instruction(KlondikeInstruction::RotateStock)
|
||||||
.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(
|
||||||
@@ -294,7 +294,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||||||
session.state().state(),
|
session.state().state(),
|
||||||
naive_instruction,
|
naive_instruction,
|
||||||
) {
|
) {
|
||||||
session.stage_instruction(instruction).unwrap().commit();
|
session.process_instruction(instruction);
|
||||||
} else {
|
} else {
|
||||||
println!("Invalid move!");
|
println!("Invalid move!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::Klondike;
|
|
||||||
use card_game::Session;
|
use card_game::Session;
|
||||||
|
use klondike::Klondike;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_is_winnable() {
|
fn test_is_winnable() {
|
||||||
// is winnable
|
// is winnable
|
||||||
@@ -16,17 +15,13 @@ fn test_is_winnable() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_json() {
|
fn test_json() {
|
||||||
let mut session = Session::new_default(Klondike::with_seed(124));
|
let mut session = Session::new_default(Klondike::with_seed(124));
|
||||||
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
|
session.process_instruction(snapshot.instruction().clone());
|
||||||
.stage_instruction(snapshot.instruction().clone())
|
|
||||||
.unwrap()
|
|
||||||
.commit();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let serialized = serde_json::to_string(&session).unwrap();
|
let serialized = serde_json::to_string(&session).unwrap();
|
||||||
@@ -34,21 +29,15 @@ fn test_json() {
|
|||||||
let round_trip_session: Session<Klondike> = serde_json::from_str(&serialized).unwrap();
|
let round_trip_session: Session<Klondike> = serde_json::from_str(&serialized).unwrap();
|
||||||
let serialized2 = serde_json::to_string(&round_trip_session).unwrap();
|
let serialized2 = serde_json::to_string(&round_trip_session).unwrap();
|
||||||
assert_eq!(serialized, serialized2);
|
assert_eq!(serialized, serialized2);
|
||||||
|
|
||||||
insta::assert_snapshot!(serialized);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rmp() {
|
fn test_rmp() {
|
||||||
let mut session = Session::new_default(Klondike::with_seed(124));
|
let mut session = Session::new_default(Klondike::with_seed(124));
|
||||||
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
|
session.process_instruction(snapshot.instruction().clone());
|
||||||
.stage_instruction(snapshot.instruction().clone())
|
|
||||||
.unwrap()
|
|
||||||
.commit();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let serialized = rmp_serde::to_vec(&session).unwrap();
|
let serialized = rmp_serde::to_vec(&session).unwrap();
|
||||||
@@ -56,6 +45,4 @@ fn test_rmp() {
|
|||||||
let round_trip_session: Session<Klondike> = rmp_serde::from_slice(&serialized).unwrap();
|
let round_trip_session: Session<Klondike> = rmp_serde::from_slice(&serialized).unwrap();
|
||||||
let serialized2 = rmp_serde::to_vec(&round_trip_session).unwrap();
|
let serialized2 = rmp_serde::to_vec(&round_trip_session).unwrap();
|
||||||
assert_eq!(serialized, serialized2);
|
assert_eq!(serialized, serialized2);
|
||||||
|
|
||||||
insta::assert_binary_snapshot!("save_rmp.bin", serialized);
|
|
||||||
}
|
}
|
||||||
+2
-8
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "klondike"
|
name = "klondike"
|
||||||
version = "0.4.0"
|
version = "0.3.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -9,15 +9,9 @@ rand = { version = "0.10.1", default-features = false, features = ["std_rng"] }
|
|||||||
serde = { version = "1.0.228", default-features = false, optional = true }
|
serde = { version = "1.0.228", default-features = false, optional = true }
|
||||||
serde_derive = { 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"
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["serde"]
|
default = ["serde"]
|
||||||
serde = ["dep:serde", "dep:serde_derive", "card_game/serde"]
|
serde = ["dep:serde", "dep:serde_derive"]
|
||||||
|
|||||||
+1
-1
@@ -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.stage_instruction(instruction).unwrap().commit();
|
session.process_instruction(instruction);
|
||||||
|
|
||||||
// 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() {
|
||||||
|
|||||||
+17
-80
@@ -1,9 +1,6 @@
|
|||||||
pub type Rng = rand::rngs::StdRng;
|
pub type Rng = rand::rngs::StdRng;
|
||||||
|
|
||||||
use card_game::{Card, Game, Pile, Rank, Stack, StagedInstruction};
|
use card_game::{Card, Game, Pile, Rank, Stack};
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test;
|
|
||||||
|
|
||||||
// test readme
|
// test readme
|
||||||
#[doc = include_str!("../README.md")]
|
#[doc = include_str!("../README.md")]
|
||||||
@@ -164,38 +161,6 @@ impl Tableau {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub struct TableauIter {
|
|
||||||
tableau: Option<Tableau>,
|
|
||||||
}
|
|
||||||
impl TableauIter {
|
|
||||||
pub const fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
tableau: Some(Tableau::ITER_BEGIN),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Default for TableauIter {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Iterator for TableauIter {
|
|
||||||
type Item = Tableau;
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
let tableau = self.tableau?;
|
|
||||||
self.tableau = tableau.next();
|
|
||||||
Some(tableau)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl IntoIterator for Tableau {
|
|
||||||
type Item = Self;
|
|
||||||
type IntoIter = TableauIter;
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
TableauIter {
|
|
||||||
tableau: Some(self),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum Foundation {
|
pub enum Foundation {
|
||||||
@@ -216,38 +181,6 @@ impl Foundation {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub struct FoundationIter {
|
|
||||||
foundation: Option<Foundation>,
|
|
||||||
}
|
|
||||||
impl FoundationIter {
|
|
||||||
pub const fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
foundation: Some(Foundation::ITER_BEGIN),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Default for FoundationIter {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Iterator for FoundationIter {
|
|
||||||
type Item = Foundation;
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
let foundation = self.foundation?;
|
|
||||||
self.foundation = foundation.next();
|
|
||||||
Some(foundation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl IntoIterator for Foundation {
|
|
||||||
type Item = Self;
|
|
||||||
type IntoIter = FoundationIter;
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
FoundationIter {
|
|
||||||
foundation: Some(self),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum KlondikePile {
|
pub enum KlondikePile {
|
||||||
@@ -621,7 +554,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,34 +763,38 @@ 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(valid_instruction: StagedInstruction<'_, Self>) {
|
fn process_instruction(
|
||||||
let (game, stats, config, instruction) = valid_instruction.consume();
|
&mut self,
|
||||||
|
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 game.state.stock.face_down().is_empty() {
|
if self.state.stock.face_down().is_empty() {
|
||||||
game.state.stock.flip_it_and_reverse_it();
|
self.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 {
|
||||||
game.state.stock.flip_up();
|
self.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) = game.state.take_top_card_flip_up(src);
|
let (card, did_flip_up) = self.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();
|
||||||
}
|
}
|
||||||
game.state.extend_foundation(foundation, card);
|
self.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 }) => {
|
||||||
@@ -866,11 +803,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) = game.state.take_stack_flip_up(src);
|
let (cards, did_flip_up) = self.state.take_stack_flip_up(src);
|
||||||
if did_flip_up {
|
if did_flip_up {
|
||||||
stats.increment_flip_up_bonus();
|
stats.increment_flip_up_bonus();
|
||||||
}
|
}
|
||||||
game.state.extend_tableau(tableau, cards);
|
self.state.extend_tableau(tableau, cards);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
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},"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]}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
source: klondike/src/test.rs
|
|
||||||
expression: serialized
|
|
||||||
extension: bin
|
|
||||||
snapshot_kind: binary
|
|
||||||
---
|
|
||||||
Binary file not shown.
Reference in New Issue
Block a user