Added rust solver to the repository

This commit is contained in:
Lukas Wölfer
2025-08-08 19:16:08 +02:00
parent a9ca38e812
commit 5fdf1602eb
91 changed files with 7047 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
[package]
name = "board"
version = "0.1.0"
authors = ["Lukas Wölfer <lukas.woelfer@rwth-aachen.de>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = {version="1.0.105",features=["derive"]}
serde_json = "1.0"
enum-iterator = "0.6.0"

4
solver-rs/lib/board/fuzz/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
target
corpus
artifacts

216
solver-rs/lib/board/fuzz/Cargo.lock generated Normal file
View File

@@ -0,0 +1,216 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "arbitrary"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75153c95fdedd7db9732dfbfc3702324a1627eec91ba56e37cd0ac78314ab2ed"
[[package]]
name = "board"
version = "0.1.0"
dependencies = [
"enum-iterator",
"serde",
"serde_json",
]
[[package]]
name = "board-fuzz"
version = "0.0.0"
dependencies = [
"board",
"enum-iterator",
"libfuzzer-sys",
"rand",
]
[[package]]
name = "cc"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "enum-iterator"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c79a6321a1197d7730510c7e3f6cb80432dfefecb32426de8cea0aa19b4bb8d7"
dependencies = [
"enum-iterator-derive",
]
[[package]]
name = "enum-iterator-derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e94aa31f7c0dc764f57896dc615ddd76fc13b0d5dca7eb6cc5e018a5a09ec06"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "getrandom"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "itoa"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
[[package]]
name = "libc"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0"
[[package]]
name = "libfuzzer-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d718794b8e23533b9069bd2c4597d69e41cc7ab1c02700a502971aca0cdcf24"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "ppv-lite86"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
[[package]]
name = "proc-macro2"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom",
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core",
]
[[package]]
name = "ryu"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76"
[[package]]
name = "serde"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78a7a12c167809363ec3bd7329fc0a3369056996de43c4b37ef3cd54a6ce4867"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"

View File

@@ -0,0 +1,26 @@
[package]
name = "board-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.3"
rand = "*"
enum-iterator = "*"
[dependencies.board]
path = ".."
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[[bin]]
name = "fuzz_target_1"
path = "fuzz_targets/fuzz_target_1.rs"

View File

