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,15 @@
[package]
name = "action_optimization"
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"
petgraph = "0.5.1"
board = {path = "../board"}
actions = {path = "../actions"}

View File

@@ -0,0 +1,98 @@
use super::graph_entity::{to_graph, ActionGraph, RelationType};
use std::{
path::Path,
process::{Command, Stdio},
};
#[must_use]
pub fn dot_actions(actions: &[actions::All]) -> String {
return dot_actiongraph(&to_graph(actions));
}
#[must_use]
fn dot_actiongraph(graph: &ActionGraph) -> String {
let edge_attr = |relation_type: &RelationType| {
let edge_style = match relation_type {
RelationType::Move => "bold",
RelationType::Unblock
| RelationType::Clear
| RelationType::Socket
| RelationType::Goal => "solid",
};
let edge_color = match relation_type {
RelationType::Move => "black",
RelationType::Unblock => "green",
RelationType::Clear => "grey",
RelationType::Socket => "red",
RelationType::Goal => "blue",
};
return format!("style=\"{}\" color=\"{}\"", edge_style, edge_color);
};
let node_attr = |action: &actions::All| {
let node_color = match action {
actions::All::Bunkerize(_) | actions::All::Move(_) => "white",
actions::All::DragonKill(_) => "silver",
actions::All::Goal(_) => "blue",
actions::All::HuaKill(_) => "gold",
};
return format!(
r#"style="filled" fillcolor="{}" label="{}" shape="rect""#,
node_color,
action.to_string().replace(r#"""#, r#"\""#)
);
};
let dot_rep = petgraph::dot::Dot::with_attr_getters(
&graph,
&[
petgraph::dot::Config::EdgeNoLabel,
petgraph::dot::Config::NodeNoLabel,
],
&|_mygraph, myedge| return edge_attr(myedge.weight()),
&|_mygraph, (_index, action)| {
return node_attr(action);
},
)
.to_string();
return dot_rep;
}
pub fn draw_graph(graph: &ActionGraph, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
//! # Errors
//! File write error
let input = dot_actiongraph(graph);
let mut child = Command::new("dot")
.args(&["-Tsvg", "-o", path.to_string_lossy().as_ref()])
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
std::io::Write::write_all(
child
.stdin
.as_mut()
.ok_or("Child process stdin has not been captured!")?,
input.as_bytes(),
)?;
let output = child.wait_with_output()?;
if !output.status.success() {
println!(
"Dot failed\n{}\n{}",
std::str::from_utf8(&output.stdout).unwrap(),
std::str::from_utf8(&output.stderr).unwrap()
);
// No idea how to return a custom error here
}
return Result::Ok(());
}
pub fn draw_actions(
actions: &[actions::All],
path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
//! # Errors
//! File write error
let graph = to_graph(actions);
return draw_graph(&graph, path);
}

View File

@@ -0,0 +1,87 @@
use super::relation::{
get_clear_parents, get_destination_parent, get_goal_parent, get_move_parents,
};
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum RelationType {
Move,
Unblock,
Clear,
Socket,
Goal,
}
impl std::fmt::Display for RelationType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let stringed = match self {
Self::Move => "Move",
Self::Unblock => "Unblock",
Self::Clear => "Clear",
Self::Socket => "Socket",
Self::Goal => "Goal",
};
return write!(f, "{}", stringed);
}
}
pub type ActionGraph = petgraph::stable_graph::StableDiGraph<actions::All, RelationType>;
pub fn to_graph(actions: &[actions::All]) -> ActionGraph {
let mut x = ActionGraph::new();
macro_rules! relations {
($actions:expr, $index: expr, $( $x:expr, $y: expr ),*) => {{
[
$(
($x)($actions, $index).into_iter().map(|b| return (b, $y)).collect::<Vec<(usize, RelationType)>>(),
)*
]
}};
}
// can you ActionGraph::from_elements here
for (index, action) in actions.iter().enumerate() {
let current_node = x.add_node(action.clone());
let relations = relations!(
actions,
index,
get_move_parents,
RelationType::Move,
get_clear_parents,
RelationType::Clear,
get_goal_parent,
RelationType::Goal
);
for (parent, relation) in relations.iter().flatten() {
x.add_edge(
petgraph::stable_graph::NodeIndex::new(*parent),
current_node,
*relation,
);
}
if let Option::Some((parent, relation)) = get_destination_parent(actions, index) {
x.add_edge(
petgraph::stable_graph::NodeIndex::new(parent),
current_node,
relation,
);
}
}
return x;
}
pub fn from_graph(graph: &ActionGraph) -> Vec<actions::All> {
match petgraph::algo::toposort(graph, Option::None) {
Ok(topo_actions) => {
let topo_actions = topo_actions
.into_iter()
.map(|index| return graph.node_weight(index).unwrap().clone())
.collect::<Vec<actions::All>>();
return topo_actions;
}
Err(c) => panic!(
"Could not toposort the graph, {:#?}, Graph: {:?}",
c,
super::draw_graph(graph, std::path::Path::new("cycle_graph.svg"))
),
}
}

View File

@@ -0,0 +1,52 @@
#![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 drawing;
mod graph_entity;
mod optimize;
pub use optimize::optimize;
mod relation;
mod util;
// mod graph_check;
pub use drawing::*;
pub mod test_actions;
#[cfg(test)]
mod tests;

View File

