Added rust solver to the repository
This commit is contained in:
14
solver-rs/lib/actions/Cargo.toml
Normal file
14
solver-rs/lib/actions/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "actions"
|
||||
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"
|
||||
|
||||
board = {path = "../board"}
|
||||
405
solver-rs/lib/actions/src/base.rs
Normal file
405
solver-rs/lib/actions/src/base.rs
Normal file
@@ -0,0 +1,405 @@
|
||||
use board::{
|
||||
Board, BunkerSlot, CardType, CardTypeNoHua, FieldPosition, NumberCard, PositionNoGoal,
|
||||
SpecialCardType,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub(super) trait BoardApplication {
|
||||
fn apply(&self, solboard: &mut Board);
|
||||
fn undo(&self, solboard: &mut Board);
|
||||
fn can_apply(&self, solboard: &Board) -> bool;
|
||||
fn can_undo(&self, solboard: &Board) -> bool;
|
||||
fn checked_apply(&self, solboard: &mut Board) -> bool {
|
||||
if self.can_apply(solboard) {
|
||||
self.apply(solboard);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
fn checked_undo(&self, solboard: &mut Board) -> bool {
|
||||
if self.can_undo(solboard) {
|
||||
self.undo(solboard);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fn can_pop_top(solboard: &Board, position: &PositionNoGoal, card: &CardType) -> bool {
|
||||
match position {
|
||||
PositionNoGoal::Field(fieldpos) => {
|
||||
if solboard.field[usize::from(fieldpos.column())]
|
||||
.last()
|
||||
.expect("Trying to pop top of empty field stack")
|
||||
!= card
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
PositionNoGoal::Bunker { slot_index } => {
|
||||
if solboard.bunker[usize::from(*slot_index)] != BunkerSlot::Stash(card.remove_hua()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
fn pop_top(solboard: &mut Board, position: &PositionNoGoal, card: &CardType) {
|
||||
debug_assert!(can_pop_top(solboard, position, card));
|
||||
match position {
|
||||
PositionNoGoal::Field(fieldpos) => {
|
||||
solboard
|
||||
.field
|
||||
.get_mut(usize::from(fieldpos.column()))
|
||||
.expect("Column index fucked")
|
||||
.pop();
|
||||
}
|
||||
PositionNoGoal::Bunker { slot_index } => {
|
||||
solboard.bunker[usize::from(*slot_index)] = BunkerSlot::Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct Goal {
|
||||
pub card: NumberCard,
|
||||
pub source: PositionNoGoal,
|
||||
pub goal_slot_index: u8,
|
||||
}
|
||||
|
||||
impl BoardApplication for Goal {
|
||||
fn can_apply(&self, solboard: &Board) -> bool {
|
||||
match &solboard.goal[usize::from(self.goal_slot_index)] {
|
||||
Option::Some(NumberCard { value, suit }) => {
|
||||
if self.card.value != *value + 1 {
|
||||
return false;
|
||||
}
|
||||
if self.card.suit != *suit {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Option::None => {
|
||||
if self.card.value != 1 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !can_pop_top(solboard, &self.source, &CardType::Number(self.card.clone())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
fn can_undo(&self, _solboard: &Board) -> bool {
|
||||
return true;
|
||||
}
|
||||
fn apply(&self, solboard: &mut Board) {
|
||||
pop_top(solboard, &self.source, &CardType::Number(self.card.clone()));
|
||||
*solboard
|
||||
.goal
|
||||
.get_mut(usize::from(self.goal_slot_index))
|
||||
.expect("Slot index fucked") = Option::Some(self.card.clone());
|
||||
}
|
||||
fn undo(&self, solboard: &mut Board) {
|
||||
match &self.source {
|
||||
PositionNoGoal::Field(position) => {
|
||||
solboard
|
||||
.field
|
||||
.get_mut(usize::from(position.column()))
|
||||
.expect("Column index fucked")
|
||||
.push(CardType::Number(self.card.clone()));
|
||||
}
|
||||
PositionNoGoal::Bunker { slot_index } => {
|
||||
solboard.bunker[usize::from(*slot_index)] =
|
||||
BunkerSlot::Stash(CardTypeNoHua::Number(self.card.clone()));
|
||||
}
|
||||
}
|
||||
if self.card.value == 1 {
|
||||
solboard.goal[usize::from(self.goal_slot_index)] = Option::None;
|
||||
} else {
|
||||
solboard.goal[usize::from(self.goal_slot_index)] = Option::Some(NumberCard {
|
||||
suit: self.card.suit.clone(),
|
||||
value: self.card.value - 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Goal {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
return write!(
|
||||
f,
|
||||
"Goal {} from {} to slot #{}",
|
||||
self.card, self.source, self.goal_slot_index
|
||||
);
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct DragonKill {
|
||||
pub card: SpecialCardType,
|
||||
pub source: [PositionNoGoal; 4],
|
||||
pub destination_slot_index: u8,
|
||||
}
|
||||
|
||||
impl BoardApplication for DragonKill {
|
||||
fn apply(&self, solboard: &mut Board) {
|
||||
for position in &self.source {
|
||||
pop_top(solboard, position, &CardType::Special(self.card.clone()));
|
||||
}
|
||||
solboard.bunker[usize::from(self.destination_slot_index)] =
|
||||
BunkerSlot::Blocked(Option::Some(self.card.clone()));
|
||||
}
|
||||
fn undo(&self, solboard: &mut Board) {
|
||||
solboard.bunker[usize::from(self.destination_slot_index)] = BunkerSlot::Empty;
|
||||
for position in &self.source {
|
||||
match &position {
|
||||
PositionNoGoal::Field(field_position) => {
|
||||
solboard.field[usize::from(field_position.column())]
|
||||
.push(CardType::Special(self.card.clone()));
|
||||
}
|
||||
PositionNoGoal::Bunker { slot_index } => {
|
||||
solboard.bunker[usize::from(*slot_index)] =
|
||||
BunkerSlot::Stash(CardTypeNoHua::Special(self.card.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn can_apply(&self, solboard: &Board) -> bool {
|
||||
if self.destination_slot_index >= 3 {
|
||||
return false;
|
||||
}
|
||||
let previous_slot_empty = solboard
|
||||
.bunker
|
||||
.iter()
|
||||
.take(self.destination_slot_index.saturating_sub(1).into())
|
||||
.all(|x| {
|
||||
if let BunkerSlot::Empty = x {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if previous_slot_empty {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
fn can_undo(&self, _solboard: &Board) -> bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DragonKill {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
return write!(
|
||||
f,
|
||||
"Kill {} to bunker #{} from {}, {}, {}, {}",
|
||||
self.card,
|
||||
self.destination_slot_index,
|
||||
self.source[0],
|
||||
self.source[1],
|
||||
self.source[2],
|
||||
self.source[3],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct Bunkerize {
|
||||
pub card: CardTypeNoHua,
|
||||
pub bunker_slot_index: u8,
|
||||
pub field_position: FieldPosition,
|
||||
pub to_bunker: bool,
|
||||
}
|
||||
impl Bunkerize {
|
||||
fn can_move_to_bunker(&self, solboard: &Board) -> bool {
|
||||
if self.field_position.row() + 1
|
||||
!= solboard.field[usize::from(self.field_position.column())].len() as u8
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if self.card.add_hua()
|
||||
!= *solboard.field[usize::from(self.field_position.column())]
|
||||
.last()
|
||||
.unwrap()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if solboard.bunker[usize::from(self.bunker_slot_index)] != BunkerSlot::Empty {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
fn move_to_bunker(&self, solboard: &mut Board) {
|
||||
debug_assert!(self.can_move_to_bunker(solboard));
|
||||
solboard.field[usize::from(self.field_position.column())].pop();
|
||||
solboard.bunker[usize::from(self.bunker_slot_index)] = BunkerSlot::Stash(self.card.clone());
|
||||
}
|
||||
fn can_move_from_bunker(&self, solboard: &Board) -> bool {
|
||||
if solboard.bunker[usize::from(self.bunker_slot_index)]
|
||||
!= BunkerSlot::Stash(self.card.clone())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if self.field_position.row()
|
||||
!= solboard.field[usize::from(self.field_position.column())].len() as u8
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
fn move_from_bunker(&self, solboard: &mut Board) {
|
||||
debug_assert!(self.can_move_from_bunker(solboard));
|
||||
solboard.field[usize::from(self.field_position.column())].push(self.card.add_hua());
|
||||
solboard.bunker[usize::from(self.bunker_slot_index)] = BunkerSlot::Empty;
|
||||
}
|
||||
}
|
||||
|
||||
impl BoardApplication for Bunkerize {
|
||||
fn apply(&self, solboard: &mut Board) {
|
||||
if self.to_bunker {
|
||||
self.move_to_bunker(solboard);
|
||||
} else {
|
||||
self.move_from_bunker(solboard);
|
||||
}
|
||||
}
|
||||
fn undo(&self, solboard: &mut Board) {
|
||||
if self.to_bunker {
|
||||
self.move_from_bunker(solboard);
|
||||
} else {
|
||||
self.move_to_bunker(solboard);
|
||||
}
|
||||
}
|
||||
fn can_apply(&self, solboard: &Board) -> bool {
|
||||
if self.to_bunker {
|
||||
return self.can_move_to_bunker(solboard);
|
||||
} else {
|
||||
return self.can_move_from_bunker(solboard);
|
||||
}
|
||||
}
|
||||
fn can_undo(&self, solboard: &Board) -> bool {
|
||||
if self.to_bunker {
|
||||
return self.can_move_from_bunker(solboard);
|
||||
} else {
|
||||
return self.can_move_to_bunker(solboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Bunkerize {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
if self.to_bunker {
|
||||
return write!(
|
||||
f,
|
||||
"Move {} from {} to bunker #{}",
|
||||
self.card, self.field_position, self.bunker_slot_index,
|
||||
);
|
||||
} else {
|
||||
return write!(
|
||||
f,
|
||||
"Move {} from bunker #{} to {}",
|
||||
self.card, self.bunker_slot_index, self.field_position,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct HuaKill {
|
||||
pub field_position: FieldPosition,
|
||||
}
|
||||
|
||||
impl BoardApplication for HuaKill {
|
||||
fn can_apply(&self, solboard: &Board) -> bool {
|
||||
if solboard.field[usize::from(self.field_position.column())].last()
|
||||
!= Option::Some(&CardType::Hua)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if solboard.field[usize::from(self.field_position.column())].len()
|
||||
!= (self.field_position.row() + 1) as usize
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
fn apply(&self, solboard: &mut Board) {
|
||||
debug_assert!(self.can_apply(solboard));
|
||||
solboard.field[usize::from(self.field_position.column())].pop();
|
||||
solboard.hua_set = true;
|
||||
}
|
||||
fn can_undo(&self, solboard: &Board) -> bool {
|
||||
if solboard.field[usize::from(self.field_position.column())].len()
|
||||
!= self.field_position.row() as usize
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
fn undo(&self, solboard: &mut Board) {
|
||||
debug_assert!(self.can_undo(solboard));
|
||||
solboard.field[usize::from(self.field_position.column())].push(CardType::Hua);
|
||||
solboard.hua_set = false;
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for HuaKill {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
return write!(f, "Kill hua from {}", self.field_position);
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub enum All {
|
||||
Bunkerize(Bunkerize),
|
||||
DragonKill(DragonKill),
|
||||
Goal(Goal),
|
||||
HuaKill(HuaKill),
|
||||
Move(super::Move),
|
||||
}
|
||||
impl std::fmt::Display for All {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Bunkerize(x) => return write!(f, "{}", x),
|
||||
Self::DragonKill(x) => return write!(f, "{}", x),
|
||||
Self::Goal(x) => return write!(f, "{}", x),
|
||||
Self::HuaKill(x) => return write!(f, "{}", x),
|
||||
Self::Move(x) => return write!(f, "{}", x),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl All {
|
||||
pub fn apply(&self, solboard: &mut Board) {
|
||||
match self {
|
||||
Self::HuaKill(obj) => {
|
||||
obj.apply(solboard);
|
||||
}
|
||||
Self::DragonKill(obj) => {
|
||||
obj.apply(solboard);
|
||||
}
|
||||
Self::Goal(obj) => {
|
||||
obj.apply(solboard);
|
||||
}
|
||||
Self::Bunkerize(obj) => {
|
||||
obj.apply(solboard);
|
||||
}
|
||||
Self::Move(obj) => obj.apply(solboard),
|
||||
}
|
||||
}
|
||||
pub fn undo(&self, solboard: &mut Board) {
|
||||
match self {
|
||||
Self::HuaKill(obj) => {
|
||||
obj.undo(solboard);
|
||||
}
|
||||
Self::DragonKill(obj) => {
|
||||
obj.undo(solboard);
|
||||
}
|
||||
Self::Goal(obj) => {
|
||||
obj.undo(solboard);
|
||||
}
|
||||
Self::Bunkerize(obj) => {
|
||||
obj.undo(solboard);
|
||||
}
|
||||
Self::Move(obj) => obj.undo(solboard),
|
||||
}
|
||||
}
|
||||
}
|
||||
49
solver-rs/lib/actions/src/lib.rs
Normal file
49
solver-rs/lib/actions/src/lib.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
#![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)]
|
||||
// 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::wildcard_enum_match_arm
|
||||
)]
|
||||
#![allow(clippy::trivially_copy_pass_by_ref)]
|
||||
#![allow(dead_code)]
|
||||
mod base;
|
||||
pub use base::*;
|
||||
|
||||
mod move_action;
|
||||
pub use move_action::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub mod possibilities;
|
||||
178
solver-rs/lib/actions/src/move_action.rs
Normal file
178
solver-rs/lib/actions/src/move_action.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use board::{Board, CardType, FieldPosition, NumberCard, NumberCardColor};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const COLOR_SEQUENCE: [NumberCardColor; 3] = [
|
||||
NumberCardColor::Red,
|
||||
NumberCardColor::Green,
|
||||
NumberCardColor::Black,
|
||||
];
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct Move {
|
||||
start_card: CardType,
|
||||
stack_len: u8,
|
||||
pattern: u8,
|
||||
pub source: FieldPosition,
|
||||
pub destination: FieldPosition,
|
||||
}
|
||||
|
||||
impl Move {
|
||||
#[must_use]
|
||||
fn alternate_card(bottom_suit: &NumberCardColor, bit: u8) -> NumberCardColor {
|
||||
let pos = COLOR_SEQUENCE
|
||||
.iter()
|
||||
.position(|x| return x == bottom_suit)
|
||||
.unwrap();
|
||||
let shift_value = if bit == 0 { 0 } else { 1 };
|
||||
return COLOR_SEQUENCE[(pos + shift_value + 1) % 3].clone();
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn bit_card(last_card: &board::NumberCardColor, current_card: &board::NumberCardColor) -> u8 {
|
||||
let last_pos = COLOR_SEQUENCE
|
||||
.iter()
|
||||
.position(|x| return x == last_card)
|
||||
.unwrap();
|
||||
let current_pos = COLOR_SEQUENCE
|
||||
.iter()
|
||||
.position(|x| return x == current_card)
|
||||
.unwrap();
|
||||
if (last_pos + 1) % 3 == current_pos {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
#[must_use]
|
||||
pub fn cards(&self) -> Vec<CardType> {
|
||||
if let CardType::Number(NumberCard { value, .. }) = self.start_card {
|
||||
let mut result = Vec::with_capacity(usize::from(self.stack_len));
|
||||
result.push(self.start_card.clone());
|
||||
for index in 1..self.stack_len {
|
||||
let new_color = if let board::CardType::Number(board::NumberCard {
|
||||
suit: last_suit,
|
||||
..
|
||||
}) = result.last().unwrap()
|
||||
{
|
||||
Self::alternate_card(last_suit, self.pattern & (1 << (index - 1)))
|
||||
} else {
|
||||
panic!("");
|
||||
};
|
||||
result.push(board::CardType::Number(board::NumberCard {
|
||||
suit: new_color,
|
||||
value: value - index,
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return vec![self.start_card.clone()];
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn stack_len(&self) -> u8 {
|
||||
return self.stack_len;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new<'a>(
|
||||
source: FieldPosition,
|
||||
destination: FieldPosition,
|
||||
cards: &'a [board::CardType],
|
||||
) -> Self {
|
||||
let mut pattern: u8 = 0;
|
||||
let numbercard_filter = |card: &'a CardType| -> Option<&'a NumberCard> {
|
||||
if let board::CardType::Number(numbercard) = card {
|
||||
return Option::Some(numbercard);
|
||||
} else {
|
||||
return Option::None;
|
||||
}
|
||||
};
|
||||
for (index, (last_card, card)) in (0_u8..).zip(
|
||||
cards
|
||||
.iter()
|
||||
.filter_map(numbercard_filter)
|
||||
.zip(cards.iter().skip(1).filter_map(numbercard_filter)),
|
||||
) {
|
||||
pattern |= Self::bit_card(&last_card.suit, &card.suit) << index;
|
||||
debug_assert_eq!(card.value + 1, last_card.value);
|
||||
}
|
||||
|
||||
return Self {
|
||||
source,
|
||||
destination,
|
||||
start_card: cards[0].clone(),
|
||||
stack_len: cards.len() as u8,
|
||||
pattern,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl super::BoardApplication for Move {
|
||||
fn apply(&self, solboard: &mut Board) {
|
||||
solboard.field[usize::from(self.source.column())].truncate(
|
||||
solboard.field[usize::from(self.source.column())].len() - usize::from(self.stack_len()),
|
||||
);
|
||||
solboard.field[usize::from(self.destination.column())].append(&mut self.cards());
|
||||
}
|
||||
|
||||
fn undo(&self, solboard: &mut Board) {
|
||||
solboard.field[usize::from(self.destination.column())].truncate(
|
||||
solboard.field[usize::from(self.destination.column())].len()
|
||||
- usize::from(self.stack_len()),
|
||||
);
|
||||
solboard.field[usize::from(self.source.column())].append(&mut self.cards());
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn can_apply(&self, _solboard: &Board) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn can_undo(&self, _solboard: &Board) -> bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Move {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let card_name = if self.stack_len() == 1 {
|
||||
format!("{}", self.cards()[0])
|
||||
} else {
|
||||
format!("{} cards", self.stack_len())
|
||||
};
|
||||
return write!(
|
||||
f,
|
||||
"Move {} from {} to {}",
|
||||
card_name, self.source, self.destination
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_storage() {
|
||||
let card_stack = vec![
|
||||
board::CardType::Number(NumberCard {
|
||||
value: 5,
|
||||
suit: board::NumberCardColor::Red,
|
||||
}),
|
||||
board::CardType::Number(NumberCard {
|
||||
value: 4,
|
||||
suit: board::NumberCardColor::Black,
|
||||
}),
|
||||
board::CardType::Number(NumberCard {
|
||||
value: 3,
|
||||
suit: board::NumberCardColor::Green,
|
||||
}),
|
||||
];
|
||||
let source = FieldPosition::new(0, 0);
|
||||
let destination = FieldPosition::new(0, 1);
|
||||
let my_move = Move::new(source.clone(), destination.clone(), &card_stack);
|
||||
assert_eq!(my_move.cards(), card_stack);
|
||||
let my_move = Move::new(source, destination, &card_stack[0..1]);
|
||||
assert_eq!(
|
||||
my_move.cards().iter().collect::<Vec<_>>(),
|
||||
card_stack.iter().take(1).collect::<Vec<_>>()
|
||||
)
|
||||
}
|
||||
373
solver-rs/lib/actions/src/possibilities.rs
Normal file
373
solver-rs/lib/actions/src/possibilities.rs
Normal file
@@ -0,0 +1,373 @@
|
||||
use board::{
|
||||
Board, BunkerSlot, CardType, CardTypeNoHua, FieldPosition, NumberCard, NumberCardColor,
|
||||
PositionNoGoal, SpecialCardType,
|
||||
};
|
||||
|
||||
#[must_use]
|
||||
pub fn bunkerize_actions(solboard: &Board) -> Vec<crate::All> {
|
||||
let first_empty_bunker_index = solboard.bunker.iter().position(|x| match x {
|
||||
BunkerSlot::Empty => return true,
|
||||
_ => return false,
|
||||
});
|
||||
if let Option::Some(first_empty_bunker_index) = first_empty_bunker_index {
|
||||
return solboard
|
||||
.field
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, row)| {
|
||||
return row
|
||||
.last()
|
||||
.filter(|card| {
|
||||
if let CardType::Hua = card {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.map(|card| {
|
||||
let field_position = FieldPosition::new(index as u8, (row.len() - 1) as u8);
|
||||
return crate::All::Bunkerize(crate::Bunkerize {
|
||||
field_position,
|
||||
card: card.remove_hua(),
|
||||
bunker_slot_index: first_empty_bunker_index as u8,
|
||||
to_bunker: true,
|
||||
});
|
||||
});
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
fn card_fits(source: &NumberCard, dest: &NumberCard) -> bool {
|
||||
return (source.suit != dest.suit) && (source.value + 1 == dest.value);
|
||||
}
|
||||
|
||||
fn fitting_field_number_position(card: &NumberCard, board: &Board) -> Option<FieldPosition> {
|
||||
return board.field.iter().enumerate().find_map(|(index, row)| {
|
||||
if let Option::Some(CardType::Number(top_card)) = row.last() {
|
||||
if card_fits(card, top_card) {
|
||||
return Option::Some(FieldPosition::new(index as u8, (row.len()) as u8));
|
||||
}
|
||||
}
|
||||
return Option::None;
|
||||
});
|
||||
}
|
||||
|
||||
fn fitting_field_positions(card: &CardType, board: &Board) -> Vec<FieldPosition> {
|
||||
let mut result = Vec::new();
|
||||
if let CardType::Number(card) = card {
|
||||
if let Option::Some(position) = fitting_field_number_position(card, board) {
|
||||
result.push(position);
|
||||
}
|
||||
}
|
||||
if let Option::Some(position) =
|
||||
(0_u8..)
|
||||
.zip(board.field.iter())
|
||||
.find_map(|(column_index, slot)| {
|
||||
if slot.is_empty() {
|
||||
return Option::Some(FieldPosition::new(column_index, 0));
|
||||
} else {
|
||||
return Option::None;
|
||||
}
|
||||
})
|
||||
{
|
||||
result.push(position)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn debunkerize_actions(solboard: &Board) -> Vec<crate::All> {
|
||||
let number_matching_cards =
|
||||
(0_u8..)
|
||||
.zip(solboard.bunker.iter())
|
||||
.filter_map(|(bunker_slot_index, slot)| {
|
||||
if let BunkerSlot::Stash(CardTypeNoHua::Number(card)) = slot {
|
||||
return fitting_field_number_position(card, solboard).map(|field_position| {
|
||||
return crate::All::Bunkerize(crate::Bunkerize {
|
||||
card: CardTypeNoHua::Number(card.clone()),
|
||||
field_position,
|
||||
bunker_slot_index,
|
||||
to_bunker: false,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return Option::None;
|
||||
}
|
||||
});
|
||||
let empty_slot = solboard
|
||||
.field
|
||||
.iter()
|
||||
.position(|row| return row.is_empty())
|
||||
.map(|column_index| {
|
||||
return FieldPosition::new(column_index as u8, 0);
|
||||
});
|
||||
if let Option::Some(field_position) = empty_slot {
|
||||
let empty_slot_cards =
|
||||
(0_u8..)
|
||||
.zip(solboard.bunker.iter())
|
||||
.filter_map(|(bunker_slot_index, slot)| {
|
||||
if let BunkerSlot::Stash(card) = slot {
|
||||
let result = crate::Bunkerize {
|
||||
card: card.clone(),
|
||||
bunker_slot_index,
|
||||
field_position,
|
||||
to_bunker: false,
|
||||
};
|
||||
return Option::Some(crate::All::Bunkerize(result));
|
||||
} else {
|
||||
return Option::None;
|
||||
}
|
||||
});
|
||||
|
||||
return number_matching_cards.chain(empty_slot_cards).collect();
|
||||
} else {
|
||||
return number_matching_cards.collect();
|
||||
}
|
||||
}
|
||||
|
||||
struct DragonTracker {
|
||||
dragons: [(u8, [PositionNoGoal; 4]); 3],
|
||||
}
|
||||
impl DragonTracker {
|
||||
fn new() -> Self {
|
||||
return Self {
|
||||
dragons: [(0, [PositionNoGoal::Bunker { slot_index: 0 }; 4]); 3],
|
||||
};
|
||||
}
|
||||
|
||||
fn dragon_to_id(dragon: &SpecialCardType) -> u8 {
|
||||
return match dragon {
|
||||
SpecialCardType::Zhong => 0,
|
||||
SpecialCardType::Bai => 1,
|
||||
SpecialCardType::Fa => 2,
|
||||
};
|
||||
}
|
||||
fn id_to_dragon(id: u8) -> SpecialCardType {
|
||||
return match id {
|
||||
0 => SpecialCardType::Zhong,
|
||||
1 => SpecialCardType::Bai,
|
||||
2 => SpecialCardType::Fa,
|
||||
_ => panic!("Dragon id too high"),
|
||||
};
|
||||
}
|
||||
|
||||
fn push(&mut self, dragon: &SpecialCardType, position: PositionNoGoal) {
|
||||
let (ref mut count, ref mut cell) = self.dragons[usize::from(Self::dragon_to_id(dragon))];
|
||||
cell[usize::from(*count)] = position;
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
fn found_dragons(&self) -> impl Iterator<Item = (SpecialCardType, &[PositionNoGoal; 4])> {
|
||||
return (0_u8..)
|
||||
.zip(self.dragons.iter())
|
||||
.filter_map(|(index, (count, positions))| {
|
||||
if *count == 4 {
|
||||
return Option::Some((Self::id_to_dragon(index), positions));
|
||||
} else {
|
||||
return Option::None;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn dragonkill_actions(solboard: &Board) -> Vec<crate::All> {
|
||||
let mut dragon_position = DragonTracker::new();
|
||||
for (position, card) in solboard.movable_cards() {
|
||||
if let CardType::Special(card) = card {
|
||||
dragon_position.push(&card, position);
|
||||
}
|
||||
}
|
||||
let mut result: Vec<crate::All> = Vec::new();
|
||||
for (card_type, positions) in dragon_position.found_dragons() {
|
||||
let dragon_destination = solboard.bunker.iter().position(|x| {
|
||||
return match x {
|
||||
BunkerSlot::Empty => true,
|
||||
BunkerSlot::Stash(CardTypeNoHua::Special(special_card_type)) => {
|
||||
special_card_type == &card_type
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
});
|
||||
if let Option::Some(dragon_destination) = dragon_destination {
|
||||
let mut my_positions: [PositionNoGoal; 4] =
|
||||
[PositionNoGoal::Bunker { slot_index: 0 }; 4];
|
||||
my_positions.clone_from_slice(positions);
|
||||
result.push(crate::All::DragonKill(crate::DragonKill {
|
||||
card: card_type.clone(),
|
||||
source: my_positions,
|
||||
destination_slot_index: dragon_destination as u8,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn get_max_stack_count(board: &Board) -> [u8; 8] {
|
||||
let mut result = [0; 8];
|
||||
for (index, row) in result.iter_mut().zip(&board.field) {
|
||||
let row_iterator = row.iter().rev();
|
||||
let mut next_row_iterator = row.iter().rev();
|
||||
if next_row_iterator.next().is_none() {
|
||||
*index = 0;
|
||||
continue;
|
||||
}
|
||||
*index = (row_iterator
|
||||
.zip(next_row_iterator)
|
||||
.take_while(|(card, bottom_card)| {
|
||||
if let (CardType::Number(card), CardType::Number(bottom_card)) = (card, bottom_card)
|
||||
{
|
||||
return card_fits(card, bottom_card);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.count()
|
||||
+ 1) as u8;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn field_move_actions(solboard: &Board) -> Vec<crate::All> {
|
||||
let max_stack_counts: [u8; 8] = get_max_stack_count(solboard);
|
||||
let required_size: u8 = max_stack_counts.iter().cloned().sum();
|
||||
let mut result = Vec::with_capacity(usize::from(required_size));
|
||||
for ((column_index, row), stack_size) in (0_u8..)
|
||||
.zip(solboard.field.iter())
|
||||
.zip(max_stack_counts.iter())
|
||||
.filter(|(_, size)| return **size > 0)
|
||||
{
|
||||
for row_index in (row.len() - usize::from(*stack_size)) as u8..(row.len()) as u8 {
|
||||
let my_stack = &row
|
||||
.get(usize::from(row_index)..row.len())
|
||||
.expect("Slicing failed");
|
||||
for position in fitting_field_positions(
|
||||
my_stack
|
||||
.first()
|
||||
.expect("Stack should at least have one entry"),
|
||||
solboard,
|
||||
) {
|
||||
result.push(crate::All::Move(crate::Move::new(
|
||||
FieldPosition::new(column_index, row_index),
|
||||
position,
|
||||
my_stack,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn goal_move_actions(solboard: &Board) -> Vec<crate::All> {
|
||||
let suit_to_id = |suit: &NumberCardColor| -> u8 {
|
||||
return match suit {
|
||||
NumberCardColor::Red => 0,
|
||||
NumberCardColor::Green => 1,
|
||||
NumberCardColor::Black => 2,
|
||||
};
|
||||
};
|
||||
let first_empty_goal_slot_index = (0_u8..)
|
||||
.zip(solboard.goal.iter())
|
||||
.find_map(|(index, card)| {
|
||||
if card.is_none() {
|
||||
return Option::Some(index);
|
||||
} else {
|
||||
return Option::None;
|
||||
}
|
||||
})
|
||||
.unwrap_or(3);
|
||||
let mut goal_desired_pos = [(1_u8, first_empty_goal_slot_index); 3];
|
||||
|
||||
for (slot_id, card) in (0_u8..).zip(solboard.goal.iter()) {
|
||||
match card {
|
||||
Option::Some(NumberCard { value, suit }) => {
|
||||
goal_desired_pos[usize::from(suit_to_id(suit))] = (*value + 1, slot_id);
|
||||
}
|
||||
Option::None => {}
|
||||
};
|
||||
}
|
||||
let mut result = Vec::<crate::All>::new();
|
||||
for (position, card) in solboard.movable_cards() {
|
||||
if let CardType::Number(card) = card {
|
||||
if goal_desired_pos[usize::from(suit_to_id(&card.suit))].0 == card.value {
|
||||
result.push(crate::All::Goal(crate::Goal {
|
||||
card: card.clone(),
|
||||
source: position,
|
||||
goal_slot_index: goal_desired_pos[usize::from(suit_to_id(&card.suit))].1,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn huakill_actions(solboard: &Board) -> Vec<crate::All> {
|
||||
for (slot_id, field_column) in (0_u8..).zip(solboard.field.iter()) {
|
||||
if let Option::Some(CardType::Hua) = field_column.last() {
|
||||
return vec![crate::All::HuaKill(crate::HuaKill {
|
||||
field_position: FieldPosition::new(slot_id, (field_column.len() - 1) as u8),
|
||||
})];
|
||||
}
|
||||
}
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn all_actions(solboard: &Board) -> Vec<crate::All> {
|
||||
return [
|
||||
&huakill_actions(solboard)[..],
|
||||
&dragonkill_actions(solboard)[..],
|
||||
&goal_move_actions(solboard)[..],
|
||||
&debunkerize_actions(solboard)[..],
|
||||
&field_move_actions(solboard)[..],
|
||||
&bunkerize_actions(solboard)[..],
|
||||
]
|
||||
.concat();
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter_actions(solboard: &Board) -> Vec<crate::All> {
|
||||
let action_list = all_actions(solboard);
|
||||
let huakill_action = action_list.iter().find(|x| {
|
||||
if let crate::All::HuaKill(_) = x {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if let Option::Some(action) = huakill_action {
|
||||
return vec![action.clone()];
|
||||
}
|
||||
let mut goal_actions = action_list.iter().filter_map(|x| {
|
||||
if let crate::All::Goal(x) = x {
|
||||
return Option::Some(x);
|
||||
} else {
|
||||
return Option::None;
|
||||
}
|
||||
});
|
||||
let minimum_goal = solboard
|
||||
.goal
|
||||
.iter()
|
||||
.map(|x| match x {
|
||||
Option::None => return 0,
|
||||
Option::Some(card) => return card.value,
|
||||
})
|
||||
.min()
|
||||
.unwrap();
|
||||
if let Option::Some(minimum_goal_action) = goal_actions
|
||||
.by_ref()
|
||||
.min_by(|x, y| return x.card.value.cmp(&y.card.value))
|
||||
{
|
||||
if minimum_goal_action.card.value <= minimum_goal + 1 {
|
||||
return vec![crate::All::Goal(minimum_goal_action.clone())];
|
||||
}
|
||||
}
|
||||
|
||||
return action_list.to_vec();
|
||||
}
|
||||
39
solver-rs/lib/actions/src/tests.rs
Normal file
39
solver-rs/lib/actions/src/tests.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use crate::possibilities::{all_actions, bunkerize_actions, dragonkill_actions};
|
||||
use board::{BunkerSlot, CardTypeNoHua, SpecialCardType};
|
||||
#[test]
|
||||
pub fn dragonkill_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut x = board::load_test_board!("specific/dragonkill.json")?;
|
||||
assert_eq!(dragonkill_actions(&x).len(), 1);
|
||||
x.field[3].pop();
|
||||
x.bunker[2] = BunkerSlot::Stash(CardTypeNoHua::Special(SpecialCardType::Zhong));
|
||||
assert_eq!(dragonkill_actions(&x).len(), 1);
|
||||
return Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn bunkerize_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let x = board::load_test_board!("specific/dragonkill.json")?;
|
||||
assert_eq!(bunkerize_actions(&x).len(), 5);
|
||||
return Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn all_actions_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let x = board::load_test_board!("specific/dragonkill.json")?;
|
||||
let possible_actions = all_actions(&x);
|
||||
assert_eq!(possible_actions.len(), 12);
|
||||
assert_eq!(
|
||||
possible_actions.iter().fold([0, 0, 0, 0, 0], |mut sum, x| {
|
||||
match x {
|
||||
crate::All::Bunkerize(_) => sum[0] += 1,
|
||||
crate::All::Move(_) => sum[1] += 1,
|
||||
crate::All::Goal(_) => sum[2] += 1,
|
||||
crate::All::DragonKill(_) => sum[3] += 1,
|
||||
_ => sum[4] += 1,
|
||||
}
|
||||
return sum;
|
||||
}),
|
||||
[5, 5, 1, 1, 0]
|
||||
);
|
||||
return Result::Ok(());
|
||||
}
|
||||
Reference in New Issue
Block a user