@@ -0,0 +1,127 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use board::{Board, CardType};
use enum_iterator::IntoEnumIterator;
use rand::seq::SliceRandom;
struct RandomBytes<'a> {
data: &'a [u8],
index: usize,
}
impl<'a> rand::RngCore for RandomBytes<'a> {
fn next_u32(&mut self) -> u32 {
if let Option::Some(x) = self.data.get(self.index..self.index + 4) {
self.index += 4;
return (u32::from(x[3]) << 24)
| (u32::from(x[2]) << 16)
| (u32::from(x[1]) << 8)
| u32::from(x[0]);
} else {
self.index = self.data.len();
return 0;
}
}
fn next_u64(&mut self) -> u64 {
return u64::from(self.next_u32()) << 32 | u64::from(self.next_u32());
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
if (self.index >= self.data.len()) || (dest.len() > self.data.len() - self.index) {
for cell in dest.iter_mut() {
*cell = 0;
}
}
if dest.len() < self.data.len() - self.index {
dest.clone_from_slice(&self.data[self.index..self.index + dest.len()]);
self.index += dest.len()
}
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> {
self.fill_bytes(dest);
return Result::Ok(());
}
}
fn correct_board_permutation(data: &[u8]) -> Board {
if let Option::Some(remove_info) = data.get(0..2) {
let remove_info: u16 = u16::from(remove_info[1]) << 8 | u16::from(remove_info[0]);
let mut result = Board::default();
let mut whole_vec = Vec::<CardType>::new();
if remove_info & 1 == 1 {
result.hua_set = true;
} else {
whole_vec.push(CardType::Hua);
result.hua_set = false;
}
for (index, card) in (1_u8..).zip(board::SpecialCardType::into_enum_iter()) {
if remove_info & (1 << index) == 0 {
result.bunker[usize::from(index - 1)] =
board::BunkerSlot::Blocked(Option::Some(card.clone()));
} else {
whole_vec.push(CardType::Special(card.clone()));
whole_vec.push(CardType::Special(card.clone()));
whole_vec.push(CardType::Special(card.clone()));
whole_vec.push(CardType::Special(card.clone()));
result.bunker[usize::from(index - 1)] = board::BunkerSlot::Empty;
}
}
for (index, suit) in (4_u8..)
.step_by(4)
.zip(board::NumberCardColor::into_enum_iter())
{
let value = (((remove_info >> index) & 0b1111) % 10) as u8;
let slot_index = usize::from((index - 4) / 4);
if value == 0 {
result.goal[slot_index] = Option::None;
} else {
result.goal[slot_index] = Option::Some(board::NumberCard {
value,
suit: suit.clone(),
});
}
for value in (value + 1)..10 {
whole_vec.push(board::CardType::Number(board::NumberCard {
value,
suit: suit.clone(),
}));
}
}
whole_vec.shuffle(&mut RandomBytes { data, index: 2 });
for ((index_start, index_end), slot) in (0..)
.step_by(8)
.zip((8..).step_by(8))
.zip(result.field.iter_mut())
{
if let Option::Some(tasty_slice) = whole_vec.get(index_start..index_end) {
slot.extend_from_slice(tasty_slice);
} else if let Option::Some(tasty_slice) = whole_vec.get(index_start..) {
slot.extend_from_slice(tasty_slice);
break;
} else {
break;
}
}
return result;
} else {
return Board::default();
}
}
fuzz_target!(|data: &[u8]| {
if data.len() == 0 {
return;
}
let x = correct_board_permutation(&data[1..]);
assert_eq!(x.check(), Result::Ok(()));
if let Option::Some(action) = board::possibilities::all_actions(&x).choose(&mut RandomBytes {
data: &data[0..1],
index: 0,
}) {
let mut action_board = x.clone();
action.apply(&mut action_board);
assert_ne!(action_board, x);
action.undo(&mut action_board);
assert_eq!(action_board, x);
}
});

View File

@@ -0,0 +1,403 @@
use crate::{
BunkerSlot, CardType, CardTypeNoHua, FieldPosition, NumberCard, NumberCardColor,
PositionNoGoal, SpecialCardType,
};
use enum_iterator::IntoEnumIterator;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Error {
CardMissing(CardType),
CardDouble(CardType),
GoalTooHigh(NumberCard),
ErraneousCard(NumberCard),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
pub struct Board {
pub field: [Vec<CardType>; 8],
pub goal: [Option<NumberCard>; 3],
pub hua_set: bool,
pub bunker: [BunkerSlot; 3],
}
impl Default for Board {
fn default() -> Self {
return Self {
field: [
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
],
goal: [
Option::Some(NumberCard {
value: 9,
suit: NumberCardColor::Black,
}),
Option::Some(NumberCard {
value: 9,
suit: NumberCardColor::Red,
}),
Option::Some(NumberCard {
value: 9,
suit: NumberCardColor::Green,
}),
],
hua_set: false,
bunker: [
BunkerSlot::Blocked(Option::Some(SpecialCardType::Bai)),
BunkerSlot::Blocked(Option::Some(SpecialCardType::Zhong)),
BunkerSlot::Blocked(Option::Some(SpecialCardType::Fa)),
],
};
}
}
pub struct BoardEqHash([u8; 32]);
impl PartialEq for BoardEqHash {
fn eq(&self, other: &Self) -> bool {
return self
.0
.iter()
.zip(other.0.iter())
.all(|(this_cell, other_cell)| return this_cell == other_cell);
}
}
impl Eq for BoardEqHash {}
impl std::hash::Hash for BoardEqHash {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write(&self.0);
}
}
impl std::str::FromStr for Board {
type Err = serde_json::error::Error;
fn from_str(json_string: &str) -> Result<Self, Self::Err> {
//! # Errors
//! Will return `io::Result::Err` when the path cannot be found,
//! and `Result::Err` when the json in the file is incorrect
return serde_json::from_str::<Self>(json_string);
}
}
struct BitSquasher<'a> {
sink: &'a mut [u8],
byte: usize,
bit: u8,
}
impl<'a> BitSquasher<'a> {
pub fn new(sink: &'a mut [u8]) -> Self {
return BitSquasher {
sink,
byte: 0,
bit: 0,
};
}
pub fn squash(&mut self, input: u8, count: u8) {
debug_assert!(count <= 8);
debug_assert!(count > 0);
self.sink[self.byte] |= input << self.bit;
if (8 - self.bit) < count {
self.sink[self.byte + 1] |= input >> (8 - self.bit);
}
self.bit += count;
self.byte += usize::from(self.bit / 8);
self.bit %= 8;
}
}
#[test]
fn bit_squasher_test() {
let mut buffer: [u8; 4] = Default::default();
let mut squasher = BitSquasher::new(&mut buffer);
squasher.squash(0b101, 3);
squasher.squash(0b1111000, 7);
squasher.squash(0b11001100, 8);
squasher.squash(0b101010, 6);
assert_eq!(buffer, [0b11000101, 0b00110011, 0b10101011, 0]);
}
impl Board {
pub fn from_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
//! # Errors
//! Will return `io::Result::Err` when the path cannot be found,
//! and `Result::Err` when the json in the file is incorrect
let f = File::open(path)?;
let reader = BufReader::new(f);
let x: Self = serde_json::from_reader(reader)?;
return Result::Ok(x);
}
#[must_use]
pub fn goal_value(&self, suit: &NumberCardColor) -> u8 {
return self
.goal
.iter()
.filter_map(|card| return card.clone())
.find_map(|card| {
if &card.suit == suit {
return Option::Some(card.value);
} else {
return Option::None;
}
})
.unwrap_or(0);
}
#[must_use]
pub fn equivalence_hash(&self) -> BoardEqHash {
// up to 40 cards on the field
// 8 empty card represents end of slot
// 3 bunker
// If hua in field -> hua not set, does not need representation
// We can skip goal, as the value of the card in the goal
// is the highest value missing from the board;
let mut result = [0_u8; 32];
let mut squasher = BitSquasher::new(&mut result);
let mut field_lengths: [usize; 8] = Default::default();
for (index, cell) in field_lengths.iter_mut().enumerate() {
*cell = index;
}
field_lengths.sort_unstable_by(|left_index, right_index| {
return self.field[*left_index].cmp(&self.field[*right_index]);
});
let sorted_iter = field_lengths.iter().map(|index| return &self.field[*index]);
for slot in sorted_iter {
let slot_size = slot.len();
debug_assert!(slot.len() < 16);
squasher.squash(slot_size as u8, 4);
for cell in slot {
let cell_byte = cell.to_byte();
debug_assert!(cell_byte < 32);
squasher.squash(cell_byte, 5);
}
}
let mut sorted_bunker = self.bunker.clone();
sorted_bunker.sort_unstable();
for slot in sorted_bunker.iter() {
let bunker_byte = match slot {
BunkerSlot::Empty => 0,
BunkerSlot::Stash(card) => card.add_hua().to_byte(),
BunkerSlot::Blocked(Option::Some(card)) => {
CardType::Special(card.clone()).to_byte() | (1 << 5)
}
BunkerSlot::Blocked(Option::None) => (1 << 5),
};
debug_assert!(bunker_byte < 64);
squasher.squash(bunker_byte, 6);
}
return BoardEqHash(result);
}
pub fn movable_cards<'a>(&'a self) -> impl Iterator<Item = (PositionNoGoal, CardType)> + 'a {
let bunker_iterator = (0_u8..)
.zip(self.bunker.iter())
.filter_map(|(index, card)| {
let pos = PositionNoGoal::Bunker { slot_index: index };
let ret_card = match card {
BunkerSlot::Stash(CardTypeNoHua::Special(card)) => {
Option::Some(CardType::Special(card.clone()))
}
BunkerSlot::Stash(CardTypeNoHua::Number(card)) => {
Option::Some(CardType::Number(card.clone()))
}
_ => Option::None,
};
return ret_card.map(|card| return (pos, card));
});
let field_iterator = (0_u8..)
.zip(self.field.iter())
.filter_map(|(column_index, row)| {
return row.last().map(|ret_card| {
let pos = PositionNoGoal::Field(FieldPosition::new(
column_index,
(row.len() - 1) as u8,
));
return (pos, ret_card.clone());
});
});
let result = bunker_iterator.chain(field_iterator);
return result;
}
fn handle_number_card(
card: &NumberCard,
number_card_map: &mut HashMap<NumberCardColor, [bool; 9]>,
) -> Result<(), Error> {
if card.value > 9 || card.value < 1 {
return Result::Err(Error::ErraneousCard(card.clone()));
}
if *number_card_map
.get_mut(&card.suit)
.unwrap()
.get(usize::from(card.value - 1))
.unwrap()
{
return Result::Err(Error::CardDouble(CardType::Number(card.clone())));
}
*number_card_map
.get_mut(&card.suit)
.unwrap()
.get_mut(usize::from(card.value - 1))
.unwrap() = true;
return Result::Ok(());
}
fn handle_special_card(
card: &SpecialCardType,
special_card_map: &mut HashMap<SpecialCardType, i8>,
) -> Result<(), Error> {
let card_slot = special_card_map.entry(card.clone()).or_insert(0);
if *card_slot > 4 {
return Result::Err(Error::CardDouble(CardType::Special(card.clone())));
}
*card_slot += 1;
return Result::Ok(());
}
pub fn check(&self) -> Result<(), Error> {
//! # Errors
//!
//! Returns the error in the board
let mut special_card_map: HashMap<SpecialCardType, i8> = HashMap::new();
let mut number_card_map: HashMap<NumberCardColor, [bool; 9]> = HashMap::new();
let mut unknown_blocked_count: u8 = 0;
for color in NumberCardColor::into_enum_iter() {
number_card_map.insert(color.clone(), [false; 9]);
}
for special_card_type in SpecialCardType::into_enum_iter() {
special_card_map.insert(special_card_type.clone(), 0);
}
let mut hua_exists: bool = self.hua_set;
for field_row in &self.field {
for cell in field_row.iter() {
match cell {
CardType::Number(number_card) => {
Self::handle_number_card(number_card, &mut number_card_map)?;
}
CardType::Special(card_type) => {
Self::handle_special_card(card_type, &mut special_card_map)?;
}
CardType::Hua => {
if hua_exists {
return Result::Err(Error::CardDouble(CardType::Hua));
} else {
hua_exists = true
}
}
}
}
}
for bunker_cell in &self.bunker {
match bunker_cell {
BunkerSlot::Blocked(Option::None) => unknown_blocked_count += 1,
BunkerSlot::Blocked(Option::Some(special_card_type)) => {
for _ in 0..4 {
Self::handle_special_card(special_card_type, &mut special_card_map)?;
}
}
BunkerSlot::Stash(CardTypeNoHua::Special(special_card_type)) => {
Self::handle_special_card(special_card_type, &mut special_card_map)?;
}
BunkerSlot::Stash(CardTypeNoHua::Number(number_card)) => {
Self::handle_number_card(number_card, &mut number_card_map)?;
}
BunkerSlot::Empty => {}
}
}
for goal_cell in &self.goal {
if let Some(NumberCard { suit, value }) = goal_cell {
let color_slice = number_card_map.get_mut(suit).unwrap();
for i in 0..*value {
if *color_slice.get(usize::from(i)).unwrap() {
return Result::Err(Error::GoalTooHigh(NumberCard {
suit: suit.clone(),
value: *value,
}));
}
*color_slice.get_mut(usize::from(i)).unwrap() = true;
}
}
}
for (card_type, count) in &special_card_map {
if *count != 4 {
if unknown_blocked_count == 0 {
return Result::Err(Error::CardMissing(CardType::Special(card_type.clone())));
}
unknown_blocked_count -= 1;
}
}
for (card_type, value_array) in &number_card_map {
for (index, value_hit) in (0_u8..).zip(value_array.iter()) {
if !*value_hit {
return Result::Err(Error::CardMissing(CardType::Number(NumberCard {
suit: card_type.clone(),
value: (index + 1),
})));
}
}
}
return Result::Ok(());
}
#[must_use]
pub fn solved(&self) -> bool {
for row in &self.field {
if !row.is_empty() {
return false;
}
}
for slot in &self.bunker {
if let BunkerSlot::Blocked(_) = slot {
} else {
return false;
}
}
if !self.hua_set {
return false;
}
for goal_slot in &self.goal {
if goal_slot.is_none() {
return false;
}
}
for color in NumberCardColor::into_enum_iter() {
let color_position = self.goal.iter().position(|goal_card| {
return goal_card
.as_ref()
.expect("We already checked that every goal slot is not None")
.suit
== color;
});
if color_position.is_none() {
return false;
}
}
for card in &self.goal {
if card
.as_ref()
.expect("We already checked that every goal slot is not None")
.value
!= 9
{
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,118 @@
use enum_iterator::IntoEnumIterator;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[derive(
Serialize, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, IntoEnumIterator,
)]
pub enum SpecialCardType {
Zhong,
Bai,
Fa,
}
impl Display for SpecialCardType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(f, "{:#?}", self);
}
}
#[derive(
Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, IntoEnumIterator,
)]
pub enum NumberCardColor {
Red,
Green,
Black,
}
impl Display for NumberCardColor {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(f, "{:#?}", self);
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct NumberCard {
pub value: u8,
pub suit: NumberCardColor,
}
impl Display for NumberCard {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(f, "{} {}", self.suit, self.value);
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub enum CardType {
Hua,
Number(NumberCard),
Special(SpecialCardType),
}
impl CardType {
#[must_use]
pub fn remove_hua(&self) -> CardTypeNoHua {
match self {
Self::Number(x) => return CardTypeNoHua::Number(x.clone()),
Self::Special(x) => return CardTypeNoHua::Special(x.clone()),
Self::Hua => panic!("Remove hua on hua"),
}
}
/// Returns a number from (1..=31)
#[must_use]
pub fn to_byte(&self) -> u8 {
match self {
Self::Number(numbercard) => {
let result = numbercard.value
+ 9 * (NumberCardColor::into_enum_iter()
.position(|suit| return numbercard.suit == suit)
.unwrap() as u8);
debug_assert!(result >= 1 && result <= 27);
return result;
}
Self::Special(specialcard) => {
let result = 28
+ (SpecialCardType::into_enum_iter()
.position(|x| return x == *specialcard)
.unwrap() as u8);
debug_assert!(result >= 28 && result <= 30);
return result;
}
Self::Hua => return 31,
}
}
}
impl Display for CardType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Hua => return write!(f, "Hua"),
Self::Number(x) => return write!(f, "{}", x),
Self::Special(x) => return write!(f, "{}", x),
}
}
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)]
pub enum CardTypeNoHua {
Number(NumberCard),
Special(SpecialCardType),
}
impl Display for CardTypeNoHua {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Number(x) => return write!(f, "{}", x),
Self::Special(x) => return write!(f, "{}", x),
}
}
}
impl CardTypeNoHua {
#[must_use] pub fn add_hua(&self) -> CardType {
match self {
Self::Number(x) => return CardType::Number(x.clone()),
Self::Special(x) => return CardType::Special(x.clone()),
}
}
}