@@ -0,0 +1,526 @@
use super::{
graph_entity::{from_graph, to_graph, ActionGraph, RelationType},
util::{get_all_cards, get_all_sources},
};
use actions::{Bunkerize, DragonKill, Goal, Move};
use board::PositionNoGoal;
use std::collections::HashSet;
use petgraph::visit::{EdgeRef, IntoNodeReferences};
pub fn merge_actions(
descendant_action: &actions::All,
parent_action: &actions::All,
) -> Result<actions::All, String> {
debug_assert_eq!(
get_all_cards(descendant_action),
get_all_cards(parent_action)
);
match descendant_action {
actions::All::Bunkerize(action) => {
let parent_source = get_all_sources(parent_action.clone());
if parent_source.len() != 1 {
return Result::Err("Only operates on parents with one source".to_string());
}
let parent_source = &parent_source[0];
if action.to_bunker {
match parent_source {
PositionNoGoal::Field(parent_field) => {
return Result::Ok(actions::All::Bunkerize(Bunkerize {
field_position: *parent_field,
..action.clone()
}));
}
PositionNoGoal::Bunker { .. } => {
return Result::Err("Cannot merge non field move to bunkerize".to_string());
}
}
} else {
match parent_source {
PositionNoGoal::Field(parent_field) => {
return Result::Ok(actions::All::Move(Move::new(
*parent_field,
action.field_position,
&[action.card.add_hua()],
)));
}
PositionNoGoal::Bunker { .. } => panic!(
"How can you have two debunkerize actions after following each other?"
),
}
}
}
actions::All::DragonKill(_) => return Result::Err("Not implemented".to_string()),
actions::All::Goal(action) => {
let parent_source = get_all_sources(parent_action.clone());
if parent_source.len() != 1 {
return Result::Err("Only operates on parents with one source".to_string());
}
let parent_source = parent_source.into_iter().next().unwrap();
return Result::Ok(actions::All::Goal(Goal {
source: parent_source,
..action.clone()
}));
}
actions::All::HuaKill(_) => {
panic!("How do you have a move parent for a hua kill?");
}
actions::All::Move(action) => {
let parent_source = get_all_sources(parent_action.clone());
if parent_source.len() != 1 {
return Result::Err("Only operates on parents with one source".to_string());
}
let parent_source = parent_source.into_iter().next().unwrap();
match parent_source {
PositionNoGoal::Field(parent_field) => {
let mut result_action = action.clone();
result_action.source = parent_field;
return Result::Ok(actions::All::Move(result_action));
}
PositionNoGoal::Bunker { slot_index } => {
assert!(action.stack_len() == 1);
return Result::Ok(actions::All::Bunkerize(Bunkerize {
bunker_slot_index: slot_index,
card: action.cards()[0].remove_hua(),
field_position: action.destination,
to_bunker: false,
}));
}
}
}
}
}
fn get_parents(
graph: &ActionGraph,
index: petgraph::stable_graph::NodeIndex,
) -> Vec<petgraph::stable_graph::NodeIndex> {
let parent = graph
.edges_directed(index, petgraph::Direction::Incoming)
.filter_map(|x| {
if x.weight() == &RelationType::Move {
return Option::Some(x.source());
} else {
return Option::None;
}
});
return parent.collect();
}
fn socket_for(
graph: &ActionGraph,
index: petgraph::stable_graph::NodeIndex,
) -> Vec<petgraph::stable_graph::NodeIndex> {
return graph
.edges_directed(index, petgraph::Direction::Outgoing)
.filter_map(|x| {
if x.weight() == &RelationType::Socket {
return Option::Some(x.target());
} else {
return Option::None;
}
})
.collect();
}
/// # Relations when merging nodes
/// - To parent:
/// - Clearing -> To merged node, needs to be cleared no matter destination
/// - Unblocking -> remove
/// - Socketing -> remove
/// - From parent:
/// - Clearing -> From merged node, clears no matter of destination
/// - Unblocking -> From merged node, when to child both nodes can be removed
/// - Socketing -> Abort merging, probably needs to be still there
/// - To child:
/// - Clearing -> Shouldn't happen when there is no traffic between parent
/// - Unblocking -> Still required, keep
/// - Socketing -> Still required, keep
/// - From child:
/// - Clearing -> Should cancel the socket to parent, remove
/// - Unblocking -> Join with incoming unblocking of parent, otherwise cell was always empty
/// - Socketing -> keep, destination stays the same, as such still sockets the target of the relation
fn merge_edges(
graph: &mut ActionGraph,
parent: petgraph::stable_graph::NodeIndex,
child: petgraph::stable_graph::NodeIndex,
) {
let mut addable_edges = Vec::new();
let mut removable_edges = Vec::new();
let mut unblock_parent = Option::None;
let mut socket_parent = Option::None;
for parent_dependent in graph.edges_directed(parent, petgraph::Direction::Incoming) {
match parent_dependent.weight() {
RelationType::Socket => socket_parent = Option::Some(parent_dependent.source()),
RelationType::Unblock => unblock_parent = Option::Some(parent_dependent.source()),
RelationType::Move | RelationType::Clear => {
addable_edges.push((parent_dependent.source(), child, *parent_dependent.weight()));
}
RelationType::Goal => panic!("Merging actions does not work on goal actions"),
}
}
for parent_dependent in graph.edges_directed(parent, petgraph::Direction::Outgoing) {
match parent_dependent.weight() {
RelationType::Move => {
debug_assert_eq!(parent_dependent.target(), child);
}
RelationType::Unblock | RelationType::Clear => {
if parent_dependent.target() != child {
addable_edges.push((
child,
parent_dependent.target(),
*parent_dependent.weight(),
));
}
}
RelationType::Socket => {
panic!("Cannot merge a parent which provides a socket for an action")
}
RelationType::Goal => panic!("Merging actions does not work on goal actions"),
}
}
for child_dependent in graph.edges_directed(child, petgraph::Direction::Incoming) {
match child_dependent.weight() {
RelationType::Move => {
if get_all_cards(&graph[child]).len() == 1 {
debug_assert_eq!(child_dependent.source(), parent);
}
}
RelationType::Clear => {
if get_all_cards(&graph[child]).len() == 1 {
panic!("What is being cleared between the parent and the child when no other interaction happened in between?\n{:?} {}\n{:?} {}",
parent, graph.node_weight(parent).unwrap(), child, graph.node_weight(child).unwrap());
}
}
RelationType::Unblock | RelationType::Socket | RelationType::Goal => {}
}
}
for child_dependent in graph.edges_directed(child, petgraph::Direction::Outgoing) {
match child_dependent.weight() {
RelationType::Goal | RelationType::Move | RelationType::Socket => {}
RelationType::Clear => removable_edges.push(child_dependent.id()),
RelationType::Unblock => {
debug_assert!(
!(unblock_parent.is_some() && socket_parent.is_some()),
"Both unblock {:?} and socket {:?} parent for {:?}",
unblock_parent.unwrap(),
socket_parent.unwrap(),
child
);
if let Option::Some(parent_unblocker) = unblock_parent {
addable_edges.push((
parent_unblocker,
child_dependent.target(),
*child_dependent.weight(),
));
removable_edges.push(child_dependent.id());
}
if let Option::Some(parent_socket) = socket_parent {
addable_edges.push((
parent_socket,
child_dependent.target(),
RelationType::Socket,
));
removable_edges.push(child_dependent.id());
}
}
}
}
for (source, target, weight) in addable_edges {
graph.add_edge(source, target, weight);
}
for edge in removable_edges {
graph.remove_edge(edge);
}
graph.remove_node(parent);
}
pub fn try_merge(
graph: &mut ActionGraph,
parent: petgraph::stable_graph::NodeIndex,
child: petgraph::stable_graph::NodeIndex,
) -> bool {
if let Result::Ok(new_action) = merge_actions(
graph.node_weight(child).unwrap(),
graph.node_weight(parent).unwrap(),
) {
*graph.node_weight_mut(child).unwrap() = new_action;
} else {
return false;
}
merge_edges(graph, parent, child);
return true;
}
/// Remove an action from the graph which has no impact on the board
pub fn delete_null_node(graph: &mut ActionGraph, null_node: petgraph::stable_graph::NodeIndex) {
let join_edge = |graph: &mut ActionGraph, reltype: RelationType| {
let incoming_edge = graph
.edges_directed(null_node, petgraph::Direction::Incoming)
.find_map(|x| {
if x.weight() == &reltype {
return Option::Some(x.source());
} else {
return Option::None;
}
});
let outgoing_edge = graph
.edges_directed(null_node, petgraph::Direction::Outgoing)
.find_map(|x| {
if x.weight() == &reltype {
return Option::Some(x.target());
} else {
return Option::None;
}
});
if let Option::Some(incoming_edge) = incoming_edge {
if let Option::Some(outgoing_edge) = outgoing_edge {
graph.add_edge(incoming_edge, outgoing_edge, reltype);
}
}
};
join_edge(graph, RelationType::Move);
join_edge(graph, RelationType::Unblock);
for weird_edge in graph
.edges_directed(null_node, petgraph::Direction::Incoming)
.chain(graph.edges_directed(null_node, petgraph::Direction::Outgoing))
.filter(|x| {
return x.weight() != &RelationType::Move && x.weight() != &RelationType::Unblock;
})
{
eprintln!(
"Weird edge while deleting null node\n{}\n{:?} {}\n{:?} {}\n{:?} {}",
weird_edge.weight(),
null_node,
graph.node_weight(null_node).unwrap(),
weird_edge.source(),
graph.node_weight(weird_edge.source()).unwrap(),
weird_edge.target(),
graph.node_weight(weird_edge.target()).unwrap(),
)
}
graph.remove_node(null_node);
}
fn try_replace_bunker_slot(
graph: &mut ActionGraph,
index: petgraph::stable_graph::NodeIndex,
parent_slot: u8,
child_slot: u8,
) {
let swap_slot = |slot| {
if slot == child_slot {
return parent_slot;
} else if slot == parent_slot {
return child_slot;
} else {
return slot;
}
};
match graph.node_weight_mut(index).unwrap() {
actions::All::Bunkerize(Bunkerize {
bunker_slot_index, ..
}) => {
*bunker_slot_index = swap_slot(*bunker_slot_index);
}
actions::All::DragonKill(DragonKill {
source,
destination_slot_index,
..
}) => {
let slot_index = source.iter_mut().filter_map(|x| {
if let board::PositionNoGoal::Bunker { slot_index } = x {
return Option::Some(slot_index);
} else {
return Option::None;
}
});
for current_slot in slot_index {
*current_slot = swap_slot(*current_slot);
}
*destination_slot_index = swap_slot(*destination_slot_index);
}
actions::All::Goal(Goal { source, .. }) => {
if let PositionNoGoal::Bunker { slot_index } = source {
*slot_index = swap_slot(*slot_index);
}
}
actions::All::HuaKill(_) | actions::All::Move(_) => {
return;
}
}
}
fn flip_bunker_slots(
graph: &mut ActionGraph,
index: petgraph::stable_graph::NodeIndex,
parent_slot: u8,
child_slot: u8,
) {
let unblock_move_graph = petgraph::visit::EdgeFiltered::from_fn(
&*graph,
&|x: petgraph::stable_graph::EdgeReference<RelationType>| match x.weight() {
RelationType::Move | RelationType::Unblock => return true,
RelationType::Clear | RelationType::Socket | RelationType::Goal => return false,
},
);
let mut visitor = petgraph::visit::Dfs::new(&unblock_move_graph, index);
while let Option::Some(index) = visitor.next(&*graph) {
try_replace_bunker_slot(graph, index, parent_slot, child_slot);
}
}
fn is_bunker_loop(
graph: &ActionGraph,
parent: petgraph::stable_graph::NodeIndex,
child: petgraph::stable_graph::NodeIndex,
) -> bool {
if let actions::All::Bunkerize(Bunkerize {
to_bunker: parent_to_bunker,
..
}) = graph.node_weight(parent).unwrap()
{
if let actions::All::Bunkerize(Bunkerize { to_bunker, .. }) =
graph.node_weight(child).unwrap()
{
if !parent_to_bunker && *to_bunker {
// if *parent_slot == *bunker_slot_index {
// return Option::Some((*parent_slot, *bunker_slot_index));
// }
return true;
}
}
}
return false;
}
fn is_field_loop(
graph: &ActionGraph,
parent: petgraph::stable_graph::NodeIndex,
child: petgraph::stable_graph::NodeIndex,
) -> bool {
if let actions::All::Move(move_action) = graph.node_weight(parent).unwrap() {
if let actions::All::Move(child_move_action) = graph.node_weight(child).unwrap() {
debug_assert_eq!(move_action.cards(), child_move_action.cards());
debug_assert_eq!(move_action.destination, child_move_action.source);
debug_assert_eq!(move_action.stack_len(), 1);
return move_action.source == child_move_action.destination;
}
}
return false;
}
pub fn merge_step(mut graph: ActionGraph) -> ActionGraph {
let mut used_nodes = HashSet::new();
let mut mergeable = Vec::new();
let mut loop_deletion = Vec::new();
let mut bunker_loop_deletion = Vec::new();
for (index, _action) in graph.node_references() {
if used_nodes.contains(&index) {
continue;
}
let parents = get_parents(&graph, index);
if parents.len() != 1 {
continue;
}
let parent = parents.into_iter().next().unwrap();
if used_nodes.contains(&parent) {
continue;
}
if get_all_cards(graph.node_weight(parent).unwrap()).len() > 1 {
continue;
}
if get_all_cards(graph.node_weight(index).unwrap()).len() > 1 {
continue;
}
if socket_for(&graph, parent)
.into_iter()
.any(|x| return x != index)
{
continue;
}
let filtered_graph = petgraph::visit::EdgeFiltered::from_fn(&graph, |x| {
return !(x.source() == parent && x.target() == index);
});
if petgraph::algo::has_path_connecting(&filtered_graph, parent, index, Option::None) {
continue;
}
if is_bunker_loop(&graph, parent, index) {
bunker_loop_deletion.push((parent, index));
} else if is_field_loop(&graph, parent, index) {
loop_deletion.push((parent, index));
} else {
mergeable.push((parent, index));
}
used_nodes.insert(parent);
used_nodes.insert(index);
}
for (parent, child) in mergeable {
try_merge(&mut graph, parent, child);
}
for (parent, child) in loop_deletion {
merge_edges(&mut graph, parent, child);
delete_null_node(&mut graph, child);
}
for (parent, child) in bunker_loop_deletion {
let parent_slot = if let actions::All::Bunkerize(Bunkerize {
bunker_slot_index,
to_bunker,
..
}) = &graph[parent]
{
assert!(!*to_bunker);
*bunker_slot_index
} else {
panic!("Should be bunkerize action")
};
let child_slot = if let actions::All::Bunkerize(Bunkerize {
bunker_slot_index,
to_bunker,
..
}) = &graph[child]
{
assert!(*to_bunker);
*bunker_slot_index
} else {
panic!("Should be bunkerize action")
};
flip_bunker_slots(&mut graph, parent, parent_slot, child_slot);
merge_edges(&mut graph, parent, child);
delete_null_node(&mut graph, child);
}
return graph;
}
fn fix_dragonkill_destination(actions: &[actions::All]) -> Vec<actions::All> {
let graph = to_graph(actions);
let result = graph
.node_indices()
.map(|node| return graph.node_weight(node).unwrap().clone())
.collect();
return result;
}
fn fix_goal_ordering(actions: &[actions::All]) -> Vec<actions::All> {
return actions.to_vec();
}
#[must_use]
pub fn optimize(actions: &[actions::All]) -> Vec<actions::All> {
let mut graph = to_graph(actions);
let mut last_length = graph.node_count();
loop {
graph = merge_step(graph);
if graph.node_count() == last_length {
break;
}
last_length = graph.node_count();
}
let optimized_sequence = from_graph(&graph);
return fix_goal_ordering(&fix_dragonkill_destination(&optimized_sequence));
}

