move into folder
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "card_game"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
repository = "https://git.aleshym.co/Quaternions/card_game"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Card game library."
|
||||
authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
keywords = ["card", "cards", "solitaire", "klondike"]
|
||||
|
||||
[dependencies]
|
||||
arrayvec = "0.7.6"
|
||||
rand = { version = "0.10.1", default-features = false, features = ["thread_rng"] }
|
||||
@@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,50 @@
|
||||
Card Game
|
||||
=========
|
||||
|
||||
`card_game` is a library to implement card games. Mainly interesting for the `Game` trait and the `Session` type. Contains klondike as the reference implementation.
|
||||
|
||||
## Example
|
||||
|
||||
```rust
|
||||
use card_game::Rng;
|
||||
use card_game::card_game::{Session, Game};
|
||||
use card_game::klondike::Klondike;
|
||||
|
||||
// create game session
|
||||
let game = Klondike::new_random_default();
|
||||
let mut session = Session::new(game);
|
||||
|
||||
// is winnable
|
||||
let is_winnable = session.is_winnable().is_some();
|
||||
|
||||
// play game
|
||||
while let Some(instruction) = session.possible_instructions().next() {
|
||||
session.process_instruction(instruction);
|
||||
}
|
||||
|
||||
// did win
|
||||
let is_win = session.is_win();
|
||||
|
||||
// print session history
|
||||
for (i, instruction) in session.history().iter().enumerate() {
|
||||
println!("move {i} = {instruction:?}");
|
||||
}
|
||||
|
||||
println!("is_winnable = {is_winnable}");
|
||||
println!("is_win = {is_win}");
|
||||
```
|
||||
|
||||
#### License
|
||||
|
||||
<sup>
|
||||
Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
|
||||
2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
|
||||
</sup>
|
||||
|
||||
<br>
|
||||
|
||||
<sub>
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
|
||||
be dual licensed as above, without any additional terms or conditions.
|
||||
</sub>
|
||||
@@ -0,0 +1,314 @@
|
||||
use core::ops::RangeBounds;
|
||||
|
||||
// TODO: pub struct ValidInstruction<I>(I);
|
||||
pub trait Game {
|
||||
type Instruction;
|
||||
fn possible_instructions(&self) -> impl Iterator<Item = Self::Instruction> + use<Self>;
|
||||
fn is_instruction_valid(&self, instruction: Self::Instruction) -> bool;
|
||||
fn process_instruction(&mut self, instruction: Self::Instruction);
|
||||
fn is_win(&self) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum Suit {
|
||||
Spades = 0b00,
|
||||
Hearts = 0b01,
|
||||
Clubs = 0b10,
|
||||
Diamonds = 0b11,
|
||||
}
|
||||
impl Suit {
|
||||
pub const SUITS: [Self; 4] = [Self::Spades, Self::Hearts, Self::Clubs, Self::Diamonds];
|
||||
/// Is the suit red.
|
||||
pub fn is_red(self) -> bool {
|
||||
self as u8 & 0b01 != 0
|
||||
}
|
||||
/// Is the suit shape spikey. (Bouba/kiki)
|
||||
pub fn is_kiki(self) -> bool {
|
||||
self as u8 & 0b10 != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct CardValue(u8);
|
||||
impl CardValue {
|
||||
pub const ACE: Self = CardValue(1);
|
||||
pub const TWO: Self = CardValue(2);
|
||||
pub const THREE: Self = CardValue(3);
|
||||
pub const FOUR: Self = CardValue(4);
|
||||
pub const FIVE: Self = CardValue(5);
|
||||
pub const SIX: Self = CardValue(6);
|
||||
pub const SEVEN: Self = CardValue(7);
|
||||
pub const EIGHT: Self = CardValue(8);
|
||||
pub const NINE: Self = CardValue(9);
|
||||
pub const TEN: Self = CardValue(10);
|
||||
pub const JACK: Self = CardValue(11);
|
||||
pub const QUEEN: Self = CardValue(12);
|
||||
pub const KING: Self = CardValue(13);
|
||||
pub fn get(self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
pub fn checked_add(self, offset: u8) -> Option<CardValue> {
|
||||
let new_value = self.0.checked_add(offset)?;
|
||||
if 13 < new_value {
|
||||
None
|
||||
} else {
|
||||
Some(CardValue(new_value))
|
||||
}
|
||||
}
|
||||
pub fn checked_sub(self, offset: u8) -> Option<CardValue> {
|
||||
let new_value = self.0.checked_sub(offset)?;
|
||||
if new_value < 1 {
|
||||
None
|
||||
} else {
|
||||
Some(CardValue(new_value))
|
||||
}
|
||||
}
|
||||
}
|
||||
/// An identifier which specifies the deck id, suit, and card value.
|
||||
/// 2 bits for deck ID
|
||||
/// 2 bits for suit ID
|
||||
/// 4 bits for card Value
|
||||
/// TODO: better encoding for slightly more decks
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Card(u8);
|
||||
impl Card {
|
||||
pub fn new(deck: u8, suit: Suit, CardValue(value): CardValue) -> Self {
|
||||
Self(deck << 6 | (suit as u8) << 4 | value)
|
||||
}
|
||||
pub fn value(&self) -> CardValue {
|
||||
let masked = self.0 & 0b1111;
|
||||
CardValue(masked)
|
||||
}
|
||||
pub fn suit(&self) -> Suit {
|
||||
let red = self.is_red();
|
||||
let kiki = self.is_kiki();
|
||||
match (kiki, red) {
|
||||
(false, false) => Suit::Spades,
|
||||
(false, true) => Suit::Hearts,
|
||||
(true, false) => Suit::Clubs,
|
||||
(true, true) => Suit::Diamonds,
|
||||
}
|
||||
}
|
||||
/// Is the suit red.
|
||||
pub fn is_red(&self) -> bool {
|
||||
self.0 & 0b010000 != 0
|
||||
}
|
||||
/// Is the suit shape spikey. (Bouba/kiki)
|
||||
pub fn is_kiki(&self) -> bool {
|
||||
self.0 & 0b100000 != 0
|
||||
}
|
||||
pub fn deck(&self) -> u8 {
|
||||
self.0 >> 6
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Stack<const CAP: usize>(arrayvec::ArrayVec<Card, CAP>);
|
||||
impl<const CAP: usize> Stack<CAP> {
|
||||
pub const fn new() -> Self {
|
||||
Self(arrayvec::ArrayVec::new_const())
|
||||
}
|
||||
pub fn take_range<R: RangeBounds<usize>>(&mut self, range: R) -> Self {
|
||||
Stack::from_iter(self.drain(range))
|
||||
}
|
||||
}
|
||||
impl Stack<52> {
|
||||
/// Generate a full deck of cards with the specified deck id.
|
||||
pub fn full_deck(deck: u8) -> Self {
|
||||
let mut stack = arrayvec::ArrayVec::new();
|
||||
for suit in Suit::SUITS {
|
||||
for value in 1..=13 {
|
||||
stack.push(Card::new(deck, suit, CardValue(value)));
|
||||
}
|
||||
}
|
||||
Stack(stack)
|
||||
}
|
||||
}
|
||||
impl<const CAP: usize> From<arrayvec::ArrayVec<Card, CAP>> for Stack<CAP> {
|
||||
fn from(value: arrayvec::ArrayVec<Card, CAP>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
impl<const CAP: usize> FromIterator<Card> for Stack<CAP> {
|
||||
fn from_iter<T: IntoIterator<Item = Card>>(iter: T) -> Self {
|
||||
Self(arrayvec::ArrayVec::from_iter(iter))
|
||||
}
|
||||
}
|
||||
impl<const CAP: usize> core::ops::Deref for Stack<CAP> {
|
||||
type Target = arrayvec::ArrayVec<Card, CAP>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl<const CAP: usize> core::ops::DerefMut for Stack<CAP> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
impl<const CAP: usize> IntoIterator for Stack<CAP> {
|
||||
type Item = Card;
|
||||
type IntoIter = arrayvec::IntoIter<Card, CAP>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Pile<const DN: usize, const UP: usize> {
|
||||
face_down: Stack<DN>,
|
||||
face_up: Stack<UP>,
|
||||
}
|
||||
impl<const DN: usize, const UP: usize> Pile<DN, UP> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
face_down: Stack::new(),
|
||||
face_up: Stack::new(),
|
||||
}
|
||||
}
|
||||
pub fn new_face_down(stack: Stack<DN>) -> Self {
|
||||
Self {
|
||||
face_down: stack,
|
||||
face_up: Stack::new(),
|
||||
}
|
||||
}
|
||||
pub fn flip_up(&mut self) {
|
||||
if let Some(card) = self.face_down.pop() {
|
||||
self.face_up.push(card);
|
||||
}
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.face_down.is_empty() && self.face_up.is_empty()
|
||||
}
|
||||
pub fn pop(&mut self) -> Option<Card> {
|
||||
self.face_up.pop()
|
||||
}
|
||||
pub fn pop_flip_up(&mut self) -> Option<Card> {
|
||||
let card = self.face_up.pop()?;
|
||||
if self.face_up.is_empty() {
|
||||
self.flip_up();
|
||||
}
|
||||
Some(card)
|
||||
}
|
||||
pub fn take_range<R: RangeBounds<usize>>(&mut self, range: R) -> Stack<UP> {
|
||||
// if self.face_up.get(range).is_none() {
|
||||
// return None;
|
||||
// }
|
||||
self.face_up.take_range(range)
|
||||
}
|
||||
pub fn take_range_flip_up<R: RangeBounds<usize>>(&mut self, range: R) -> Stack<UP> {
|
||||
let cards = self.take_range(range);
|
||||
if self.face_up.is_empty() {
|
||||
self.flip_up();
|
||||
}
|
||||
cards
|
||||
}
|
||||
pub fn push(&mut self, card: Card) {
|
||||
self.face_up.push(card);
|
||||
}
|
||||
pub fn extend<I: IntoIterator<Item = Card>>(&mut self, cards: I) {
|
||||
self.face_up.extend(cards);
|
||||
}
|
||||
pub fn face_up(&self) -> &[Card] {
|
||||
&self.face_up
|
||||
}
|
||||
pub fn face_down(&self) -> &[Card] {
|
||||
&self.face_down
|
||||
}
|
||||
}
|
||||
impl<const CAP: usize> Pile<CAP, CAP> {
|
||||
pub fn flip_it_and_reverse_it(&mut self) {
|
||||
self.swap_up_down();
|
||||
self.face_down.reverse();
|
||||
}
|
||||
pub fn swap_up_down(&mut self) {
|
||||
core::mem::swap(&mut self.face_up, &mut self.face_down);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Session<G: Game> {
|
||||
seed: G,
|
||||
state: G,
|
||||
history: Vec<G::Instruction>,
|
||||
}
|
||||
impl<G: Game + Clone + Eq + core::hash::Hash> Session<G>
|
||||
where
|
||||
G::Instruction: Clone + Eq + core::hash::Hash,
|
||||
{
|
||||
pub fn new(state: G) -> Self {
|
||||
Self {
|
||||
seed: state.clone(),
|
||||
state,
|
||||
history: Vec::new(),
|
||||
}
|
||||
}
|
||||
pub fn state(&self) -> &G {
|
||||
&self.state
|
||||
}
|
||||
pub fn history(&self) -> &[G::Instruction] {
|
||||
&self.history
|
||||
}
|
||||
pub fn is_winnable(&self) -> Option<Vec<G::Instruction>> {
|
||||
let mut observed = std::collections::HashSet::new();
|
||||
struct StateMachine<G, P, I> {
|
||||
state: G,
|
||||
possible_instructions_iter: P,
|
||||
instruction: I,
|
||||
}
|
||||
let mut state = self.state.clone();
|
||||
let mut it = state.possible_instructions();
|
||||
let mut path = Vec::new();
|
||||
'outer: while !state.is_win() {
|
||||
observed.insert(state.clone());
|
||||
for instruction in &mut it {
|
||||
let mut next_state = state.clone();
|
||||
next_state.process_instruction(instruction.clone());
|
||||
if !observed.contains(&next_state) {
|
||||
let possible_instructions_iter =
|
||||
core::mem::replace(&mut it, next_state.possible_instructions());
|
||||
let state = core::mem::replace(&mut state, next_state);
|
||||
path.push(StateMachine {
|
||||
state,
|
||||
possible_instructions_iter,
|
||||
instruction,
|
||||
});
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
let Some(last_state) = path.pop() else {
|
||||
return None;
|
||||
};
|
||||
state = last_state.state;
|
||||
it = last_state.possible_instructions_iter;
|
||||
}
|
||||
Some(path.into_iter().map(|state| state.instruction).collect())
|
||||
}
|
||||
pub fn undo(&mut self) {
|
||||
// replay the entire history of the game except one move
|
||||
self.history.pop();
|
||||
let mut state = self.seed.clone();
|
||||
for instruction in self.history() {
|
||||
state.process_instruction(instruction.clone());
|
||||
}
|
||||
self.state = state;
|
||||
}
|
||||
}
|
||||
impl<G: Game> Game for Session<G>
|
||||
where
|
||||
G::Instruction: Clone,
|
||||
{
|
||||
type Instruction = G::Instruction;
|
||||
fn possible_instructions(&self) -> impl Iterator<Item = Self::Instruction> + use<G> {
|
||||
self.state.possible_instructions()
|
||||
}
|
||||
fn is_instruction_valid(&self, instruction: Self::Instruction) -> bool {
|
||||
self.state.is_instruction_valid(instruction)
|
||||
}
|
||||
fn process_instruction(&mut self, instruction: Self::Instruction) {
|
||||
self.history.push(instruction.clone());
|
||||
self.state.process_instruction(instruction);
|
||||
}
|
||||
fn is_win(&self) -> bool {
|
||||
self.state.is_win()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
use crate::Rng;
|
||||
use crate::card_game::{Card, CardValue, Game, Pile, Stack};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct KlondikeConfig {}
|
||||
impl Default for KlondikeConfig {
|
||||
fn default() -> Self {
|
||||
KlondikeConfig {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum Tableau {
|
||||
Tableau1,
|
||||
Tableau2,
|
||||
Tableau3,
|
||||
Tableau4,
|
||||
Tableau5,
|
||||
Tableau6,
|
||||
Tableau7,
|
||||
}
|
||||
impl Tableau {
|
||||
const ITER_BEGIN: Self = Self::Tableau1;
|
||||
const fn next(self) -> Option<Self> {
|
||||
use Tableau::*;
|
||||
Some(match self {
|
||||
Tableau1 => Tableau2,
|
||||
Tableau2 => Tableau3,
|
||||
Tableau3 => Tableau4,
|
||||
Tableau4 => Tableau5,
|
||||
Tableau5 => Tableau6,
|
||||
Tableau6 => Tableau7,
|
||||
Tableau7 => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum Foundation {
|
||||
Foundation1,
|
||||
Foundation2,
|
||||
Foundation3,
|
||||
Foundation4,
|
||||
}
|
||||
impl Foundation {
|
||||
const ITER_BEGIN: Self = Self::Foundation1;
|
||||
const fn next(self) -> Option<Self> {
|
||||
use Foundation::*;
|
||||
Some(match self {
|
||||
Foundation1 => Foundation2,
|
||||
Foundation2 => Foundation3,
|
||||
Foundation3 => Foundation4,
|
||||
Foundation4 => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum KlondikePile {
|
||||
Tableau(Tableau),
|
||||
Stock,
|
||||
Foundation(Foundation),
|
||||
}
|
||||
impl KlondikePile {
|
||||
const ITER_BEGIN: Self = Self::Tableau(Tableau::ITER_BEGIN);
|
||||
const fn next(self) -> Option<Self> {
|
||||
Some(match self {
|
||||
Self::Tableau(tableau_stack) => match tableau_stack.next() {
|
||||
Some(tableau_stack) => Self::Tableau(tableau_stack),
|
||||
None => Self::Stock,
|
||||
},
|
||||
Self::Stock => Self::Foundation(Foundation::ITER_BEGIN),
|
||||
Self::Foundation(foundation) => match foundation.next() {
|
||||
Some(foundation) => Self::Foundation(foundation),
|
||||
None => return None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
impl From<Tableau> for KlondikePile {
|
||||
fn from(value: Tableau) -> Self {
|
||||
KlondikePile::Tableau(value)
|
||||
}
|
||||
}
|
||||
impl From<Foundation> for KlondikePile {
|
||||
fn from(value: Foundation) -> Self {
|
||||
KlondikePile::Foundation(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SkipCards {
|
||||
Skip0,
|
||||
Skip1,
|
||||
Skip2,
|
||||
Skip3,
|
||||
Skip4,
|
||||
Skip5,
|
||||
Skip6,
|
||||
Skip7,
|
||||
Skip8,
|
||||
Skip9,
|
||||
Skip10,
|
||||
Skip11,
|
||||
Skip12,
|
||||
}
|
||||
impl SkipCards {
|
||||
const ITER_BEGIN: Self = Self::Skip0;
|
||||
const fn next(self) -> Option<Self> {
|
||||
use SkipCards::*;
|
||||
Some(match self {
|
||||
Skip0 => Skip1,
|
||||
Skip1 => Skip2,
|
||||
Skip2 => Skip3,
|
||||
Skip3 => Skip4,
|
||||
Skip4 => Skip5,
|
||||
Skip5 => Skip6,
|
||||
Skip6 => Skip7,
|
||||
Skip7 => Skip8,
|
||||
Skip8 => Skip9,
|
||||
Skip9 => Skip10,
|
||||
Skip10 => Skip11,
|
||||
Skip11 => Skip12,
|
||||
Skip12 => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct TableauStack {
|
||||
pub tableau: Tableau,
|
||||
pub skip_cards: SkipCards,
|
||||
}
|
||||
|
||||
impl TableauStack {
|
||||
const ITER_BEGIN: Self = Self {
|
||||
tableau: Tableau::ITER_BEGIN,
|
||||
skip_cards: SkipCards::ITER_BEGIN,
|
||||
};
|
||||
const fn next(self) -> Option<Self> {
|
||||
let TableauStack {
|
||||
tableau,
|
||||
skip_cards,
|
||||
} = self;
|
||||
if let Some(skip_cards) = skip_cards.next() {
|
||||
return Some(Self {
|
||||
tableau,
|
||||
skip_cards,
|
||||
});
|
||||
}
|
||||
if let Some(tableau) = tableau.next() {
|
||||
let skip_cards = SkipCards::Skip0;
|
||||
return Some(Self {
|
||||
tableau,
|
||||
skip_cards,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum KlondikePileStack {
|
||||
Tableau(TableauStack),
|
||||
Stock,
|
||||
Foundation(Foundation),
|
||||
}
|
||||
impl KlondikePileStack {
|
||||
const ITER_BEGIN: Self = Self::Tableau(TableauStack::ITER_BEGIN);
|
||||
const fn next(self) -> Option<Self> {
|
||||
Some(match self {
|
||||
Self::Tableau(tableau_stack) => match tableau_stack.next() {
|
||||
Some(tableau_stack) => Self::Tableau(tableau_stack),
|
||||
None => Self::Stock,
|
||||
},
|
||||
Self::Stock => Self::Foundation(Foundation::ITER_BEGIN),
|
||||
Self::Foundation(foundation) => match foundation.next() {
|
||||
Some(foundation) => Self::Foundation(foundation),
|
||||
None => return None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct DstFoundation {
|
||||
pub src: KlondikePile,
|
||||
pub foundation: Foundation,
|
||||
}
|
||||
impl DstFoundation {
|
||||
const ITER_BEGIN: Self = Self {
|
||||
src: KlondikePile::ITER_BEGIN,
|
||||
foundation: Foundation::ITER_BEGIN,
|
||||
};
|
||||
const fn next(self) -> Option<Self> {
|
||||
let DstFoundation { src, foundation } = self;
|
||||
if let Some(src) = src.next() {
|
||||
return Some(Self { src, foundation });
|
||||
}
|
||||
if let Some(foundation) = foundation.next() {
|
||||
let src = KlondikePile::ITER_BEGIN;
|
||||
return Some(Self { src, foundation });
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct DstTableau {
|
||||
pub src: KlondikePileStack,
|
||||
pub tableau: Tableau,
|
||||
}
|
||||
impl DstTableau {
|
||||
const ITER_BEGIN: Self = Self {
|
||||
src: KlondikePileStack::ITER_BEGIN,
|
||||
tableau: Tableau::ITER_BEGIN,
|
||||
};
|
||||
const fn next(self) -> Option<Self> {
|
||||
let DstTableau { src, tableau } = self;
|
||||
if let Some(src) = src.next() {
|
||||
return Some(Self { src, tableau });
|
||||
}
|
||||
if let Some(tableau) = tableau.next() {
|
||||
let src = KlondikePileStack::ITER_BEGIN;
|
||||
return Some(Self { src, tableau });
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum KlondikeInstruction {
|
||||
DstFoundation(DstFoundation),
|
||||
DstTableau(DstTableau),
|
||||
RotateStock,
|
||||
}
|
||||
impl KlondikeInstruction {
|
||||
const ITER_BEGIN: Self = Self::DstFoundation(DstFoundation::ITER_BEGIN);
|
||||
const fn next(self) -> Option<Self> {
|
||||
Some(match self {
|
||||
Self::DstFoundation(dst_foundation) => match dst_foundation.next() {
|
||||
Some(dst_foundation) => Self::DstFoundation(dst_foundation),
|
||||
None => Self::DstTableau(DstTableau::ITER_BEGIN),
|
||||
},
|
||||
Self::DstTableau(tableau) => match tableau.next() {
|
||||
Some(tableau) => Self::DstTableau(tableau),
|
||||
None => Self::RotateStock,
|
||||
},
|
||||
Self::RotateStock => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const TABLEAUS: usize = 7;
|
||||
const fn sum(n: usize) -> usize {
|
||||
n * (n + 1) / 2
|
||||
}
|
||||
const STOCK: usize = 52 - sum(TABLEAUS);
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct KlondikeState {
|
||||
stock: Pile<STOCK, STOCK>,
|
||||
foundations: [Stack<13>; 4],
|
||||
tableau1: Pile<0, 13>,
|
||||
tableau2: Pile<1, 13>,
|
||||
tableau3: Pile<2, 13>,
|
||||
tableau4: Pile<3, 13>,
|
||||
tableau5: Pile<4, 13>,
|
||||
tableau6: Pile<5, 13>,
|
||||
tableau7: Pile<6, 13>,
|
||||
}
|
||||
impl KlondikeState {
|
||||
pub const fn stock(&self) -> &Pile<STOCK, STOCK> {
|
||||
&self.stock
|
||||
}
|
||||
pub const fn foundation1(&self) -> &Stack<13> {
|
||||
&self.foundations[Foundation::Foundation1 as usize]
|
||||
}
|
||||
pub const fn foundation2(&self) -> &Stack<13> {
|
||||
&self.foundations[Foundation::Foundation2 as usize]
|
||||
}
|
||||
pub const fn foundation3(&self) -> &Stack<13> {
|
||||
&self.foundations[Foundation::Foundation3 as usize]
|
||||
}
|
||||
pub const fn foundation4(&self) -> &Stack<13> {
|
||||
&self.foundations[Foundation::Foundation4 as usize]
|
||||
}
|
||||
pub const fn tableau1(&self) -> &Pile<0, 13> {
|
||||
&self.tableau1
|
||||
}
|
||||
pub const fn tableau2(&self) -> &Pile<1, 13> {
|
||||
&self.tableau2
|
||||
}
|
||||
pub const fn tableau3(&self) -> &Pile<2, 13> {
|
||||
&self.tableau3
|
||||
}
|
||||
pub const fn tableau4(&self) -> &Pile<3, 13> {
|
||||
&self.tableau4
|
||||
}
|
||||
pub const fn tableau5(&self) -> &Pile<4, 13> {
|
||||
&self.tableau5
|
||||
}
|
||||
pub const fn tableau6(&self) -> &Pile<5, 13> {
|
||||
&self.tableau6
|
||||
}
|
||||
pub const fn tableau7(&self) -> &Pile<6, 13> {
|
||||
&self.tableau7
|
||||
}
|
||||
pub fn card(&self, src: KlondikePileStack) -> Option<&Card> {
|
||||
match src {
|
||||
KlondikePileStack::Tableau(TableauStack {
|
||||
tableau,
|
||||
skip_cards,
|
||||
}) => match tableau {
|
||||
Tableau::Tableau1 => self.tableau1.face_up().get(skip_cards as usize),
|
||||
Tableau::Tableau2 => self.tableau2.face_up().get(skip_cards as usize),
|
||||
Tableau::Tableau3 => self.tableau3.face_up().get(skip_cards as usize),
|
||||
Tableau::Tableau4 => self.tableau4.face_up().get(skip_cards as usize),
|
||||
Tableau::Tableau5 => self.tableau5.face_up().get(skip_cards as usize),
|
||||
Tableau::Tableau6 => self.tableau6.face_up().get(skip_cards as usize),
|
||||
Tableau::Tableau7 => self.tableau7.face_up().get(skip_cards as usize),
|
||||
},
|
||||
KlondikePileStack::Foundation(foundation) => {
|
||||
self.foundations[foundation as usize].last()
|
||||
}
|
||||
KlondikePileStack::Stock => self.stock.face_up().last(),
|
||||
}
|
||||
}
|
||||
pub fn top_card(&self, src: KlondikePile) -> Option<&Card> {
|
||||
match src {
|
||||
KlondikePile::Tableau(tableau) => match tableau {
|
||||
Tableau::Tableau1 => self.tableau1.face_up().last(),
|
||||
Tableau::Tableau2 => self.tableau2.face_up().last(),
|
||||
Tableau::Tableau3 => self.tableau3.face_up().last(),
|
||||
Tableau::Tableau4 => self.tableau4.face_up().last(),
|
||||
Tableau::Tableau5 => self.tableau5.face_up().last(),
|
||||
Tableau::Tableau6 => self.tableau6.face_up().last(),
|
||||
Tableau::Tableau7 => self.tableau7.face_up().last(),
|
||||
},
|
||||
KlondikePile::Foundation(foundation) => self.foundations[foundation as usize].last(),
|
||||
KlondikePile::Stock => self.stock.face_up().last(),
|
||||
}
|
||||
}
|
||||
fn take_stack(&mut self, src: KlondikePileStack) -> Stack<13> {
|
||||
match src {
|
||||
KlondikePileStack::Tableau(TableauStack {
|
||||
tableau,
|
||||
skip_cards,
|
||||
}) => match tableau {
|
||||
Tableau::Tableau1 => self.tableau1.take_range_flip_up(skip_cards as usize..),
|
||||
Tableau::Tableau2 => self.tableau2.take_range_flip_up(skip_cards as usize..),
|
||||
Tableau::Tableau3 => self.tableau3.take_range_flip_up(skip_cards as usize..),
|
||||
Tableau::Tableau4 => self.tableau4.take_range_flip_up(skip_cards as usize..),
|
||||
Tableau::Tableau5 => self.tableau5.take_range_flip_up(skip_cards as usize..),
|
||||
Tableau::Tableau6 => self.tableau6.take_range_flip_up(skip_cards as usize..),
|
||||
Tableau::Tableau7 => self.tableau7.take_range_flip_up(skip_cards as usize..),
|
||||
},
|
||||
KlondikePileStack::Foundation(foundation) => {
|
||||
Stack::from_iter(self.foundations[foundation as usize].pop())
|
||||
}
|
||||
KlondikePileStack::Stock => Stack::from_iter(self.stock.pop()),
|
||||
}
|
||||
}
|
||||
fn take_top_card(&mut self, src: KlondikePile) -> Option<Card> {
|
||||
match src {
|
||||
KlondikePile::Tableau(tableau) => match tableau {
|
||||
Tableau::Tableau1 => self.tableau1.pop_flip_up(),
|
||||
Tableau::Tableau2 => self.tableau2.pop_flip_up(),
|
||||
Tableau::Tableau3 => self.tableau3.pop_flip_up(),
|
||||
Tableau::Tableau4 => self.tableau4.pop_flip_up(),
|
||||
Tableau::Tableau5 => self.tableau5.pop_flip_up(),
|
||||
Tableau::Tableau6 => self.tableau6.pop_flip_up(),
|
||||
Tableau::Tableau7 => self.tableau7.pop_flip_up(),
|
||||
},
|
||||
KlondikePile::Foundation(foundation) => self.foundations[foundation as usize].pop(),
|
||||
KlondikePile::Stock => self.stock.pop_flip_up(),
|
||||
}
|
||||
}
|
||||
fn extend<I: IntoIterator<Item = Card>>(&mut self, dst: KlondikePile, cards: I) {
|
||||
match dst {
|
||||
KlondikePile::Tableau(tableau) => match tableau {
|
||||
Tableau::Tableau1 => self.tableau1.extend(cards),
|
||||
Tableau::Tableau2 => self.tableau2.extend(cards),
|
||||
Tableau::Tableau3 => self.tableau3.extend(cards),
|
||||
Tableau::Tableau4 => self.tableau4.extend(cards),
|
||||
Tableau::Tableau5 => self.tableau5.extend(cards),
|
||||
Tableau::Tableau6 => self.tableau6.extend(cards),
|
||||
Tableau::Tableau7 => self.tableau7.extend(cards),
|
||||
},
|
||||
KlondikePile::Foundation(foundation) => {
|
||||
self.foundations[foundation as usize].extend(cards)
|
||||
}
|
||||
KlondikePile::Stock => self.stock.extend(cards),
|
||||
}
|
||||
}
|
||||
pub fn is_instruction_valid(&self, instruction: KlondikeInstruction) -> bool {
|
||||
match instruction {
|
||||
// Stock -> Stock draws a card or resets the stock
|
||||
KlondikeInstruction::RotateStock => {
|
||||
// cannot move stock when stock is empty
|
||||
!self.stock.is_empty()
|
||||
}
|
||||
|
||||
// moving to foundation has special rules
|
||||
KlondikeInstruction::DstFoundation(dst_foundation) => {
|
||||
// get the top cards
|
||||
if let Some(src_card) = self.top_card(dst_foundation.src) {
|
||||
match self.top_card(dst_foundation.foundation.into()) {
|
||||
// destination card exists
|
||||
Some(dst_card) => {
|
||||
// suit matches?
|
||||
src_card.suit() == dst_card.suit()
|
||||
// value is +1?
|
||||
&& dst_card.value().checked_add(1) == Some(src_card.value())
|
||||
}
|
||||
// only ace is allowed to go onto empty foundation
|
||||
None => src_card.value() == CardValue::ACE,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
// other = move to tableau
|
||||
KlondikeInstruction::DstTableau(dst_tableau) => {
|
||||
// get the cards
|
||||
if let Some(src_card) = self.card(dst_tableau.src) {
|
||||
match self.top_card(dst_tableau.tableau.into()) {
|
||||
// destination card exists
|
||||
Some(dst_card) => {
|
||||
// red-ness is opposite?
|
||||
src_card.is_red() != dst_card.is_red()
|
||||
// value is -1?
|
||||
&& dst_card.value().checked_sub(1) == Some(src_card.value())
|
||||
}
|
||||
// only king is allowed to go onto empty tableau
|
||||
None => src_card.value() == CardValue::KING,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KlondikeIter {
|
||||
instruction: Option<KlondikeInstruction>,
|
||||
}
|
||||
impl KlondikeIter {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
instruction: Some(KlondikeInstruction::ITER_BEGIN),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Iterator for KlondikeIter {
|
||||
type Item = KlondikeInstruction;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let instruction = self.instruction;
|
||||
self.instruction = instruction?.next();
|
||||
instruction
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct Klondike {
|
||||
config: KlondikeConfig,
|
||||
state: KlondikeState,
|
||||
}
|
||||
impl Klondike {
|
||||
pub fn new_random_default() -> Self {
|
||||
Self::new(Rng::default(), KlondikeConfig::default())
|
||||
}
|
||||
pub fn new(mut seed: Rng, config: KlondikeConfig) -> Self {
|
||||
// shuffle a new deck
|
||||
let mut deck = Stack::full_deck(0);
|
||||
use rand::seq::SliceRandom;
|
||||
deck.shuffle(&mut seed);
|
||||
let mut deck = deck.into_iter();
|
||||
|
||||
// generate tableaus
|
||||
fn pile<const DN: usize>(deck: &mut arrayvec::IntoIter<Card, 52>) -> Pile<DN, 13> {
|
||||
let stack = arrayvec::ArrayVec::from_iter(deck.take(DN)).into();
|
||||
let mut pile = Pile::new_face_down(stack);
|
||||
pile.push(deck.next().unwrap());
|
||||
pile
|
||||
}
|
||||
let tableau1 = pile(&mut deck);
|
||||
let tableau2 = pile(&mut deck);
|
||||
let tableau3 = pile(&mut deck);
|
||||
let tableau4 = pile(&mut deck);
|
||||
let tableau5 = pile(&mut deck);
|
||||
let tableau6 = pile(&mut deck);
|
||||
let tableau7 = pile(&mut deck);
|
||||
|
||||
// stock is remaining cards
|
||||
let stock = Pile::new_face_down(arrayvec::ArrayVec::from_iter(deck).into());
|
||||
|
||||
let state = KlondikeState {
|
||||
stock,
|
||||
foundations: core::array::from_fn(|_| Stack::new()),
|
||||
tableau1,
|
||||
tableau2,
|
||||
tableau3,
|
||||
tableau4,
|
||||
tableau5,
|
||||
tableau6,
|
||||
tableau7,
|
||||
};
|
||||
Self { config, state }
|
||||
}
|
||||
pub const fn state(&self) -> &KlondikeState {
|
||||
&self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl Game for Klondike {
|
||||
type Instruction = KlondikeInstruction;
|
||||
fn possible_instructions(&self) -> impl Iterator<Item = Self::Instruction> + use<> {
|
||||
let state = self.state.clone();
|
||||
KlondikeIter::new().filter(move |&instruction| state.is_instruction_valid(instruction))
|
||||
}
|
||||
fn is_instruction_valid(&self, instruction: Self::Instruction) -> bool {
|
||||
self.state.is_instruction_valid(instruction)
|
||||
}
|
||||
fn process_instruction(&mut self, instruction: Self::Instruction) {
|
||||
match instruction {
|
||||
// Reset the stock if it's empty
|
||||
KlondikeInstruction::RotateStock => {
|
||||
if self.state.stock.face_down().is_empty() {
|
||||
self.state.stock.flip_it_and_reverse_it();
|
||||
} else {
|
||||
self.state.stock.flip_up();
|
||||
}
|
||||
}
|
||||
KlondikeInstruction::DstFoundation(DstFoundation { src, foundation }) => {
|
||||
let cards = self.state.take_top_card(src);
|
||||
self.state.extend(foundation.into(), cards);
|
||||
}
|
||||
KlondikeInstruction::DstTableau(DstTableau { src, tableau }) => {
|
||||
let cards = self.state.take_stack(src);
|
||||
self.state.extend(tableau.into(), cards);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn is_win(&self) -> bool {
|
||||
// all face down cards empty means win
|
||||
self.state.stock.face_down().is_empty()
|
||||
&& self.state.tableau1.face_down().is_empty()
|
||||
&& self.state.tableau2.face_down().is_empty()
|
||||
&& self.state.tableau3.face_down().is_empty()
|
||||
&& self.state.tableau4.face_down().is_empty()
|
||||
&& self.state.tableau5.face_down().is_empty()
|
||||
&& self.state.tableau6.face_down().is_empty()
|
||||
&& self.state.tableau7.face_down().is_empty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
pub mod card_game;
|
||||
pub mod klondike;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
pub type Rng = rand::rngs::ThreadRng;
|
||||
|
||||
// // test readme
|
||||
// #[doc = include_str!("../README.md")]
|
||||
// #[cfg(doctest)]
|
||||
// struct ReadmeDoctests;
|
||||
@@ -0,0 +1,273 @@
|
||||
mod card_game;
|
||||
mod klondike;
|
||||
|
||||
pub type Rng = rand::rngs::ThreadRng;
|
||||
|
||||
use card_game::{Card, Game, Pile, Session, Suit};
|
||||
use klondike::{
|
||||
DstFoundation, DstTableau, Foundation, Klondike, KlondikeInstruction, KlondikePile,
|
||||
KlondikePileStack, SkipCards, Tableau, TableauStack,
|
||||
};
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
impl Display for Card {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.value().get() {
|
||||
1 => write!(f, "A"),
|
||||
11 => write!(f, "J"),
|
||||
12 => write!(f, "Q"),
|
||||
13 => write!(f, "K"),
|
||||
other => write!(f, "{other}"),
|
||||
}?;
|
||||
match self.suit() {
|
||||
Suit::Spades => write!(f, "♠"),
|
||||
Suit::Hearts => write!(f, "♡"),
|
||||
Suit::Clubs => write!(f, "♣"),
|
||||
Suit::Diamonds => write!(f, "♢"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OptionalCard<'a>(Option<&'a Card>);
|
||||
impl Display for OptionalCard<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
OptionalCard(Some(card)) => write!(f, "{card}"),
|
||||
OptionalCard(None) => write!(f, "None"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Klondike {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// Stock
|
||||
let stock_count = self.state().stock().face_down().len();
|
||||
writeln!(f, "Stock: {stock_count}")?;
|
||||
|
||||
// Hand
|
||||
let hand = self.state().stock().face_up().last();
|
||||
writeln!(f, "Hand: {}", OptionalCard(hand))?;
|
||||
|
||||
// Foundations
|
||||
write!(
|
||||
f,
|
||||
"Foundations: {} {} {} {}",
|
||||
OptionalCard(self.state().foundation1().last()),
|
||||
OptionalCard(self.state().foundation2().last()),
|
||||
OptionalCard(self.state().foundation3().last()),
|
||||
OptionalCard(self.state().foundation4().last()),
|
||||
)?;
|
||||
writeln!(f)?;
|
||||
|
||||
fn write_pile<const DN: usize, const UP: usize>(
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
pile: &Pile<DN, UP>,
|
||||
pile_id: usize,
|
||||
) -> std::fmt::Result {
|
||||
write!(f, "T{} ", pile_id)?;
|
||||
for _ in pile.face_down() {
|
||||
write!(f, "]")?;
|
||||
}
|
||||
for card in pile.face_up() {
|
||||
write!(f, "{card}")?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
Ok(())
|
||||
}
|
||||
write_pile(f, self.state().tableau1(), 1)?;
|
||||
write_pile(f, self.state().tableau2(), 2)?;
|
||||
write_pile(f, self.state().tableau3(), 3)?;
|
||||
write_pile(f, self.state().tableau4(), 4)?;
|
||||
write_pile(f, self.state().tableau5(), 5)?;
|
||||
write_pile(f, self.state().tableau6(), 6)?;
|
||||
write_pile(f, self.state().tableau7(), 7)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invalid;
|
||||
struct Parsed<T>(T);
|
||||
struct NaiveInstruction {
|
||||
src: KlondikePile,
|
||||
dst: KlondikePile,
|
||||
}
|
||||
impl core::str::FromStr for NaiveInstruction {
|
||||
type Err = Invalid;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let Parsed(src) = s.get(0..2).ok_or(Invalid)?.parse()?;
|
||||
let Parsed(dst) = s.get(3..5).ok_or(Invalid)?.parse()?;
|
||||
Ok(NaiveInstruction { src, dst })
|
||||
}
|
||||
}
|
||||
impl core::str::FromStr for Parsed<KlondikePile> {
|
||||
type Err = Invalid;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Parsed(match s {
|
||||
"st" => KlondikePile::Stock,
|
||||
"t1" => KlondikePile::Tableau(Tableau::Tableau1),
|
||||
"t2" => KlondikePile::Tableau(Tableau::Tableau2),
|
||||
"t3" => KlondikePile::Tableau(Tableau::Tableau3),
|
||||
"t4" => KlondikePile::Tableau(Tableau::Tableau4),
|
||||
"t5" => KlondikePile::Tableau(Tableau::Tableau5),
|
||||
"t6" => KlondikePile::Tableau(Tableau::Tableau6),
|
||||
"t7" => KlondikePile::Tableau(Tableau::Tableau7),
|
||||
"f1" => KlondikePile::Foundation(Foundation::Foundation1),
|
||||
"f2" => KlondikePile::Foundation(Foundation::Foundation2),
|
||||
"f3" => KlondikePile::Foundation(Foundation::Foundation3),
|
||||
"f4" => KlondikePile::Foundation(Foundation::Foundation4),
|
||||
_ => return Err(Invalid),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
enum SessionInstruction {
|
||||
New,
|
||||
Undo,
|
||||
Hint,
|
||||
Auto,
|
||||
Stock,
|
||||
Exit,
|
||||
Klondike(NaiveInstruction),
|
||||
}
|
||||
impl core::str::FromStr for SessionInstruction {
|
||||
type Err = Invalid;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"new" | "n" => Self::New,
|
||||
"undo" | "u" => Self::Undo,
|
||||
"hint" | "h" => Self::Hint,
|
||||
"auto" | "a" => Self::Auto,
|
||||
"exit" => Self::Exit,
|
||||
"s" => Self::Stock,
|
||||
other => Self::Klondike(other.parse()?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn find_valid_instruction(
|
||||
state: &Klondike,
|
||||
naive_instruction: NaiveInstruction,
|
||||
) -> Option<KlondikeInstruction> {
|
||||
const SKIP_LIST: [SkipCards; 13] = [
|
||||
SkipCards::Skip0,
|
||||
SkipCards::Skip1,
|
||||
SkipCards::Skip2,
|
||||
SkipCards::Skip3,
|
||||
SkipCards::Skip4,
|
||||
SkipCards::Skip5,
|
||||
SkipCards::Skip6,
|
||||
SkipCards::Skip7,
|
||||
SkipCards::Skip8,
|
||||
SkipCards::Skip9,
|
||||
SkipCards::Skip10,
|
||||
SkipCards::Skip11,
|
||||
SkipCards::Skip12,
|
||||
];
|
||||
let instruction = match (naive_instruction.dst, naive_instruction.src) {
|
||||
(KlondikePile::Tableau(tableau), src) => {
|
||||
let src = match src {
|
||||
KlondikePile::Tableau(src_tableau) => {
|
||||
for skip_cards in SKIP_LIST {
|
||||
let src = KlondikePileStack::Tableau(TableauStack {
|
||||
tableau: src_tableau,
|
||||
skip_cards,
|
||||
});
|
||||
let instruction =
|
||||
KlondikeInstruction::DstTableau(DstTableau { tableau, src });
|
||||
if state.is_instruction_valid(instruction) {
|
||||
return Some(instruction);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
KlondikePile::Stock => KlondikePileStack::Stock,
|
||||
KlondikePile::Foundation(foundation) => KlondikePileStack::Foundation(foundation),
|
||||
};
|
||||
KlondikeInstruction::DstTableau(DstTableau { tableau, src })
|
||||
}
|
||||
(KlondikePile::Stock, KlondikePile::Stock) => KlondikeInstruction::RotateStock,
|
||||
(KlondikePile::Foundation(foundation), src) => {
|
||||
KlondikeInstruction::DstFoundation(DstFoundation { foundation, src })
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
state
|
||||
.is_instruction_valid(instruction)
|
||||
.then_some(instruction)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), std::io::Error> {
|
||||
let mut session = Session::new(Klondike::new_random_default());
|
||||
loop {
|
||||
// display game
|
||||
println!("{}", session.state());
|
||||
|
||||
// parse input
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let Ok(instruction) = input.trim().parse() else {
|
||||
println!("Invalid instruction.");
|
||||
continue;
|
||||
};
|
||||
|
||||
// run game
|
||||
match instruction {
|
||||
SessionInstruction::New => session = Session::new(Klondike::new_random_default()),
|
||||
SessionInstruction::Undo => session.undo(),
|
||||
SessionInstruction::Exit => break Ok(()),
|
||||
SessionInstruction::Hint => {
|
||||
for instruction in session.possible_instructions() {
|
||||
println!("{instruction:?}");
|
||||
}
|
||||
}
|
||||
SessionInstruction::Auto => {
|
||||
fn useless_moves(instruction: &KlondikeInstruction) -> bool {
|
||||
!matches!(
|
||||
instruction,
|
||||
// foundation -> foundation is a useless move
|
||||
KlondikeInstruction::DstFoundation(DstFoundation {
|
||||
src: KlondikePile::Foundation(_),
|
||||
..
|
||||
})
|
||||
// Tableau -> Tableau when not revealing a new card is _usually_ a useless move
|
||||
| KlondikeInstruction::DstTableau(DstTableau {
|
||||
src: KlondikePileStack::Tableau(TableauStack {
|
||||
skip_cards: SkipCards::Skip1
|
||||
| SkipCards::Skip2 | SkipCards::Skip3
|
||||
| SkipCards::Skip4 | SkipCards::Skip5
|
||||
| SkipCards::Skip6 | SkipCards::Skip7
|
||||
| SkipCards::Skip8 | SkipCards::Skip9
|
||||
| SkipCards::Skip10 | SkipCards::Skip11
|
||||
| SkipCards::Skip12,
|
||||
..
|
||||
}),
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
if let Some(instruction) =
|
||||
session.possible_instructions().filter(useless_moves).next()
|
||||
{
|
||||
session.process_instruction(instruction);
|
||||
} else {
|
||||
println!("No valid moves!");
|
||||
}
|
||||
}
|
||||
SessionInstruction::Stock => {
|
||||
session.process_instruction(KlondikeInstruction::RotateStock)
|
||||
}
|
||||
SessionInstruction::Klondike(naive_instruction) => {
|
||||
if let Some(instruction) =
|
||||
find_valid_instruction(session.state(), naive_instruction)
|
||||
{
|
||||
session.process_instruction(instruction);
|
||||
} else {
|
||||
println!("Invalid move!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use crate::card_game::{Game, Session};
|
||||
use crate::klondike::Klondike;
|
||||
#[test]
|
||||
fn test_is_winnable() {
|
||||
// is winnable
|
||||
let is_winnable = Session::new(Klondike::new_random_default()).is_winnable();
|
||||
println!("is_winnable = {is_winnable:?}");
|
||||
}
|
||||
#[test]
|
||||
fn test_klondike() {
|
||||
// create game session
|
||||
let game = Klondike::new_random_default();
|
||||
let mut session = Session::new(game);
|
||||
|
||||
// is winnable
|
||||
let is_winnable = session.is_winnable();
|
||||
println!("is_winnable = {is_winnable:?}");
|
||||
|
||||
// play game
|
||||
while let Some(instruction) = session.possible_instructions().next() {
|
||||
session.process_instruction(instruction);
|
||||
}
|
||||
|
||||
// did win
|
||||
let is_win = session.is_win();
|
||||
|
||||
// print session history
|
||||
for (i, instruction) in session.history().iter().enumerate() {
|
||||
println!("move {i} = {instruction:?}");
|
||||
}
|
||||
|
||||
println!("is_win = {is_win}");
|
||||
}
|
||||
Reference in New Issue
Block a user