View File

@@ -0,0 +1,112 @@
use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor};
use std::fmt;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct FieldPosition {
buffer: u8,
}
impl FieldPosition {
#[must_use]
pub fn column(&self) -> u8 {
return self.buffer & 0b1111;
}
#[must_use]
pub fn row(&self) -> u8 {
return (self.buffer & (0b1111 << 4)) >> 4;
}
#[must_use]
pub fn new(column: u8, row: u8) -> Self {
debug_assert!(column < 8);
// Should be 13, allowing some buffer for end-markers because we've got the space
debug_assert!(row < 16);
return Self {
buffer: (column & 0b1111) | ((row & 0b1111) << 4),
};
}
}
impl std::fmt::Display for FieldPosition {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(f, "slot #{} index #{}", self.column(), self.row());
}
}
impl serde::Serialize for FieldPosition {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("FieldPosition", 2)?;
state.serialize_field("column", &self.column())?;
state.serialize_field("row", &self.row())?;
return state.end();
}
}
impl<'de> serde::Deserialize<'de> for FieldPosition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Column,
Row,
};
struct FieldPositionVisitor;
impl<'de> Visitor<'de> for FieldPositionVisitor {
type Value = FieldPosition;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
return formatter.write_str("struct FieldPosition")
}
fn visit_seq<V>(self, mut seq: V) -> Result<FieldPosition, V::Error>
where
V: SeqAccess<'de>,
{
let column = seq
.next_element()?
.ok_or_else(|| return de::Error::invalid_length(0, &self))?;
let row = seq
.next_element()?
.ok_or_else(|| return de::Error::invalid_length(1, &self))?;
return Ok(FieldPosition::new(column, row));
}
fn visit_map<V>(self, mut map: V) -> Result<FieldPosition, V::Error>
where
V: MapAccess<'de>,
{
let mut column = None;
let mut row = None;
while let Some(key) = map.next_key()? {
match key {
Field::Column => {
if column.is_some() {
return Err(de::Error::duplicate_field("column"));
}
column = Some(map.next_value()?);
}
Field::Row => {
if row.is_some() {
return Err(de::Error::duplicate_field("row"));
}
row = Some(map.next_value()?);
}
}
}
let column = column.ok_or_else(|| return de::Error::missing_field("column"))?;
let row = row.ok_or_else(|| return de::Error::missing_field("row"))?;
return Ok(FieldPosition::new(column, row));
}
}
const FIELDS: &[&str] = &["column", "row"];
return deserializer.deserialize_struct("Duration", FIELDS, FieldPositionVisitor)
}
}