View File

@@ -0,0 +1,149 @@
use super::{
graph_entity::RelationType,
util::{
get_all_bottom_sources, get_all_destinations, get_all_sources, get_all_top_sources,
get_destination, get_top_destination, search_parent_tree, top_card,
},
};
use actions::{Goal, Move};
use board::PositionNoGoal;
pub fn get_move_parents(actions: &[actions::All], current_action: usize) -> Vec<usize> {
let result = get_all_sources(actions[current_action].clone())
.into_iter()
.filter_map(|cur_source_pos| {
let is_move_parent = |other_action: &actions::All| {
let destinations =
get_all_destinations(other_action.clone())
.into_iter()
.any(|cur_dest_pos| {
return cur_dest_pos == cur_source_pos;
});
return destinations;
};
let source_action = search_parent_tree(actions, current_action, is_move_parent);
return source_action.map(|(index, _)| return index);
})
.collect();
return result;
}
fn get_unblocking_parent(actions: &[actions::All], current_action: usize) -> Option<usize> {
let destination = get_destination(&actions[current_action])?;
let is_unblocking = |other_action: &actions::All| {
return get_all_sources(other_action.clone())
.into_iter()
.any(|source| return source == destination);
};
return search_parent_tree(actions, current_action, is_unblocking)
.filter(|&(_, found_action)| {
if let actions::All::Move(Move { ref source, .. }) = found_action {
return board::Position::Field(*source) == destination;
}
return true;
})
.map(|(index, _)| return index);
}
fn get_socket_parent(actions: &[actions::All], current_action: usize) -> Option<usize> {
let top_action = get_destination(&actions[current_action]);
if let Option::Some(board::Position::Field(top_action)) = top_action {
let is_socket = |action: &actions::All| {
let socket_destination = get_top_destination(action.clone());
if let Option::Some(board::Position::Field(destination)) = socket_destination {
return top_card(&destination) == top_action;
}
return false;
};
let added_socket =
search_parent_tree(actions, current_action, is_socket).map(|(index, _)| {
return index;
});
let unblocking_parent = get_unblocking_parent(actions, current_action);
if added_socket < unblocking_parent {
return Option::None;
} else {
return added_socket;
}
}
return Option::None;
}
pub fn get_destination_parent(
actions: &[actions::All],
current_action: usize,
) -> Option<(usize, RelationType)> {
let socket_parent = get_socket_parent(actions, current_action);
let unblock_parent = get_unblocking_parent(actions, current_action);
if socket_parent.is_none() && unblock_parent.is_none() {
return Option::None;
} else if socket_parent > unblock_parent {
return Option::Some((socket_parent.unwrap(), RelationType::Socket));
} else {
return Option::Some((unblock_parent.unwrap(), RelationType::Unblock));
}
}
/// Actions which moved cards on top of other cards away
pub fn get_clear_parents(actions: &[actions::All], current_action: usize) -> Vec<usize> {
let filter_fields = |x: PositionNoGoal| {
if let PositionNoGoal::Field(f) = x {
return Some(f);
} else {
return None;
}
};
let source_positions = get_all_top_sources(&actions[current_action]);
let parents: Vec<usize> = source_positions
.into_iter()
.filter_map(|current_source_pos| {
let current_source_pos = filter_fields(current_source_pos)?;
let latest_moves = get_move_parents(actions, current_action);
let latest_move = if let actions::All::DragonKill(_) = actions[current_action] {
latest_moves
.into_iter()
.find(|index| {
return get_destination(&actions[*index])
== Option::Some(board::Position::Field(current_source_pos));
})
.unwrap_or(0)
} else {
latest_moves.into_iter().max().unwrap_or(0)
};
let is_clearing = move |other_action: &actions::All| {
let sources = get_all_bottom_sources(other_action);
let clear_parent = sources
.into_iter()
.filter_map(filter_fields)
.any(|cur_dest_pos| return top_card(&current_source_pos) == cur_dest_pos);
return clear_parent;
};
return search_parent_tree(actions, current_action, is_clearing)
.map(|(index, _)| return index)
.filter(|index| return *index >= latest_move);
})
.collect();
return parents;
}
pub fn get_goal_parent(actions: &[actions::All], current_action: usize) -> Option<usize> {
if let actions::All::Goal(Goal { card, .. }) = &actions[current_action] {
let is_successive_goal = move |other_action: &actions::All| {
if let actions::All::Goal(Goal {
card: other_card, ..
}) = other_action
{
return other_card.value + 1 == card.value && other_card.suit == card.suit;
}
return false;
};
if card.value > 1 {
let parent_goal = search_parent_tree(actions, current_action, is_successive_goal)
.map(|(index, _)| return index);
return parent_goal;
}
}
return Option::None;
}

