Added rust solver to the repository
This commit is contained in:
12
solver-rs/lib/board/Cargo.toml
Normal file
12
solver-rs/lib/board/Cargo.toml
Normal 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
4
solver-rs/lib/board/fuzz/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
216
solver-rs/lib/board/fuzz/Cargo.lock
generated
Normal file
216
solver-rs/lib/board/fuzz/Cargo.lock
generated
Normal 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"
|
||||
26
solver-rs/lib/board/fuzz/Cargo.toml
Normal file
26
solver-rs/lib/board/fuzz/Cargo.toml
Normal 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"
|
||||
127
solver-rs/lib/board/fuzz/fuzz_targets/fuzz_target_1.rs
Normal file
127
solver-rs/lib/board/fuzz/fuzz_targets/fuzz_target_1.rs
Normal 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);
|
||||
}
|
||||
});
|
||||
403
solver-rs/lib/board/src/board_class.rs
Normal file
403
solver-rs/lib/board/src/board_class.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
118
solver-rs/lib/board/src/cards.rs
Normal file
118
solver-rs/lib/board/src/cards.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
112
solver-rs/lib/board/src/fieldposition.rs
Normal file
112
solver-rs/lib/board/src/fieldposition.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
54
solver-rs/lib/board/src/lib.rs
Normal file
54
solver-rs/lib/board/src/lib.rs
Normal 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;
|
||||
85
solver-rs/lib/board/src/positions.rs
Normal file
85
solver-rs/lib/board/src/positions.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
solver-rs/lib/board/src/test_boards.rs
Normal file
21
solver-rs/lib/board/src/test_boards.rs
Normal 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)))
|
||||
}
|
||||
};
|
||||
}
|
||||
30
solver-rs/lib/board/src/tests.rs
Normal file
30
solver-rs/lib/board/src/tests.rs
Normal 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(());
|
||||
}
|
||||
Reference in New Issue
Block a user