View File

@@ -0,0 +1,54 @@
#![warn(
clippy::all,
clippy::restriction,
clippy::pedantic,
clippy::nursery,
clippy::cargo
)]
#![allow(clippy::cargo)]
// Style choices
#![allow(
clippy::missing_docs_in_private_items,
clippy::needless_return,
clippy::get_unwrap,
clippy::indexing_slicing,
clippy::explicit_iter_loop
)]
// Way too pedantic
#![allow(clippy::integer_arithmetic)]
#![allow(clippy::integer_division)]
// Useless
#![allow(clippy::missing_inline_in_public_items, clippy::missing_const_for_fn)]
// Useful for production
#![allow(
clippy::use_debug,
clippy::print_stdout,
clippy::dbg_macro,
clippy::panic
)]
// Useful for improving code robustness
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::option_unwrap_used,
clippy::option_expect_used,
clippy::as_conversions,
clippy::result_unwrap_used,
clippy::result_expect_used,
// clippy::wildcard_enum_match_arm
)]
#![allow(clippy::trivially_copy_pass_by_ref)]
#![allow(dead_code)]
mod cards;
pub use cards::*;
mod fieldposition;
pub use fieldposition::*;
mod positions;
pub use positions::*;
mod board_class;
pub use crate::board_class::*;
pub mod test_boards;
#[cfg(test)]
mod tests;