View File

@@ -0,0 +1,20 @@
// 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_ACTION_ROOT {
() => {
concat!(env!("CARGO_MANIFEST_DIR"),
"/../../aux/actions/")
}
}
#[macro_export]
macro_rules! load_test_action {
( $relpath:expr ) => {
{
return serde_json::from_str::<Vec<actions::All>>(include_str!(concat!($crate::TEST_ACTION_ROOT!(),
$relpath)));
}
};
}

View File

@@ -0,0 +1,66 @@
use crate::{draw_graph, graph_entity::to_graph};
use std::str::FromStr;
#[test]
#[ignore]
pub fn optimize_bunker_loop() {
use actions::{All, Bunkerize, Goal};
use board::FieldPosition;
let numbercard = board::NumberCard {
suit: board::NumberCardColor::Red,
value: 1,
};
let zhong_card = board::CardType::Number(numbercard.clone());
let actions = vec![
All::Bunkerize(Bunkerize {
bunker_slot_index: 0,
card: zhong_card.remove_hua(),
to_bunker: false,
field_position: FieldPosition::new(2, 0),
}),
All::Bunkerize(Bunkerize {
bunker_slot_index: 2,
card: zhong_card.remove_hua(),
to_bunker: true,
field_position: FieldPosition::new(2, 0),
}),
All::Goal(Goal {
card: numbercard,
goal_slot_index: 0,
source: board::PositionNoGoal::Bunker { slot_index: 2 },
}),
];
let graph = to_graph(&actions);
draw_graph(&graph, std::path::Path::new("unopt_bunker.svg")).unwrap();
let graph = crate::optimize::merge_step(graph);
draw_graph(&graph, std::path::Path::new("opt_bunker.svg")).unwrap();
}
#[test]
pub fn all_boards_correct() -> Result<(), Box<dyn std::error::Error>> {
for i in 1..19 {
let action_string =
std::fs::read_to_string(std::format!("{}/{:02}.json", crate::TEST_ACTION_ROOT!(), i))?;
let actions: Vec<actions::All> = serde_json::from_str(&action_string)?;
let board_string = std::fs::read_to_string(std::format!(
"{}/normal/{:02}.json",
board::TEST_BOARD_ROOT!(),
i
))?;
let src_board = board::Board::from_str(&board_string)?;
let mut board = src_board.clone();
for action in actions.iter() {
action.apply(&mut board);
}
assert!(board.solved());
let actions = crate::optimize(&actions);
let mut board = src_board;
for (index, action) in actions.into_iter().enumerate() {
println!("{}", index);
action.apply(&mut board);
}
assert!(board.solved());
}
return Result::Ok(());
}