View File

@@ -0,0 +1,85 @@
use crate::cards::{CardTypeNoHua, SpecialCardType};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, Clone)]
pub enum BunkerSlot {
Empty,
Blocked(Option<SpecialCardType>),
Stash(CardTypeNoHua),
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
pub enum Position {
Field(crate::FieldPosition),
Bunker { slot_index: u8 },
Goal { slot_index: u8 },
}
impl std::fmt::Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Field(x) => return write!(f, "Field ({})", x),
Self::Bunker { slot_index } => return write!(f, "Bunker #{}", slot_index),
Self::Goal { slot_index } => return write!(f, "Goal #{}", slot_index),
}
}
}
impl From<PositionNoGoal> for Position {
fn from(input: PositionNoGoal) -> Self {
match input {
PositionNoGoal::Field(x) => return Self::Field(x),
PositionNoGoal::Bunker { slot_index } => return Self::Bunker { slot_index },
};
}
}
impl PartialEq<PositionNoGoal> for Position {
fn eq(&self, other: &PositionNoGoal) -> bool {
return other.eq(self);
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Hash)]
pub enum PositionNoGoal {
Field(crate::FieldPosition),
Bunker { slot_index: u8 },
}
impl std::fmt::Display for PositionNoGoal {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(f, "{}", Position::from(*self));
}
}
#[derive(Debug, Clone)]
pub struct GoalTransformError;
impl std::convert::TryFrom<Position> for PositionNoGoal {
type Error = GoalTransformError;
fn try_from(input: Position) -> Result<Self, Self::Error> {
match input {
Position::Field(field_position) => return Result::Ok(Self::Field(field_position)),
Position::Bunker { slot_index } => {
return Result::Ok(Self::Bunker { slot_index });
}
Position::Goal { .. } => {
return Result::Err(GoalTransformError);
}
}
}
}
impl PartialEq<Position> for PositionNoGoal {
fn eq(&self, other: &Position) -> bool {
let other = <Self as std::convert::TryFrom<Position>>::try_from(other.clone());
match other {
Ok(other) => {
return Self::eq(self, &other);
}
Err(GoalTransformError) => {
return false;
}
}
}
}