View File

@@ -0,0 +1,173 @@
use actions::{Bunkerize, DragonKill, Goal, HuaKill, Move};
use board::{CardType, FieldPosition};
use std::convert::TryFrom;
fn node_name(index: usize) -> String {
return format!("action_{:04}", index);
}
/// Position on top of this position (increments `position.row_index` by one)
pub fn top_card(position: &FieldPosition) -> FieldPosition {
return FieldPosition::new(position.column(), position.row() + 1);
}
pub fn column_range(position: &FieldPosition, count: usize) -> Vec<FieldPosition> {
return (0..count)
.map(|i| {
return FieldPosition::new(
position.column(),
position.row() + u8::try_from(i).unwrap(),
);
})
.collect();
}
pub fn get_all_sources(action: actions::All) -> Vec<board::PositionNoGoal> {
match action {
actions::All::Bunkerize(Bunkerize {
bunker_slot_index,
to_bunker,
field_position,
..
}) => {
if to_bunker {
return vec![board::PositionNoGoal::Field(field_position)];
} else {
return vec![board::PositionNoGoal::Bunker {
slot_index: bunker_slot_index,
}];
}
}
actions::All::DragonKill(DragonKill { source, .. }) => {
return source.to_vec();
}
actions::All::Goal(Goal { source, .. }) => {
return vec![source];
}
actions::All::HuaKill(HuaKill { field_position }) => {
return vec![board::PositionNoGoal::Field(field_position)]
}
actions::All::Move(move_action) => {
return column_range(&move_action.source, usize::from(move_action.stack_len()))
.into_iter()
.map(board::PositionNoGoal::Field)
.collect()
}
}
}
pub fn get_all_top_sources(action: &actions::All) -> Vec<board::PositionNoGoal> {
if let actions::All::Move(move_action) = &action {
return vec![board::PositionNoGoal::Field(FieldPosition::new(
move_action.source.column(),
move_action.source.row() + move_action.stack_len() - 1,
))];
} else {
return get_all_sources(action.clone());
};
}
pub fn get_all_bottom_sources(action: &actions::All) -> Vec<board::PositionNoGoal> {
if let actions::All::Move(Move { source, .. }) = &action {
return vec![board::PositionNoGoal::Field(*source)];
} else {
return get_all_sources(action.clone());
};
}
pub fn get_all_cards(action: &actions::All) -> Vec<board::CardType> {
match action {
actions::All::Bunkerize(Bunkerize { card, .. }) => return vec![card.add_hua()], /* Does this actually work? */
actions::All::DragonKill(DragonKill { card, .. }) => {
return vec![
CardType::Special(card.clone()),
CardType::Special(card.clone()),
CardType::Special(card.clone()),
CardType::Special(card.clone()),
]
}
actions::All::Goal(Goal { card, .. }) => return vec![CardType::Number(card.clone())],
actions::All::HuaKill(_) => return vec![CardType::Hua],
actions::All::Move(move_action) => return move_action.cards(),
}
}
pub fn get_destination(action: &actions::All) -> Option<board::Position> {
match action {
actions::All::Bunkerize(Bunkerize {
field_position,
to_bunker,
bunker_slot_index,
..
}) => {
if *to_bunker {
return Option::Some(board::Position::Bunker {
slot_index: *bunker_slot_index,
});
} else {
return Option::Some(board::Position::Field(*field_position));
}
}
actions::All::DragonKill(DragonKill {
destination_slot_index,
..
}) => {
return Option::Some(board::Position::Bunker {
slot_index: *destination_slot_index,
});
}
actions::All::Goal(Goal {
goal_slot_index, ..
}) => {
return Option::Some(board::Position::Goal {
slot_index: *goal_slot_index,
});
}
actions::All::HuaKill(_) => return Option::None,
actions::All::Move(Move { destination, .. }) => {
return Option::Some(board::Position::Field(*destination));
}
}
}
/// Returns the destination of a move, or the topmost card in its destination when moving multiple cards
pub fn get_top_destination(action: actions::All) -> Option<board::Position> {
if let actions::All::Move(move_action) = action {
return Option::Some(board::Position::Field(FieldPosition::new(
move_action.destination.column(),
move_action.destination.row() + move_action.stack_len() - 1,
)));
} else {
return get_destination(&action);
};
}
pub fn get_all_destinations(action: actions::All) -> Vec<board::Position> {
if let actions::All::Move(move_action) = action {
return column_range(
&move_action.destination,
usize::from(move_action.stack_len()),
)
.into_iter()
.map(board::Position::Field)
.collect();
} else {
return get_destination(&action).into_iter().collect();
};
}
pub fn search_parent_tree<F>(
actions: &[actions::All],
current_action: usize,
predicate: F,
) -> Option<(usize, &actions::All)>
where
F: Fn(&actions::All) -> bool,
{
return actions
.iter()
.enumerate()
.take(current_action)
.rev()
.find(|&(_, action)| return predicate(action));
}