View File

@@ -0,0 +1,21 @@
// This is incredibly shit, as other crates call this macro with _their_ CARGO_MANIFEST_DIR. Ideally we would move
// the boards into the board crate, and use the path of the board crate. But it seems to be really hard to get this done with
// macros, and const variables can't be used by macros, so we're using this hack for now.
#[macro_export]
macro_rules! TEST_BOARD_ROOT {
() => {
concat!(env!("CARGO_MANIFEST_DIR"),
"/../../aux/boards/")
}
}
#[macro_export]
macro_rules! load_test_board {
( $relpath:expr ) => {
{
<$crate::Board as std::str::FromStr>::from_str(
include_str!(concat!($crate::TEST_BOARD_ROOT!(),
$relpath)))
}
};
}

View File

@@ -0,0 +1,30 @@
use crate::{CardType, Error, NumberCard, NumberCardColor};
#[test]
pub fn check_test() -> Result<(), Box<dyn std::error::Error>> {
// let mut x = Board::from_file(std::path::Path::new(crate::test_boards::SPECIFIC)?;
let mut x = crate::load_test_board!("specific/solved.json")?;
assert_eq!(x.check(), Result::Ok(()));
assert_eq!(x.solved(), true);
x.field[2].push(CardType::Hua);
assert_eq!(x.check(), Result::Err(Error::CardDouble(CardType::Hua)));
x.hua_set = false;
assert_eq!(x.check(), Result::Ok(()));
x.field[2].push(CardType::Number(NumberCard {
suit: NumberCardColor::Black,
value: 9,
}));
assert_eq!(
x.check(),
Result::Err(Error::GoalTooHigh(NumberCard {
value: 9,
suit: NumberCardColor::Black
}))
);
x.goal[0] = Some(NumberCard {
suit: NumberCardColor::Black,
value: 8,
});
assert_eq!(x.check(), Result::Ok(()));
assert_eq!(x.solved(), false);
return Result::Ok(());
}