View 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"}

View 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),
}
}
}

View 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;

View 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<_>>()
)
}

View 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();
}

View 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(());
}

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(());
}

View File

@@ -0,0 +1,12 @@
[package]
name = "solving"
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]
board = {path = "../board" }
actions = {path = "../actions"}
graphing = {package = "action_optimization", path = "../action_optimization"}

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 128 KiB

View File

@@ -0,0 +1,63 @@
use super::solve;
#[must_use]
pub fn actions_correct(actions: &[actions::All], mut board: board::Board, verbose: bool) -> bool {
for (index, action) in actions.iter().enumerate() {
if verbose {
println!("Action #{:3}: {}", index, action);
}
action.apply(&mut board);
}
return board.solved();
}
fn run_test(board: board::Board) -> (f32, usize) {
use std::io::Write;
std::io::stdout().flush().expect("Flushing did not work");
assert_eq!(board.check(), Result::Ok(()));
let start_time = std::time::Instant::now();
let result = solve(&board);
let result_time = start_time.elapsed().as_secs_f32();
assert!(!result.is_err());
let mut result_length: usize = 0;
if let Result::Ok(actions) = result {
assert!(actions_correct(&actions, board, false));
result_length = actions.len();
}
return (result_time, result_length);
}
fn run_all_tests(
dirname: &std::path::Path,
exclude: &[&str],
) -> Result<(), Box<dyn std::error::Error>> {
for board_json in std::fs::read_dir(dirname)? {
let board_json = board_json?;
if exclude.contains(
&board_json
.path()
.as_path()
.file_name()
.unwrap()
.to_str()
.unwrap(),
) {
continue;
}
let x = board::Board::from_file(&board_json.path())?;
print!(
"> {:<20} ",
board_json.path().file_stem().unwrap().to_string_lossy(),
);
let (time, length) = run_test(x);
println!("{:.02} {:3}", time, length);
}
return Result::Ok(());
}
#[test]
#[ignore]
pub fn possible() -> Result<(), Box<dyn std::error::Error>> {
//! # Errors
let whole_board_dir: std::path::PathBuf = [board::TEST_BOARD_ROOT!(), "normal"].iter().collect();
println!("{:?}", whole_board_dir);
return run_all_tests(&whole_board_dir, &[]);
}

View File

@@ -0,0 +1,30 @@
#[derive(Clone, Debug)]
pub(super) struct ActionBoard {
board: board::Board,
stack: Vec<actions::All>,
}
impl ActionBoard {
pub(super) fn new(board: board::Board) -> Self {
return Self {
board,
stack: Vec::new(),
};
}
pub(super) fn push(&mut self, action: actions::All) {
action.apply(&mut self.board);
self.stack.push(action);
debug_assert_eq!(self.board.check(), Result::Ok(()));
}
pub(super) fn pop(&mut self) -> Option<actions::All> {
if let Option::Some(action) = self.stack.pop() {
action.undo(&mut self.board);
debug_assert_eq!(self.board.check(), Result::Ok(()));
return Option::Some(action);
}
return Option::None;
}
pub(super) const fn board(&self) -> &board::Board {
return &self.board;
}
}

View File

@@ -0,0 +1,32 @@
use super::{ super::BoardState};
use crate::board_state_iterator::BoardStateIterator;
use super::loop_move_avoider::LoopMoveAvoider;
pub(crate) trait BoardStateIteratorAdapter {
fn advance(&mut self);
fn get(&mut self) -> Option<BoardState>;
fn next(&mut self) -> Option<BoardState> {
self.advance();
return self.get();
}
fn unique(self) -> super::Unique<Self>
where
Self: Sized,
{
return super::Unique::new(self);
}
fn avoid_loops(self) -> LoopMoveAvoider<Self>
where Self: Sized,
{
return LoopMoveAvoider::new(self);
}
}
impl BoardStateIteratorAdapter for BoardStateIterator {
fn advance(&mut self) {
self.advance();
}
fn get(&mut self) -> Option<BoardState> {
return self.get();
}
}

View File

@@ -0,0 +1,38 @@
use super::super::BoardState;
use super::BoardStateIteratorAdapter;
pub(crate) struct LoopMoveAvoider<T: BoardStateIteratorAdapter> {
base_iterator: T,
}
impl<T: BoardStateIteratorAdapter> LoopMoveAvoider<T> {
pub(crate) fn new(base_iterator: T) -> Self {
return Self { base_iterator };
}
fn is_loop_move(state: &BoardState) -> bool {
let last_action = state.action_stack().rev().next();
if let Option::Some(actions::All::Move(last_move_action)) = last_action {
let loop_move = state.action_stack().rev().skip(1).find(|x| {
if let actions::All::Move(action) = x {
return action.cards() == last_move_action.cards();
} else {
return false;
}
});
return loop_move.is_some();
} else {
return false;
}
}
}
impl<T: BoardStateIteratorAdapter> BoardStateIteratorAdapter for LoopMoveAvoider<T> {
fn advance(&mut self) {
while let Option::Some(mut state) = self.base_iterator.next() {
if !Self::is_loop_move(&state) {
return;
}
state.kill();
}
}
fn get(&mut self) -> Option<BoardState> {
return self.base_iterator.get();
}
}

View File

@@ -0,0 +1,5 @@
mod loop_move_avoider;
mod unique;
pub(crate) use unique::*;
mod base;
pub(crate) use base::*;

View File

@@ -0,0 +1,30 @@
use super::super::BoardState;
use super::BoardStateIteratorAdapter;
pub(crate) struct Unique<T> {
base_iterator: T,
known_boards: std::collections::HashSet<board::BoardEqHash>,
}
impl<T> Unique<T> {
pub(crate) fn new(base_iterator: T) -> Self {
return Self {
base_iterator,
known_boards: std::collections::HashSet::new(),
};
}
}
impl<T: BoardStateIteratorAdapter> BoardStateIteratorAdapter for Unique<T> {
fn get(&mut self) -> Option<BoardState> {
return self.base_iterator.get();
}
fn advance(&mut self) {
while let Option::Some(mut nextboard) = self.base_iterator.next() {
let eq_hash = nextboard.board().equivalence_hash();
if !self.known_boards.contains(&eq_hash) {
self.known_boards.insert(eq_hash);
return;
}
nextboard.kill();
}
}
}

View File

@@ -0,0 +1,105 @@
use super::action_board::ActionBoard;
use super::stack_frame::StackFrame;
#[derive(Debug)]
pub(crate) struct BoardState<'a> {
state_it: &'a mut BoardStateIterator,
}
impl<'a> BoardState<'a> {
pub(crate) fn new(state_it: &'a mut BoardStateIterator) -> Self {
return Self { state_it };
}
pub(crate) fn board(&'a self) -> &'a board::Board {
return self.state_it.board.board();
}
pub(crate) fn action_stack(
&'a self,
) -> Box<dyn std::iter::DoubleEndedIterator<Item = actions::All> + 'a> {
return self.state_it.action_stack();
}
pub(crate) fn kill(&'a mut self) {
if let Option::Some(node) = self.state_it.stack.last_mut() {
node.taint_child();
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct BoardStateIterator {
board: ActionBoard,
stack: Vec<StackFrame>,
}
impl BoardStateIterator {
pub(crate) fn new(board: board::Board) -> Self {
let mut result = Self {
board: ActionBoard::new(board),
stack: Vec::new(),
};
let actions = actions::possibilities::filter_actions(result.board.board());
if !actions.is_empty() {
result.push(actions);
}
return result;
}
fn unwind(&mut self) {
for i in (0..self.stack.len()).rev() {
self.board.pop();
if let Option::Some(action) = self.stack[i].next() {
self.board.push(action.clone());
return;
}
self.stack.pop();
}
}
fn pop(&mut self) {
if self.stack.is_empty() {
return;
}
assert_ne!(self.stack.pop().unwrap().get(), Option::None);
self.board.pop();
self.stack.pop();
self.unwind();
}
fn push(&mut self, actions: Vec<actions::All>) {
assert!(!actions.is_empty());
self.board.push(actions.first().unwrap().clone());
self.stack.push(StackFrame::new(actions));
}
fn action_stack<'a>(
&'a self,
) -> Box<dyn std::iter::DoubleEndedIterator<Item = actions::All> + 'a> {
return Box::new(self.stack.iter().map(|x| return x.get().unwrap().clone()));
}
pub(crate) fn get(&mut self) -> Option<BoardState> {
if self.stack.is_empty() {
return Option::None;
}
return Option::Some(BoardState::new(self));
}
pub(crate) fn next(&mut self) -> Option<BoardState> {
self.advance();
return self.get();
}
pub(crate) fn advance(&mut self) {
if let Option::Some(node) = self.stack.last() {
if node.child_tainted() {
return self.unwind();
}
}
if let Option::Some(current_board) = self.get() {
let actions = actions::possibilities::filter_actions(current_board.board());
if actions.is_empty() {
return self.unwind();
}
self.push(actions);
}
}
}

View File

@@ -0,0 +1,86 @@
enum SearcherNodeState<'a> {
Unexplored,
Exhausted,
Exploring(Box<SearcherNode<'a>>),
}
struct SearcherNode<'a> {
children: Vec<SearcherNodeState<'a>>,
parent: &'a SearcherNodeType<'a>,
parent_id: usize,
action: board::actions::All,
}
struct SearcherNodeRoot<'a> {
children: Vec<SearcherNodeState<'a>>,
}
enum SearcherNodeType<'a> {
Root(SearcherNodeRoot<'a>),
Normal(SearcherNode<'a>),
}
fn toSearcherNodes<'a>(
parent: &'a SearcherNodeType<'a>,
actions: Vec<board::actions::All>,
) -> Vec<SearcherNodeState<'a>> {
return actions
.into_iter()
.enumerate()
.map(|(index, action)| {
return SearcherNodeState::Exploring(Box::new(SearcherNode {
parent,
children: vec![],
parent_id: index,
action,
}));
})
.collect();
}
struct Searcher<'a> {
root: SearcherNodeRoot<'a>,
board: board::Board,
current_node: Option<&'a mut SearcherNodeState<'a>>,
current_board: board::Board,
}
impl<'a> Searcher<'a> {
pub(crate) fn new(board: board::Board) -> Self {
let actions = super::filter_actions(&board, &board::possibilities::all_actions(&board));
let mut root = SearcherNodeRoot { children: vec![] };
root.children = toSearcherNodes(&SearcherNodeType::Root(root), actions);
let mut current_board = board.clone();
let current_node = root.children.first_mut();
if let Option::Some(SearcherNodeState::Exploring(action_node)) = current_node {
action_node.action.apply(&mut current_board);
}
return Self {
root,
board,
current_node,
current_board,
};
}
pub(crate) fn advance(&'a mut self) {
if let Option::Some(action_node) = self.current_node {
if let SearcherNodeState::Exploring(expl_action_node) = action_node {
let actions = super::filter_actions(
&self.current_board,
&board::possibilities::all_actions(&self.current_board),
);
expl_action_node.children =
toSearcherNodes(&SearcherNodeType::Normal(**expl_action_node), actions);
}
}
}
pub(crate) fn get(&'a self) -> Option<&'a board::Board> {
self.current_node?;
return Option::Some(&self.current_board);
}
pub(crate) fn next(&'a mut self) -> Option<&'a board::Board> {
self.advance();
return self.get();
}
pub(crate) fn kill_children(&'a mut self) {
}
}

View File

@@ -0,0 +1,5 @@
mod action_board;
pub mod adapter;
mod base;
mod stack_frame;
pub(crate) use base::*;

View File

@@ -0,0 +1,36 @@
#[derive(Clone, Debug)]
pub(super) struct StackFrame {
all_options: Vec<actions::All>,
options_iter: usize,
child_tainted: bool,
}
impl StackFrame {
pub(super) fn new(actions: Vec<actions::All>) -> Self {
return Self {
all_options: actions,
child_tainted: false,
options_iter: 0,
};
}
pub(super) fn get(&self) -> Option<&actions::All> {
if self.options_iter >= self.all_options.len() {
return Option::None;
}
return Option::Some(&self.all_options[self.options_iter]);
}
pub(super) fn advance(&mut self) {
self.options_iter += 1;
self.child_tainted = false;
}
pub(super) fn next(&mut self) -> Option<&actions::All> {
self.advance();
return self.get();
}
pub(super) fn taint_child(&mut self) {
assert_eq!(self.child_tainted, false);
self.child_tainted = true;
}
pub(super) fn child_tainted(&self) -> bool {
return self.child_tainted;
}
}

View File

@@ -0,0 +1,46 @@
#![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,
clippy::redundant_pub_crate, // Just dont understand it, maybe fix instead?
)]
// 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::result_unwrap_used,
clippy::result_expect_used,
// clippy::wildcard_enum_match_arm
)]
#![allow(clippy::trivially_copy_pass_by_ref)]
#![allow(dead_code)]
mod board_state_iterator;
mod solve;
pub use solve::*;
pub mod benchmark;
#[cfg(test)]
pub mod tests;

View File

@@ -0,0 +1,30 @@
use super::board_state_iterator::adapter::BoardStateIteratorAdapter;
use super::board_state_iterator::BoardStateIterator;
#[derive(Debug)]
pub struct Error {}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return write!(f, "Board options exhausted, no solution found");
}
}
impl std::error::Error for Error {}
pub fn solve(solboard: &board::Board) -> Result<Vec<actions::All>, Error> {
//! # Errors
//! Returns error when no solution could be found
if solboard.solved() {
return Result::Ok(vec![]);
}
let mut stack = BoardStateIterator::new(solboard.clone())
.unique()
.avoid_loops();
while let Option::Some(current_board) = stack.next() {
if current_board.board().solved() {
return Result::Ok(current_board.action_stack().collect());
}
}
return Result::Err(Error {});
}

View File

@@ -0,0 +1,11 @@
use crate::solve;
#[test]
pub fn test_almost_solved() -> Result<(), Box<dyn std::error::Error>> {
//! # Errors
let x = board::load_test_board!("specific/scarce.json")?;
assert_eq!(x.check(), Result::Ok(()));
let result = solve(&x);
assert!(result.is_ok());
return Result::Ok(());
}