Compare commits

..

12 Commits

Author SHA1 Message Date
Lukas Wölfer
5fdf1602eb Added rust solver to the repository 2025-08-08 19:16:08 +02:00
Lukas Wölfer
a9ca38e812 Refactoring 2020-07-05 13:42:35 +02:00
Lukas Wölfer
1f086a515c Worked on adjustment 2020-06-19 12:15:20 +02:00
Lukas Wölfer
7efa290295 Worked on feature extraction 2020-06-18 05:10:31 +02:00
Lukas Wölfer
11919bb13c Worked on feature extraction 2020-06-17 20:12:54 +02:00
Lukas Wölfer
fc2e3aca4c Tested with more advanced methods of card detection 2020-06-16 14:37:00 +02:00
Lukas Wölfer
1fb5a92de4 Added laptop config 2020-06-14 00:56:31 +02:00
Lukas Wölfer
63d4348f94 Made it work again, automatic goal moves messing with me though 2020-06-13 03:51:14 +02:00
Lukas Wölfer
b5d74d1ac0 Worked on assistant 2020-06-12 22:40:58 +02:00
Lukas Wölfer
f4ac445f61 Made calibration more ergonomic. Crashes because loading empty 'empty_card' directory 2020-06-12 17:31:12 +02:00
Lukas Wölfer
6565792030 Worked on additional tools 2020-06-12 03:58:42 +02:00
Lukas Wölfer
9a38c60488 Worked on rust compatibility 2020-04-09 02:55:08 +02:00
131 changed files with 7733 additions and 1241 deletions

1
.gitignore vendored
View File

@@ -1,2 +1 @@
test_config/
build/

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "shenzhen_solitaire/c++/cmake"]
path = shenzhen_solitaire/c++/cmake
url = git@github.com:corrodedHash/cmake-framework.git

View File

@@ -9,6 +9,12 @@
"python.testing.pytestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.unittestEnabled": true,
"python.pythonPath": "/home/lukas/.local/share/virtualenvs/shenzhen-solitaire-nsu5dgrx/bin/python",
"python.linting.mypyArgs": [
"--ignore-missing-imports",
"--follow-imports=silent",
"--show-column-numbers",
"--strict"
],
"python.linting.mypyEnabled": true,
"python.formatting.provider": "black"
}

BIN
16_10_conf.zip Normal file

Binary file not shown.

BIN
laptop_conf.zip Normal file

Binary file not shown.

View File

@@ -1,21 +0,0 @@
from distutils.core import setup, Extension
module1 = Extension(
"shenzhen_solitaire._shenzhen_solitaire",
sources=["shenzhen_solitaire/c++/main.i", "shenzhen_solitaire/c++/main.cpp",],
swig_opts=["-c++", "-py3"],
)
setup(
name="shenzhen_solitaire",
version="1.0",
description="This is a demo package",
packages=[
"shenzhen_solitaire",
"shenzhen_solitaire.card_detection",
"shenzhen_solitaire.clicker",
"shenzhen_solitaire.solver",
],
ext_modules=[module1],
)

View File

@@ -3,7 +3,7 @@ import enum
import itertools
from dataclasses import dataclass
from typing import Dict, List, Optional, Set, Tuple, Union
import json
class SpecialCard(enum.Enum):
"""Different types of special cards"""
@@ -50,6 +50,27 @@ class Position(enum.Enum):
Bunker = enum.auto()
Goal = enum.auto()
def _field_card_to_str(card: Card):
if card == SpecialCard.Hua:
return "Hua"
if isinstance(card, SpecialCard):
return {"Special": card.name}
elif isinstance(card, NumberCard):
return {"Number": {"value": card.number, "suit": card.suit.name}}
def _bunker_card_to_str(card: Union[Tuple[SpecialCard, int], Optional[Card]]):
if card is None:
return "Empty"
if isinstance(card, tuple):
return {"Blocked": card[0].name}
return {"Stashed": _field_card_to_str(card)}
def _goal_card_to_str(card: Optional[NumberCard]):
if card is None:
return None
return {"value": card.number, "suit": card.suit.name}
class Board:
"""Solitaire board"""
@@ -59,63 +80,53 @@ class Board:
MAX_COLUMN_SIZE = 8
def __init__(self) -> None:
self._field: List[List[Card]] = [[]] * Board.MAX_COLUMN_SIZE
self._bunker: List[Union[Tuple[SpecialCard, int], Optional[Card]]] = [None] * 3
self._goal: List[Optional[NumberCard]] = [None] * 3
self._flower_gone: bool = False
self.field: List[List[Card]] = [[]] * Board.MAX_COLUMN_SIZE
self.bunker: List[Union[Tuple[SpecialCard, int], Optional[Card]]] = [None] * 3
self.goal: List[Optional[NumberCard]] = [None] * 3
self.flower_gone: bool = False
def getGoal(self, suit: NumberCard.Suit) -> int:
for card in self._goal:
for card in self.goal:
if card is not None and card.suit == suit:
return card.number
else:
return 0
def setField(self, field: List[List[Card]]) -> None:
assert len(field) == 8
self._field = field
def getGoalId(self, suit: NumberCard.Suit) -> int:
for index, card in enumerate(self._goal):
for index, card in enumerate(self.goal):
if card is not None and card.suit == suit:
return index
else:
return self._goal.index(None)
return self.goal.index(None)
def setGoal(self, suit: NumberCard.Suit, value: int) -> None:
assert len(self._goal) == 3
assert len(self.goal) == 3
assert 0 <= value
assert value <= 9
if value == 0:
self._goal[self.getGoalId(suit)] = None
self.goal[self.getGoalId(suit)] = None
else:
self._goal[self.getGoalId(suit)] = NumberCard(suit, number=value)
self.goal[self.getGoalId(suit)] = NumberCard(suit, number=value)
def incGoal(self, suit: NumberCard.Suit) -> None:
self.setGoal(suit, self.getGoal(suit) + 1)
def solved(self) -> bool:
"""Returns true if the board is solved"""
if any(x.number != 9 for x in self._goal if x is not None):
if any(x.number != 9 for x in self.goal if x is not None):
return False
if any(not isinstance(x, tuple) for x in self._bunker):
if any(not isinstance(x, tuple) for x in self.bunker):
return False
if not self._flower_gone:
if not self.flower_gone:
return False
assert all(not x for x in self._field)
assert all(not x for x in self.field)
return True
def getField(self) -> List[List[Card]]:
return self._field
def getBunker(self) -> List[Union[Tuple[SpecialCard, int], Optional[Card]]]:
return self._bunker
@property
def state_identifier(self) -> int:
"""Returns a unique identifier to represent the board state"""
result: int = 0
for card in self._bunker:
for card in self.bunker:
result <<= 2
if isinstance(card, tuple):
result |= 0
@@ -129,12 +140,12 @@ class Board:
result |= card.identifier()
result <<= 1
if self._flower_gone:
if self.flower_gone:
result |= 1
assert len(self._goal) == 3
assert len(self.goal) == 3
suit_sequence = list(NumberCard.Suit)
for card in self._goal:
for card in self.goal:
result <<= 5
if card is None:
result |= len(suit_sequence) * 10
@@ -144,12 +155,12 @@ class Board:
# Max stack size is 13
# (4 random cards from the start, plus a stack from 9 to 1)
# So 4 bits are sufficient
for stack in self._field:
for stack in self.field:
assert len(stack) == len(stack) & 0b1111
result <<= 4
result |= len(stack)
for field_card in itertools.chain.from_iterable(self._field):
for field_card in itertools.chain.from_iterable(self.field):
result <<= 5
result |= field_card.identifier()
@@ -169,12 +180,12 @@ class Board:
SpecialCard.Hua: 0,
}
if self._flower_gone == True:
if self.flower_gone:
special_cards[SpecialCard.Hua] += 1
for card in itertools.chain(
self._bunker,
itertools.chain.from_iterable(stack for stack in self._field if stack),
self.bunker,
itertools.chain.from_iterable(stack for stack in self.field if stack),
):
if isinstance(card, tuple):
special_cards[card[0]] += 4
@@ -197,3 +208,12 @@ class Board:
if count != 4:
return False
return True
def to_json(self) -> str:
mystruct = {
"field": [[_field_card_to_str(card) for card in row] for row in self.field],
"hua_set": self.flower_gone,
"bunker": [_bunker_card_to_str(card) for card in self.bunker],
"goal": [_goal_card_to_str(card) for card in self.goal],
}
return json.dumps(mystruct)

View File

@@ -1 +0,0 @@
build*/

View File

@@ -1,11 +0,0 @@
project("solitaire" LANGUAGES CXX)
cmake_minimum_required(VERSION 3.16)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
include( "common" )
add_library(board STATIC lib/board.cpp lib/card.cpp lib/goal.cpp)
set_property(TARGET board PROPERTY CXX_STANDARD 17)
target_include_directories(board PUBLIC include)
add_subdirectory(auxiliary)

View File

@@ -1,12 +0,0 @@
find_package(SWIG REQUIRED)
include(${SWIG_USE_FILE})
find_package(PythonLibs REQUIRED)
set_property(SOURCE swig.i PROPERTY CPLUSPLUS ON)
swig_add_library(shenzhen_python TYPE SHARED LANGUAGE PYTHON SOURCES swig.i)
target_include_directories(shenzhen_python PUBLIC ${PYTHON_INCLUDE_PATH})
target_link_libraries(shenzhen_python PUBLIC board)
set_property(TARGET shenzhen_python PROPERTY CXX_STANDARD 17)

View File

@@ -1,57 +0,0 @@
%module shenzhen
%{
#include "board.hpp"
#include "card.hpp"
#include "goal.hpp"
%}
%include "std_array.i"
%include "std_string.i"
namespace solitaire {
enum class CardType : int { Zhong, Bai, Fa, Hua, Red, Green, Black };
auto isNormalCardType(CardType type) -> bool;
struct Card {
CardType type;
int value;
auto toString() const noexcept -> std::string;
};
class Goal {
std::array<std::optional<Card>, 3> goal{};
auto getEmptyId() -> std::optional<int>;
public:
auto getId(CardType suit) const noexcept -> std::optional<int>;
auto get(CardType suit) const noexcept -> std::optional<int>;
void set(CardType suit, int value) noexcept;
void inc(CardType suit) noexcept;
};
struct BunkerField {
std::optional<Card> card;
bool empty{};
bool closed{};
};
inline constexpr int MAX_ROW_SIZE = 13;
inline constexpr int MAX_COLUMN_SIZE = 8;
class Stack {
std::array<std::optional<Card>, MAX_COLUMN_SIZE> values{};
};
struct Board {
std::array<Stack, MAX_ROW_SIZE> field{};
std::array<BunkerField, 3> bunker{};
Goal goal{};
bool flower_gone{};
auto solved() const noexcept -> bool;
auto hash() const noexcept -> std::size_t;
auto correct() const noexcept -> bool;
};
}

View File

@@ -1,35 +0,0 @@
#pragma once
#include "card.hpp"
#include "goal.hpp"
#include <array>
#include <cassert>
#include <optional>
namespace solitaire {
struct BunkerField {
std::optional<Card> card;
bool empty{};
bool closed{};
};
inline constexpr int MAX_ROW_SIZE = 13;
inline constexpr int MAX_COLUMN_SIZE = 8;
class Stack {
std::array<std::optional<Card>, MAX_COLUMN_SIZE> values{};
};
struct Board {
std::array<Stack, MAX_ROW_SIZE> field{};
std::array<BunkerField, 3> bunker{};
Goal goal{};
bool flower_gone{};
[[nodiscard]] auto solved() const noexcept -> bool;
[[nodiscard]] auto hash() const noexcept -> std::size_t;
[[nodiscard]] auto correct() const noexcept -> bool;
};
} // namespace solitaire

View File

@@ -1,15 +0,0 @@
#pragma once
#include <cassert>
#include <string>
namespace solitaire {
enum class CardType : int { Zhong, Bai, Fa, Hua, Red, Green, Black };
auto isNormalCardType(CardType type) -> bool;
struct Card {
CardType type;
int value;
[[nodiscard]] auto toString() const noexcept -> std::string;
};
} // namespace solitaire

View File

@@ -1,20 +0,0 @@
#pragma once
#include "card.hpp"
#include <array>
#include <optional>
namespace solitaire {
class Goal {
std::array<std::optional<Card>, 3> goal{};
auto getEmptyId() -> std::optional<int>;
public:
[[nodiscard]] auto getId(CardType suit) const noexcept -> std::optional<int>;
[[nodiscard]] auto get(CardType suit) const noexcept -> std::optional<int>;
void set(CardType suit, int value) noexcept;
void inc(CardType suit) noexcept;
};
} // namespace solitaire

View File

@@ -1 +0,0 @@
#include "board.hpp"

View File

@@ -1,15 +0,0 @@
#include "card.hpp"
namespace solitaire {
auto
isNormalCardType(CardType type) -> bool {
switch (type) {
case CardType::Red:
case CardType::Green:
case CardType::Black:
return true;
break;
default:
return false;
}
}
} // namespace solitaire

View File

@@ -1,66 +0,0 @@
#include "goal.hpp"
namespace solitaire {
auto
Goal::getEmptyId() -> std::optional<int> {
int counter = 0;
for (const auto& slot : goal) {
if (!slot) {
return counter;
}
++counter;
}
return std::nullopt;
}
[[nodiscard]] auto
Goal::getId(CardType suit) const noexcept -> std::optional<int> {
int counter = 0;
for (const auto& slot : goal) {
if (slot && slot->type == suit) {
return counter;
}
++counter;
}
return std::nullopt;
}
[[nodiscard]] auto
Goal::get(CardType suit) const noexcept -> std::optional<int> {
if (auto index = getId(suit); index) {
return goal[*index]->value;
}
return std::nullopt;
}
void
Goal::set(CardType suit, int value) noexcept {
assert(value >= 0);
assert(value <= 9);
const auto card = [&]() -> std::optional<Card> {
if (value == 0) {
return std::nullopt;
}
return Card{suit, value};
}();
const int goal_index = [&]() -> int {
if (auto index = getId(suit); index) {
return *index;
}
return *getEmptyId();
}();
goal[goal_index] = card;
}
void
Goal::inc(CardType suit) noexcept {
auto get_value = get(suit);
int new_value = get_value ? (*get_value) + 1 : 1;
set(suit, new_value);
}
} // namespace solitaire

View File

@@ -6,6 +6,7 @@ from typing import Optional, Tuple
import cv2
import numpy
import math
@dataclass
@@ -16,8 +17,8 @@ class Adjustment:
y: int = 0
w: int = 0
h: int = 0
dx: int = 0
dy: int = 0
dx: float = 0
dy: float = 0
def get_square(
@@ -25,10 +26,10 @@ def get_square(
) -> Tuple[int, int, int, int]:
"""Get one square from index and adjustment"""
return (
adjustment.x + adjustment.dx * index_x,
adjustment.y + adjustment.dy * index_y,
adjustment.x + adjustment.w + adjustment.dx * index_x,
adjustment.y + adjustment.h + adjustment.dy * index_y,
math.floor(adjustment.x + adjustment.dx * index_x),
math.floor(adjustment.y + adjustment.dy * index_y),
math.floor(adjustment.x + adjustment.w + adjustment.dx * index_x),
math.floor(adjustment.y + adjustment.h + adjustment.dy * index_y),
)
@@ -41,61 +42,53 @@ def adjust_squares(
if not adjustment:
adjustment = Adjustment(w=10, h=10)
speed_mod = "n"
speed_mods = ["n", "s", "h"]
def _adjustment_step(keycode: int) -> None:
def _adjustment_step(keycode: int, speed_mod: str) -> None:
assert adjustment is not None
x_keys = {81: -1, 83: +1, 104: -10, 115: +10}
y_keys = {82: -1, 84: +1, 116: -10, 110: +10}
x_keys = {104: -1, 115: +1}
y_keys = {116: -1, 110: +1}
w_keys = {97: -1, 117: +1}
h_keys = {111: -1, 101: +1}
dx_keys = {59: -1, 112: +1}
dy_keys = {44: -1, 46: +1}
speed_facs = {"n": 1, "s": 8, "h": 64}
cur_high_speed_fac = speed_facs[speed_mod]
if keycode in x_keys:
adjustment.x += x_keys[keycode]
adjustment.x += x_keys[keycode] * cur_high_speed_fac
elif keycode in y_keys:
adjustment.y += y_keys[keycode]
adjustment.y += y_keys[keycode] * cur_high_speed_fac
elif keycode in w_keys:
adjustment.w += w_keys[keycode]
adjustment.w += w_keys[keycode] * cur_high_speed_fac
elif keycode in h_keys:
adjustment.h += h_keys[keycode]
adjustment.h += h_keys[keycode] * cur_high_speed_fac
elif keycode in dx_keys:
adjustment.dx += dx_keys[keycode]
adjustment.dx += dx_keys[keycode] * cur_high_speed_fac * 1 / 8
elif keycode in dy_keys:
adjustment.dy += dy_keys[keycode]
adjustment.dy += dy_keys[keycode] * cur_high_speed_fac * 1 / 8
cv2.namedWindow("Window", flags=cv2.WINDOW_NORMAL)
while True:
working_image = image.copy()
for index_x, index_y in itertools.product(range(count_x), range(count_y)):
square = get_square(adjustment, index_x, index_y)
cv2.rectangle(
working_image, (square[0], square[1]), (square[2], square[3]), (0, 0, 0)
working_image,
(math.floor(square[0]), math.floor(square[1])),
(math.floor(square[2]), math.floor(square[3])),
(0, 0, 0),
)
cv2.imshow("Window", working_image)
keycode = cv2.waitKey(0)
print(keycode)
if keycode == 27:
break
_adjustment_step(keycode)
if keycode == 229:
speed_mod = speed_mods[(speed_mods.index(speed_mod) + 1) % len(speed_mods)]
continue
_adjustment_step(keycode, speed_mod)
cv2.destroyWindow("Window")
return adjustment
def adjust_field(image: numpy.ndarray) -> Adjustment:
"""Open configuration grid for the field"""
return adjust_squares(image, 8, 13, Adjustment(42, 226, 15, 15, 119, 24))
def adjust_bunker(image: numpy.ndarray) -> Adjustment:
"""Open configuration grid for the bunker"""
return adjust_squares(image, 3, 1)
def adjust_hua(image: numpy.ndarray) -> Adjustment:
"""Open configuration grid for the flower card"""
return adjust_squares(image, 1, 1)
def adjust_goal(image: numpy.ndarray) -> Adjustment:
"""Open configuration grid for the goal"""
return adjust_squares(image, 3, 1)

View File

@@ -6,6 +6,7 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
import cv2
import numpy as np
import json
from ..board import Board, Card, NumberCard, SpecialCard
from . import adjustment, card_finder
@@ -35,7 +36,6 @@ def get_field_square_iterator(
"""Return iterator for both the square, as well as the matching card border"""
my_adj = fake_adjustment(conf.field_adjustment)
my_border_adj = fake_adjustment(conf.border_adjustment)
squares = card_finder.get_field_squares(
image, my_adj, count_x=row_count, count_y=column_count
)
@@ -49,7 +49,6 @@ def get_field_square_iterator(
def match_template(template: np.ndarray, search_image: np.ndarray) -> float:
"""Return matchiness for the template on the search image"""
res = cv2.matchTemplate(search_image, template, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
assert isinstance(max_val, (int, float))
@@ -63,7 +62,6 @@ def parse_field_square(
(match_template(template, square), name) for template, name in conf.catalogue
]
best_val, best_name = max(square_fits, key=lambda x: x[0])
best_border = max(
match_template(template=template, search_image=border)
for template in conf.card_border
@@ -225,8 +223,16 @@ def parse_goal(image: np.ndarray, conf: Configuration) -> List[Optional[NumberCa
def parse_board(image: np.ndarray, conf: Configuration) -> Board:
result = Board()
result.setField(parse_field(image, conf))
result.setGoal(parse_goal(image, conf))
result.field = parse_field(image, conf)
result.flower_gone = parse_hua(image, conf)
result.bunker = parse_bunker(image, conf)
result.goal = parse_goal(image, conf)
return result
def parse_start_board(image: np.ndarray, conf: Configuration) -> Board:
result = Board()
result.field = parse_field(image, conf)
result.flower_gone = parse_hua(image, conf)
result.bunker = [None] * 3
result.goal = parse_goal(image, conf)
return result

View File

@@ -32,7 +32,6 @@ def get_field_squares(
def catalogue_cards(squares: List[np.ndarray]) -> List[Tuple[np.ndarray, Card]]:
"""Run manual cataloging for given squares"""
cv2.namedWindow("Catalogue", cv2.WINDOW_NORMAL)
cv2.waitKey(1)
result: List[Tuple[np.ndarray, Card]] = []
print("Card ID is [B]ai, [Z]hong, [F]a, [H]ua, [R]ed, [G]reen, [B]lack")
print("Numbercard e.g. R3")
@@ -51,7 +50,7 @@ def catalogue_cards(squares: List[np.ndarray]) -> List[Tuple[np.ndarray, Card]]:
for square in squares:
while True:
cv2.imshow("Catalogue", cv2.resize(square, (500, 500)))
cv2.waitKey(1)
cv2.waitKey(100)
card_id = input("Card ID:").lower()
card_type: Optional[Card] = None
if len(card_id) == 1:

View File

@@ -99,10 +99,65 @@ def _save_adjustments(zip_file: zipfile.ZipFile, conf: Configuration) -> None:
adjustments = {}
adjustments[FIELD_ADJUSTMENT_KEY] = dataclasses.asdict(conf.field_adjustment)
adjustments[BORDER_ADJUSTMENT_KEY] = dataclasses.asdict(conf.border_adjustment)
zip_file.writestr(
ADJUSTMENT_FILE_NAME, json.dumps(adjustment),
adjustments[GOAL_ADJUSTMENT_KEY] = dataclasses.asdict(conf.goal_adjustment)
adjustments[BUNKER_ADJUSTMENT_KEY] = dataclasses.asdict(conf.bunker_adjustment)
adjustments[HUA_ADJUSTMENT_KEY] = dataclasses.asdict(conf.hua_adjustment)
adjustments[SPECIAL_BUTTON_ADJUSTMENT_KEY] = dataclasses.asdict(
conf.special_button_adjustment
)
zip_file.writestr(
ADJUSTMENT_FILE_NAME, json.dumps(adjustments),
)
def _save_special_images(zip_file: zipfile.ZipFile, conf: Configuration) -> None:
def _save_special_image(
zip_file: zipfile.ZipFile, images: List[np.ndarray], directory: str
) -> None:
for index, image in enumerate(images):
fd, myfile = tempfile.mkstemp(suffix=f".{PICTURE_EXTENSION}")
cv2.imwrite(myfile, image)
file_name = ""
zip_file.write(
myfile, arcname=f"{directory}/{index:03}.{PICTURE_EXTENSION}"
)
_save_special_image(zip_file, conf.card_border, CARD_BORDER_DIRECTORY)
_save_special_image(zip_file, conf.empty_card, EMPTY_CARD_DIRECTORY)
_save_special_image(zip_file, conf.green_card, GREEN_CARD_DIRECTORY)
_save_special_image(zip_file, conf.card_back, CARD_BACK_DIRECTORY)
def _generate_special_button_filename(
state: ButtonState, special_card: board.SpecialCard
) -> str:
state_char_map = {
ButtonState.normal: "n",
ButtonState.greyed: "g",
ButtonState.shiny: "s",
}
special_card_char_map = {
board.SpecialCard.Fa: "f",
board.SpecialCard.Zhong: "z",
board.SpecialCard.Bai: "b",
}
return f"{state_char_map[state]}{special_card_char_map[special_card]}"
def _save_special_button_images(
zip_file: zipfile.ZipFile,
special_button_images: List[Tuple[ButtonState, board.SpecialCard, np.ndarray]],
):
for index, (state, card, image) in enumerate(special_button_images):
fd, myfile = tempfile.mkstemp(suffix=f".{PICTURE_EXTENSION}")
cv2.imwrite(myfile, image)
file_name = ""
zip_file.write(
myfile,
arcname=f"{SPECIAL_BUTTON_DIRECTORY}/"
f"{_generate_special_button_filename(state,card)}"
f"{index:03}.{PICTURE_EXTENSION}",
)
def save(conf: Configuration, filename: str) -> None:
@@ -112,7 +167,8 @@ def save(conf: Configuration, filename: str) -> None:
with zipfile.ZipFile(zip_stream, "w") as zip_file:
_save_adjustments(zip_file, conf)
_save_catalogue(zip_file, conf.catalogue)
# TODO: Save card_borders and emtpy_card and green_card and special_buttons and card_back
_save_special_images(zip_file, conf)
_save_special_button_images(zip_file, conf.special_buttons)
with open(filename, "wb") as zip_archive:
zip_archive.write(zip_stream.getvalue())

View File

@@ -1,139 +1,173 @@
import time
from typing import List, Tuple
from typing import List, Tuple, Dict, Any, Union
import pyautogui
import shenzhen_solitaire.board as board
import shenzhen_solitaire.card_detection.adjustment as adjustment
import shenzhen_solitaire.card_detection.configuration as configuration
import shenzhen_solitaire.solver.board_actions as board_actions
import warnings
from dataclasses import dataclass
from shenzhen_solitaire.board import SpecialCard
DRAG_DURATION = 0.2
CLICK_DURATION = 1
DRAGON_WAIT = 1
HUA_WAIT = 1
GOAL_WAIT = 0.4
def drag(
src: Tuple[int, int], dst: Tuple[int, int], offset: Tuple[int, int] = (0, 0)
) -> None:
time.sleep(DRAG_DURATION / 3)
pyautogui.moveTo(x=src[0] + offset[0], y=src[1] + offset[1])
pyautogui.dragTo(
x=dst[0] + offset[0],
y=dst[1] + offset[1],
duration=0.4,
tween=lambda x: 0 if x < 0.5 else 1,
)
def dragSquare(
src: Tuple[int, int, int, int],
dst: Tuple[int, int, int, int],
offset: Tuple[int, int] = (0, 0),
) -> None:
drag(
(src[0] + (src[2] - src[0]) // 2, src[1] + (src[3] - src[1]) // 2),
(dst[0] + (dst[2] - dst[0]) // 2, dst[1] + (dst[3] - dst[1]) // 2),
offset,
pyautogui.mouseDown()
time.sleep(DRAG_DURATION / 3)
pyautogui.moveTo(
x=dst[0] + offset[0], y=dst[1] + offset[1],
)
pyautogui.mouseUp()
time.sleep(DRAG_DURATION / 3)
def click(point: Tuple[int, int], offset: Tuple[int, int] = (0, 0)) -> None:
time.sleep(CLICK_DURATION / 3)
pyautogui.moveTo(x=point[0] + offset[0], y=point[1] + offset[1])
pyautogui.mouseDown()
time.sleep(0.2)
time.sleep(CLICK_DURATION / 3)
pyautogui.mouseUp()
time.sleep(CLICK_DURATION / 3)
time.sleep(DRAGON_WAIT)
def clickSquare(
field: Tuple[int, int, int, int], offset: Tuple[int, int] = (0, 0)
) -> None:
click(
(field[0] + (field[2] - field[0]) // 2, field[1] + (field[3] - field[1]) // 2),
offset,
@dataclass
class DragAction:
source: Tuple[int, int]
destination: Tuple[int, int]
@dataclass
class ClickAction:
destination: Tuple[int, int]
@dataclass
class WaitAction:
duration: float
def _parse_field(
field: Dict[str, Any], conf: configuration.Configuration
) -> Tuple[int, int]:
return (
int(field["column"]) * conf.field_adjustment.dx
+ conf.field_adjustment.x
+ conf.field_adjustment.w // 2,
int(field["row"]) * conf.field_adjustment.dy
+ conf.field_adjustment.y
+ conf.field_adjustment.h // 2,
)
def handle_action(
action: board_actions.Action,
offset: Tuple[int, int],
def parse_action(
action: Dict[str, Any],
conf: configuration.Configuration,
) -> None:
if isinstance(action, board_actions.MoveAction):
src = adjustment.get_square(
conf.field_adjustment,
index_x=action.source_id,
index_y=action.source_row_index,
goal_values: Dict[str, int],
) -> Union[DragAction, ClickAction, WaitAction]:
assert len(action) == 1
action_name, info = next(iter(action.items()))
action_name = action_name.lower()
if action_name == "bunkerize":
field = _parse_field(info["field_position"], conf)
bunker = (
int(info["bunker_slot_index"]) * conf.bunker_adjustment.dx
+ conf.bunker_adjustment.x
+ conf.bunker_adjustment.w // 2,
conf.bunker_adjustment.y + conf.bunker_adjustment.h // 2,
)
dst = adjustment.get_square(
conf.field_adjustment,
index_x=action.destination_id,
index_y=action.destination_row_index,
)
dragSquare(src, dst, offset)
return
if isinstance(action, board_actions.HuaKillAction):
warnings.warn("Hua kill should be handled before handle_action")
return
if isinstance(action, board_actions.BunkerizeAction):
field = adjustment.get_square(
conf.field_adjustment,
index_x=action.field_id,
index_y=action.field_row_index,
)
bunker = adjustment.get_square(
conf.bunker_adjustment, index_x=action.bunker_id, index_y=0,
)
if action.to_bunker:
dragSquare(field, bunker, offset)
if str(info["to_bunker"]).lower() == "true":
return DragAction(source=field, destination=bunker)
else:
dragSquare(bunker, field, offset)
return
if isinstance(action, board_actions.DragonKillAction):
dragon_sequence = [
board.SpecialCard.Zhong,
board.SpecialCard.Fa,
board.SpecialCard.Bai,
]
field = adjustment.get_square(
conf.special_button_adjustment,
index_x=0,
index_y=dragon_sequence.index(action.dragon),
return DragAction(source=bunker, destination=field)
elif action_name == "move":
return DragAction(
source=_parse_field(info["source"], conf),
destination=_parse_field(info["destination"], conf),
)
clickSquare(
field, offset,
)
time.sleep(1)
return
if isinstance(action, board_actions.GoalAction):
dst = adjustment.get_square(
conf.goal_adjustment, index_x=action.goal_id, index_y=0,
)
if action.source_position == board.Position.Field:
assert action.source_row_index is not None
src = adjustment.get_square(
conf.field_adjustment,
index_x=action.source_id,
index_y=action.source_row_index,
elif action_name == "dragonkill":
dragon_sequence = [SpecialCard.Zhong, SpecialCard.Fa, SpecialCard.Bai]
dragon_name_map = {
"zhong": SpecialCard.Zhong,
"fa": SpecialCard.Fa,
"bai": SpecialCard.Bai,
}
card_type = dragon_name_map[info["card"].lower()]
dragon_id = dragon_sequence.index(card_type)
return ClickAction(
destination=(
conf.special_button_adjustment.x
+ conf.special_button_adjustment.w // 2,
conf.special_button_adjustment.y
+ dragon_id * conf.special_button_adjustment.dy
+ conf.special_button_adjustment.h // 2,
)
)
elif action_name == "goal":
current_value = goal_values[info["card"]["suit"].lower()]
proposed_value = info["card"]["value"]
assert (current_value == 0) or (current_value + 1 == proposed_value)
if proposed_value == min(goal_values.values()) + 1:
obvious = True
elif proposed_value == 2:
obvious = True
else:
assert action.source_position == board.Position.Bunker
src = adjustment.get_square(
conf.bunker_adjustment, index_x=action.source_id, index_y=0,
obvious = False
goal_values[info["card"]["suit"].lower()] = proposed_value
if obvious:
return WaitAction(duration=GOAL_WAIT)
goal = (
int(info["goal_slot_index"]) * conf.goal_adjustment.dx
+ conf.goal_adjustment.x
+ conf.goal_adjustment.w // 2,
conf.goal_adjustment.y + conf.goal_adjustment.h // 2,
)
if "Field" in info["source"]:
source = _parse_field(info["source"]["Field"], conf)
else:
source = (
int(info["source"]["Bunker"]["slot_index"]) * conf.bunker_adjustment.dx
+ conf.bunker_adjustment.x
+ conf.bunker_adjustment.w // 2,
conf.bunker_adjustment.y + conf.bunker_adjustment.h // 2,
)
dragSquare(src, dst, offset)
return
raise AssertionError("You forgot an Action type")
return DragAction(source=source, destination=goal)
elif action_name == "huakill":
return WaitAction(duration=HUA_WAIT)
else:
assert 0
def handle_actions(
actions: List[board_actions.Action],
actions: List[Dict[str, Dict[str, Any]]],
offset: Tuple[int, int],
conf: configuration.Configuration,
) -> None:
automatic_count = 0
for action in actions:
print(action)
if isinstance(action, board_actions.HuaKillAction):
automatic_count += 1
else:
time.sleep(0.5 * automatic_count)
automatic_count = 0
handle_action(action, offset, conf)
time.sleep(0.5 * automatic_count)
goal_values = {"red": 0, "black": 0, "green": 0}
action_tuples = (
(action, parse_action(action, conf, goal_values)) for action in actions
)
for name, action in action_tuples:
print(name)
if isinstance(action, DragAction):
drag(action.source, action.destination, offset)
elif isinstance(action, ClickAction):
click(action.destination, offset)
elif isinstance(action, WaitAction):
time.sleep(action.duration)

View File

@@ -1,217 +0,0 @@
"""Contains actions that can be used on the board"""
from dataclasses import dataclass
from typing import List, Optional, Tuple
from .. import board
class Action:
"""Base class for a card move action on a solitaire board"""
_before_state: int = 0
_after_state: int = 0
def _apply(self, action_board: board.Board) -> None:
pass
def _undo(self, action_board: board.Board) -> None:
pass
def apply(self, action_board: board.Board) -> None:
"""Apply action to board"""
if __debug__:
self._before_state = action_board.state_identifier
self._apply(action_board)
if __debug__:
self._after_state = action_board.state_identifier
def undo(self, action_board: board.Board) -> None:
"""Undo action to board"""
assert action_board.state_identifier == self._after_state
self._undo(action_board)
assert action_board.state_identifier == self._before_state
def automatic(self) -> bool:
if isinstance(self, HuaKillAction):
return True
if isinstance(self, GoalAction) and self.obvious:
return True
return False
@dataclass
class GoalAction(Action):
"""Move card from field to goal"""
card: board.NumberCard
source_id: int
source_row_index: Optional[int]
source_position: board.Position
goal_id: int
obvious: bool
def _apply(self, action_board: board.Board) -> None:
"""Do action"""
assert action_board.getGoalId(self.card.suit) == self.goal_id
assert action_board.getGoal(self.card.suit) + 1 == self.card.number
if self.source_position == board.Position.Field:
assert action_board.field[self.source_id][-1] == self.card
action_board.field[self.source_id].pop()
action_board.incGoal(self.card.suit)
elif self.source_position == board.Position.Bunker:
assert action_board.bunker[self.source_id] == self.card
action_board.bunker[self.source_id] = None
action_board.incGoal(self.card.suit)
else:
raise RuntimeError("Unknown position")
def _undo(self, action_board: board.Board) -> None:
"""Undo action"""
assert action_board.getGoalId(self.card.suit) == self.goal_id
assert action_board.getGoal(self.card.suit) == self.card.number
if self.source_position == board.Position.Field:
action_board.field[self.source_id].append(self.card)
elif self.source_position == board.Position.Bunker:
assert action_board.bunker[self.source_id] is None
action_board.bunker[self.source_id] = self.card
else:
raise RuntimeError("Unknown position")
action_board.setGoal(self.card.suit, action_board.getGoal(self.card.suit) - 1)
@dataclass
class BunkerizeAction(Action):
"""Move card from bunker to field"""
card: board.Card
bunker_id: int
field_id: int
field_row_index: int
to_bunker: bool
def _move_from_bunker(self, action_board: board.Board) -> None:
assert action_board.bunker[self.bunker_id] == self.card
action_board.bunker[self.bunker_id] = None
action_board.field[self.field_id].append(self.card)
def _move_to_bunker(self, action_board: board.Board) -> None:
assert action_board.field[self.field_id][-1] == self.card
assert action_board.bunker[self.bunker_id] is None
action_board.bunker[self.bunker_id] = self.card
action_board.field[self.field_id].pop()
def _apply(self, action_board: board.Board) -> None:
"""Do action"""
if self.to_bunker:
self._move_to_bunker(action_board)
else:
self._move_from_bunker(action_board)
def _undo(self, action_board: board.Board) -> None:
"""Undo action"""
if self.to_bunker:
self._move_from_bunker(action_board)
else:
self._move_to_bunker(action_board)
@dataclass
class MoveAction(Action):
"""Moving a card from one field stack to another"""
cards: List[board.Card]
source_id: int
source_row_index: int
destination_id: int
destination_row_index: int
def _shift(self, action_board: board.Board, source: int, dest: int) -> None:
"""Shift a card from the field id 'source' to field id 'dest'"""
for stack_offset, card in enumerate(self.cards, start=-len(self.cards)):
assert action_board.getField()[source][stack_offset] == card
action_board.getField()[source] = action_board.getField()[source][: -len(self.cards)]
action_board.getField()[dest].extend(self.cards)
def _apply(self, action_board: board.Board) -> None:
"""Do action"""
if action_board.getField()[self.destination_id]:
dest_card = action_board.getField()[self.destination_id][-1]
if not all(isinstance(x, board.NumberCard) for x in self.cards):
raise AssertionError()
if not isinstance(dest_card, board.NumberCard):
raise AssertionError()
if not isinstance(self.cards[0], board.NumberCard):
raise AssertionError()
if dest_card.suit == self.cards[0].suit:
raise AssertionError()
if dest_card.number != self.cards[0].number + 1:
raise AssertionError()
self._shift(action_board, self.source_id, self.destination_id)
def _undo(self, action_board: board.Board) -> None:
"""Undo action"""
self._shift(action_board, self.destination_id, self.source_id)
@dataclass
class DragonKillAction(Action):
"""Removing four dragons from the top of the stacks to a bunker"""
dragon: board.SpecialCard
source_stacks: List[Tuple[board.Position, int]]
destination_bunker_id: int
def _apply(self, action_board: board.Board) -> None:
"""Do action"""
assert (
action_board.bunker[self.destination_bunker_id] is None
or action_board.bunker[self.destination_bunker_id] == self.dragon
)
assert len(self.source_stacks) == 4
for position, index in self.source_stacks:
if position == board.Position.Field:
assert action_board.field[index]
assert action_board.field[index][-1] == self.dragon
action_board.field[index].pop()
elif position == board.Position.Bunker:
assert action_board.bunker[index] == self.dragon
action_board.bunker[index] = None
else:
raise RuntimeError("Can only kill dragons in field and bunker")
action_board.bunker[self.destination_bunker_id] = (self.dragon, 4)
def _undo(self, action_board: board.Board) -> None:
"""Undo action"""
assert action_board.bunker[self.destination_bunker_id] == (self.dragon, 4)
assert len(self.source_stacks) == 4
action_board.bunker[self.destination_bunker_id] = None
for position, index in self.source_stacks:
if position == board.Position.Field:
action_board.field[index].append(self.dragon)
elif position == board.Position.Bunker:
action_board.bunker[index] = self.dragon
else:
raise RuntimeError("Can only kill dragons in field and bunker")
@dataclass
class HuaKillAction(Action):
"""Remove the flower card"""
source_field_id: int
source_field_row_index: int
def _apply(self, action_board: board.Board) -> None:
"""Do action"""
assert not action_board.flower_gone
assert action_board.field[self.source_field_id][-1] == board.SpecialCard.Hua
action_board.field[self.source_field_id].pop()
action_board.flower_gone = True
def _undo(self, action_board: board.Board) -> None:
"""Undo action"""
assert action_board.flower_gone
action_board.field[self.source_field_id].append(board.SpecialCard.Hua)
action_board.flower_gone = False

View File

@@ -1,235 +0,0 @@
"""Contains function to iterate different kinds of possible actions"""
from typing import Iterator, List, Tuple
from .. import board
from . import board_actions
def possible_huakill_action(
search_board: board.Board,
) -> Iterator[board_actions.HuaKillAction]:
"""Check if the flowercard can be eliminated"""
for index, stack in enumerate(search_board.getField()):
if stack and stack[-1] == board.SpecialCard.Hua:
yield board_actions.HuaKillAction(
source_field_id=index, source_field_row_index=len(stack) - 1
)
def possible_dragonkill_actions(
search_board: board.Board,
) -> Iterator[board_actions.DragonKillAction]:
"""Enumerate all possible dragon kills"""
possible_dragons = [
board.SpecialCard.Zhong,
board.SpecialCard.Fa,
board.SpecialCard.Bai,
]
if not any(x is None for x in search_board.getBunker()):
new_possible_dragons = []
for dragon in possible_dragons:
if any(x == dragon for x in search_board.getBunker()):
new_possible_dragons.append(dragon)
possible_dragons = new_possible_dragons
for dragon in possible_dragons:
bunker_dragons = [i for i, d in enumerate(search_board.getBunker()) if d == dragon]
field_dragons = [
i for i, f in enumerate(search_board.getField()) if f if f[-1] == dragon
]
if len(bunker_dragons) + len(field_dragons) != 4:
continue
destination_bunker_id = 0
if bunker_dragons:
destination_bunker_id = bunker_dragons[0]
else:
destination_bunker_id = [
i for i, x in enumerate(search_board.getBunker()) if x is None
][0]
source_stacks = [(board.Position.Bunker, i) for i in bunker_dragons]
source_stacks.extend([(board.Position.Field, i) for i in field_dragons])
yield board_actions.DragonKillAction(
dragon=dragon,
source_stacks=source_stacks,
destination_bunker_id=destination_bunker_id,
)
def possible_bunkerize_actions(
search_board: board.Board,
) -> Iterator[board_actions.BunkerizeAction]:
"""Enumerates all possible card moves from the field to the bunker"""
open_bunker_list = [i for i, x in enumerate(search_board.getBunker()) if x is None]
if not open_bunker_list:
return
open_bunker = open_bunker_list[0]
for index, stack in enumerate(search_board.getField()):
if not stack:
continue
yield board_actions.BunkerizeAction(
card=stack[-1],
field_id=index,
field_row_index=len(stack) - 1,
bunker_id=open_bunker,
to_bunker=True,
)
def possible_debunkerize_actions(
search_board: board.Board,
) -> Iterator[board_actions.BunkerizeAction]:
"""Enumerates all possible card moves from the bunker to the field"""
bunker_number_cards = [
(i, x)
for i, x in enumerate(search_board.getBunker())
if isinstance(x, board.NumberCard)
]
for index, card in bunker_number_cards:
for other_index, other_stack in enumerate(search_board.getField()):
if not other_stack:
continue
if not isinstance(other_stack[-1], board.NumberCard):
continue
if other_stack[-1].suit == card.suit:
continue
if other_stack[-1].number != card.number + 1:
continue
yield board_actions.BunkerizeAction(
card=card,
bunker_id=index,
field_id=other_index,
field_row_index=len(other_stack),
to_bunker=False,
)
def possible_goal_move_actions(
search_board: board.Board,
) -> Iterator[board_actions.GoalAction]:
"""Enumerates all possible moves from anywhere to the goal"""
field_cards = [
(board.Position.Field, index, stack[-1])
for index, stack in enumerate(search_board.getField())
if stack
]
bunker_cards = [
(board.Position.Bunker, index, card)
for index, card in enumerate(search_board.getBunker())
]
top_cards = [
x for x in field_cards + bunker_cards if isinstance(x[2], board.NumberCard)
]
top_cards = [
x for x in top_cards if x[2].number == search_board.getGoal(x[2].suit) + 1
]
result = []
for source, index, card in top_cards:
assert isinstance(card, board.NumberCard)
obvious = all(
search_board.getGoal(other_suit) >= card.number - 2
for other_suit in set(board.NumberCard.Suit) - {card.suit}
)
result.append(
board_actions.GoalAction(
card=card,
source_id=index,
source_row_index=len(search_board.getField()[index]) - 1
if source == board.Position.Field
else None,
source_position=source,
goal_id=search_board.getGoalId(card.suit),
obvious=obvious,
)
)
break
yield from sorted(result, key=lambda x: x.card.number)
def _can_stack(bottom: board.Card, top: board.Card) -> bool:
if not isinstance(bottom, board.NumberCard):
return False
if not isinstance(top, board.NumberCard):
return False
if bottom.suit == top.suit:
return False
if bottom.number != top.number + 1:
return False
return True
def _get_cardstacks(search_board: board.Board) -> List[List[board.Card]]:
"""Returns all cards on one stack that can be moved at once"""
result: List[List[board.Card]] = []
for stack in search_board.getField():
result.append([])
if not stack:
continue
result[-1].append(stack[-1])
for card in stack[-2::-1]:
if not _can_stack(card, result[-1][0]):
break
if not isinstance(card, board.NumberCard):
break
result[-1].insert(0, card)
return result
def possible_field_move_actions(
search_board: board.Board,
) -> Iterator[board_actions.MoveAction]:
"""Enumerate all possible move actions
from one field stack to another field stack"""
first_empty_field_id = -1
cardstacks = [x for x in enumerate(_get_cardstacks(search_board)) if x[1]]
cardstacks = sorted(cardstacks, key=lambda x: len(x[1]))
substacks: List[Tuple[int, List[board.Card]]] = []
for index, stack in cardstacks:
substacks.extend(
(index, substack) for substack in (stack[i:] for i in range(len(stack)))
)
for source_index, source_substack in substacks:
for destination_index, destination_stack in enumerate(search_board.getField()):
if source_index == destination_index:
continue
if destination_stack:
if not _can_stack(destination_stack[-1], source_substack[0]):
continue
elif len(source_substack) == len(search_board.getField()[source_index]):
continue
elif first_empty_field_id == -1:
first_empty_field_id = destination_index
elif destination_index != first_empty_field_id:
continue
yield board_actions.MoveAction(
cards=source_substack,
source_id=source_index,
source_row_index=len(search_board.getField()[source_index])
- len(source_substack),
destination_id=destination_index,
destination_row_index=len(destination_stack),
)
def possible_actions(search_board: board.Board) -> List[board_actions.Action]:
"""Enumerate all possible actions on the current search_board"""
result: List[board_actions.Action] = [
*list(possible_huakill_action(search_board)),
*list(possible_dragonkill_actions(search_board)),
*list(possible_goal_move_actions(search_board)),
*list(possible_debunkerize_actions(search_board)),
*list(possible_field_move_actions(search_board)),
*list(possible_bunkerize_actions(search_board)),
]
for action in result:
if action.automatic():
return [action]
return result

View File

@@ -1,129 +0,0 @@
"""Contains solver for solitaire"""
import typing
from typing import Iterator, List, Optional
import time
from dataclasses import dataclass
from ..board import Board
from . import board_actions
from .board_actions import DragonKillAction, GoalAction, HuaKillAction, MoveAction
from .board_possibilities import possible_actions
@dataclass
class ActionStackFrame:
iterator: Iterator[board_actions.Action]
last_action: Optional[board_actions.Action]
state: int
def next(self) -> Optional[board_actions.Action]:
"""Get next iteration of top action iterator"""
try:
self.last_action = next(self.iterator)
except StopIteration:
return None
return self.last_action
class ActionStack:
"""Stack of chosen actions on the board"""
def __init__(self) -> None:
self.frames: List[ActionStackFrame] = []
def push(self, board: Board) -> None:
"""Append another board state to stack"""
self.frames.append(
ActionStackFrame(
iterator=iter(possible_actions(board)),
last_action=None,
state=board.state_identifier,
)
)
@property
def top(self) -> ActionStackFrame:
"""Get next iteration of top action iterator"""
return self.frames[-1]
def pop(self) -> None:
"""Pop one action from stack"""
self.frames.pop()
def __len__(self) -> int:
return len(self.frames)
def solve(
board: Board, *, timeout: Optional[float] = None, verbose: bool = False
) -> Iterator[List[board_actions.Action]]:
"""Solve a solitaire puzzle"""
state_set = {board.state_identifier}
stack = ActionStack()
stack.push(board)
def _limit_stack_size(stack_size: int) -> None:
if len(stack) == stack_size:
stack.pop()
assert stack.top.last_action is not None
stack.top.last_action.undo(board)
assert board.state_identifier in state_set
def _backtrack_action() -> None:
stack.pop()
assert stack.top.last_action is not None
stack.top.last_action.undo(board)
assert board.state_identifier in state_set
def _skip_loop_move(action: board_actions.Action) -> bool:
if not isinstance(action, MoveAction):
return False
for frame in stack.frames[-2::-1]:
if not isinstance(frame.last_action, MoveAction):
continue
if frame.last_action.cards == action.cards:
return True
return False
iter_start = time.time()
count = 0
while len(stack) > 0:
count += 1
if count > 5000:
count = 0
if verbose:
print(f"{time.time() - iter_start} {len(stack)} {board.goal}")
if timeout is not None and time.time() - iter_start > timeout:
return
# _limit_stack_size(80)
assert board.state_identifier == stack.top.state
action = stack.top.next()
if action is None:
_backtrack_action()
continue
if _skip_loop_move(action):
continue
action.apply(board)
if board.solved():
assert all(x.last_action is not None for x in stack.frames)
yield [
typing.cast(board_actions.Action, x.last_action) for x in stack.frames
]
iter_start = time.time()
action.undo(board)
assert board.state_identifier in state_set
continue
if board.state_identifier in state_set:
action.undo(board)
assert board.state_identifier in state_set
continue
state_set.add(board.state_identifier)
stack.push(board)

1
solver-rs/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

46
solver-rs/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,46 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in library 'board'",
"cargo": {
"args": [
"test",
"--no-run",
"--lib",
"--package=board"
],
"filter": {
"name": "board",
"kind": "lib"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in library 'solver'",
"cargo": {
"args": [
"test",
"--no-run",
"--lib",
"--package=solver"
],
"filter": {
"name": "solver",
"kind": "lib"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

26
solver-rs/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "cargo",
"subcommand": "build",
"problemMatcher": [
"$rustc"
],
"group": "build"
},
{
"type": "cargo",
"subcommand": "check",
"problemMatcher": [
"$rustc"
],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

375
solver-rs/Cargo.lock generated Normal file
View File

@@ -0,0 +1,375 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "action_optimization"
version = "0.1.0"
dependencies = [
"actions",
"board",
"petgraph",
"serde",
"serde_json",
]
[[package]]
name = "actions"
version = "0.1.0"
dependencies = [
"board",
"enum-iterator",
"serde",
"serde_json",
]
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "board"
version = "0.1.0"
dependencies = [
"enum-iterator",
"serde",
"serde_json",
]
[[package]]
name = "clap"
version = "2.33.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim",
"textwrap",
"unicode-width",
"vec_map",
]
[[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 = "fixedbitset"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
[[package]]
name = "heck"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hermit-abi"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71"
dependencies = [
"libc",
]
[[package]]
name = "indexmap"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292"
dependencies = [
"autocfg",
]
[[package]]
name = "itoa"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
[[package]]
name = "petgraph"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]]
name = "proc-macro-error"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn-mid",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435"
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 = "ryu"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76"
[[package]]
name = "serde"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e707fbbf255b8fc8c3b99abb91e7257a622caeb20a9818cbadbeeede4e0932ff"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac5d00fc561ba2724df6758a17de23df5914f20e41cb00f94d5b7ae42fffaff8"
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 = "shenzhen"
version = "0.1.0"
dependencies = [
"action_optimization",
"actions",
"board",
"serde",
"serde_json",
"solving",
"structopt",
]
[[package]]
name = "solving"
version = "0.1.0"
dependencies = [
"action_optimization",
"actions",
"board",
]
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "structopt"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863246aaf5ddd0d6928dfeb1a9ca65f505599e4e1b399935ef7e75107516b4ef"
dependencies = [
"clap",
"lazy_static",
"structopt-derive",
]
[[package]]
name = "structopt-derive"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d239ca4b13aee7a2142e6795cbd69e457665ff8037aed33b3effdc430d2f927a"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[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 = "syn-mid"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "unicode-segmentation"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
[[package]]
name = "unicode-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
name = "winapi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

17
solver-rs/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "shenzhen"
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 = "lib/board"}
actions = {path = "lib/actions"}
action_optimization = {path = "lib/action_optimization"}
solving = {path = "lib/solving"}
serde = {version="1.0.105",features=["derive"]}
serde_json = "1.0"
structopt = "0.3.14"

1
solver-rs/README.md Normal file
View File

@@ -0,0 +1 @@
# SHENZHEN I/O Solitaire Solver

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 3, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 1, "suit": "Green"}}, {"Special": "Fa"}, {"Special": "Zhong"}], [{"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Special": "Zhong"}], [{"Special": "Zhong"}, {"Special": "Bai"}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Black"}}], [{"Special": "Fa"}, {"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Black"}}], [{"Number": {"value": 7, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Black"}}], [{"Number": {"value": 2, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 3, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 8, "suit": "Black"}}], [{"Number": {"value": 4, "suit": "Green"}}, "Hua", {"Number": {"value": 7, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 9, "suit": "Red"}}], [{"Number": {"value": 8, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Green"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 8, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 3, "suit": "Green"}}, "Hua", {"Number": {"value": 8, "suit": "Green"}}], [{"Special": "Bai"}, {"Number": {"value": 1, "suit": "Green"}}, {"Special": "Bai"}, {"Special": "Zhong"}, {"Number": {"value": 2, "suit": "Green"}}], [{"Special": "Fa"}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Green"}}], [{"Number": {"value": 1, "suit": "Black"}}, {"Number": {"value": 7, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Special": "Zhong"}], [{"Special": "Bai"}, {"Number": {"value": 1, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Red"}}], [{"Number": {"value": 5, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Black"}}, {"Special": "Zhong"}], [{"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 3, "suit": "Red"}}], [{"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Green"}}, {"Number": {"value": 2, "suit": "Black"}}, {"Special": "Zhong"}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 4, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Green"}}], [{"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Red"}}], [{"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Special": "Fa"}, {"Number": {"value": 6, "suit": "Black"}}, {"Special": "Zhong"}], [{"Special": "Fa"}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Special": "Zhong"}, {"Special": "Zhong"}], [{"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 2, "suit": "Green"}}], [{"Number": {"value": 9, "suit": "Green"}}, "Hua", {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Special": "Fa"}], [{"Number": {"value": 3, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 4, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 7, "suit": "Black"}}], [{"Number": {"value": 7, "suit": "Green"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Special": "Bai"}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Special": "Bai"}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Green"}}, {"Special": "Bai"}], [{"Number": {"value": 9, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Black"}}], [{"Number": {"value": 7, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Special": "Zhong"}], [{"Special": "Fa"}, {"Number": {"value": 6, "suit": "Red"}}, {"Special": "Zhong"}, {"Special": "Fa"}, {"Number": {"value": 4, "suit": "Green"}}], [{"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Black"}}, {"Special": "Fa"}, {"Special": "Zhong"}, {"Number": {"value": 5, "suit": "Black"}}], [{"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Special": "Zhong"}], [{"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 8, "suit": "Green"}}], ["Hua", {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Red"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Red"}, {"value": 1, "suit": "Black"}, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Special": "Fa"}, {"Number": {"value": 8, "suit": "Red"}}, {"Special": "Zhong"}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Green"}}], [{"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Black"}}], [{"Number": {"value": 3, "suit": "Green"}}, {"Special": "Zhong"}, {"Special": "Fa"}, {"Special": "Bai"}], [{"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Green"}}], [{"Special": "Bai"}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 2, "suit": "Black"}}, {"Special": "Fa"}], [{"Number": {"value": 6, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 2, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 9, "suit": "Red"}}], [{"Number": {"value": 7, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Red"}}], [{"Special": "Zhong"}, {"Number": {"value": 1, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Red"}}]], "hua_set": true, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Black"}, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 9, "suit": "Green"}}], [{"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Red"}}], [{"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 3, "suit": "Green"}}], [{"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 2, "suit": "Red"}}], [{"Number": {"value": 7, "suit": "Green"}}, {"Special": "Bai"}, {"Special": "Zhong"}, {"Special": "Fa"}, {"Special": "Bai"}], [{"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 5, "suit": "Green"}}], [{"Special": "Fa"}, "Hua", {"Number": {"value": 3, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Black"}}], [{"Special": "Fa"}, {"Special": "Fa"}, {"Special": "Zhong"}, {"Number": {"value": 4, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Black"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Green"}, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 7, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Special": "Bai"}], [{"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 3, "suit": "Black"}}, {"Special": "Fa"}], [{"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Black"}}], [{"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Special": "Fa"}, {"Special": "Bai"}, {"Number": {"value": 7, "suit": "Black"}}], ["Hua", {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Black"}}, {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Red"}}], [{"Special": "Zhong"}, {"Special": "Zhong"}, {"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}], [{"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 3, "suit": "Red"}}], [{"Special": "Bai"}, {"Special": "Zhong"}, {"Number": {"value": 6, "suit": "Green"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Black"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Special": "Bai"}, {"Number": {"value": 7, "suit": "Black"}}], [{"Special": "Zhong"}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Green"}}, "Hua", {"Number": {"value": 8, "suit": "Red"}}], [{"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Special": "Bai"}, {"Special": "Fa"}], [{"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Red"}}], [{"Special": "Zhong"}, {"Special": "Fa"}, {"Number": {"value": 1, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 8, "suit": "Green"}}], [{"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}], [{"Special": "Bai"}, {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Special": "Zhong"}, {"Number": {"value": 9, "suit": "Black"}}], [{"Number": {"value": 1, "suit": "Red"}}, {"Special": "Fa"}, {"Special": "Fa"}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Black"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 2, "suit": "Green"}, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 7, "suit": "Green"}}, {"Special": "Zhong"}, {"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Black"}}], [{"Special": "Bai"}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Bai"}, {"Number": {"value": 3, "suit": "Black"}}], [{"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Black"}}, {"Special": "Fa"}, {"Special": "Zhong"}, {"Number": {"value": 3, "suit": "Green"}}], [{"Number": {"value": 2, "suit": "Green"}}, {"Special": "Zhong"}, {"Number": {"value": 1, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Red"}}], [{"Number": {"value": 2, "suit": "Black"}}, {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Red"}}], [{"Number": {"value": 5, "suit": "Black"}}, {"Special": "Fa"}, {"Number": {"value": 1, "suit": "Green"}}, {"Special": "Zhong"}, {"Special": "Fa"}], [{"Special": "Bai"}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Special": "Bai"}], [{"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}]], "hua_set": true, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Red"}, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Special": "Fa"}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Special": "Zhong"}, {"Special": "Fa"}], [{"Number": {"value": 1, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Black"}}], [{"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Special": "Zhong"}, {"Number": {"value": 8, "suit": "Black"}}], [{"Special": "Zhong"}, {"Special": "Zhong"}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Bai"}], [{"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Red"}}], [{"Special": "Fa"}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Red"}}, "Hua", {"Number": {"value": 5, "suit": "Green"}}], [{"Special": "Fa"}, {"Special": "Bai"}, {"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Black"}}], [{"Special": "Bai"}, {"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Green"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Black"}, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Red"}}], [{"Special": "Bai"}, "Hua", {"Special": "Bai"}, {"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Green"}}], [{"Number": {"value": 1, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 5, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 9, "suit": "Red"}}], [{"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Green"}}], [{"Special": "Bai"}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 9, "suit": "Black"}}], [{"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 7, "suit": "Black"}}], [{"Number": {"value": 2, "suit": "Black"}}, {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Special": "Zhong"}], [{"Special": "Zhong"}, {"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Special": "Zhong"}, {"Special": "Zhong"}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Green"}}, "Hua", {"Number": {"value": 3, "suit": "Black"}}], [{"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Bai"}], [{"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Black"}}], [{"Special": "Bai"}, {"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 2, "suit": "Green"}}], [{"Special": "Fa"}, {"Special": "Fa"}, {"Special": "Fa"}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Red"}}], [{"Special": "Zhong"}, {"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 9, "suit": "Red"}}], [{"Number": {"value": 2, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Special": "Fa"}], [{"Special": "Bai"}, {"Special": "Zhong"}, {"Number": {"value": 7, "suit": "Green"}}, {"Number": {"value": 8, "suit": "Red"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Black"}, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Special": "Fa"}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Red"}}], [{"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 7, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Special": "Bai"}], [{"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Special": "Bai"}, {"Special": "Bai"}, {"Special": "Fa"}], [{"Special": "Fa"}, {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Red"}}], [{"Special": "Zhong"}, {"Special": "Zhong"}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 3, "suit": "Green"}}], [{"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 7, "suit": "Green"}}], [{"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Red"}}], [{"Special": "Bai"}, {"Special": "Fa"}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Green"}}, {"Special": "Zhong"}]], "hua_set": true, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 1, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Green"}}], ["Hua", {"Special": "Zhong"}, {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 3, "suit": "Red"}}], [{"Special": "Bai"}, {"Special": "Fa"}, {"Special": "Bai"}, {"Number": {"value": 7, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Green"}}], [{"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 9, "suit": "Green"}}], [{"Special": "Zhong"}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Black"}}], [{"Number": {"value": 9, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Green"}}], [{"Special": "Bai"}, {"Number": {"value": 1, "suit": "Green"}}, {"Special": "Bai"}, {"Special": "Fa"}, {"Number": {"value": 7, "suit": "Red"}}], [{"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Special": "Zhong"}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Special": "Zhong"}, {"Number": {"value": 7, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 3, "suit": "Black"}}], ["Hua", {"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Black"}}], [{"Number": {"value": 7, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 2, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 3, "suit": "Green"}}], [{"Special": "Zhong"}, {"Number": {"value": 5, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 3, "suit": "Red"}}, {"Special": "Bai"}], [{"Number": {"value": 6, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Special": "Fa"}], [{"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Special": "Zhong"}], [{"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 8, "suit": "Green"}}, {"Special": "Zhong"}, {"Number": {"value": 8, "suit": "Black"}}], [{"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Bai"}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Red"}, {"value": 2, "suit": "Black"}, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Fa"}], [{"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 7, "suit": "Green"}}], [{"Special": "Zhong"}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Special": "Bai"}, {"Special": "Zhong"}], [{"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Special": "Fa"}, {"Special": "Fa"}], [{"Number": {"value": 8, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 7, "suit": "Red"}}], [{"Special": "Fa"}, {"Special": "Bai"}, {"Number": {"value": 2, "suit": "Red"}}, "Hua", {"Special": "Bai"}], [{"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 9, "suit": "Red"}}], [{"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Red"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Red"}, {"value": 1, "suit": "Black"}, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Green"}}], ["Hua", {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Green"}}], [{"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Zhong"}], [{"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Red"}}, {"Special": "Fa"}], [{"Special": "Bai"}, {"Number": {"value": 7, "suit": "Black"}}, {"Special": "Fa"}, {"Special": "Bai"}, {"Number": {"value": 6, "suit": "Green"}}], [{"Number": {"value": 9, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 5, "suit": "Green"}}, {"Special": "Bai"}, {"Special": "Zhong"}], [{"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Red"}}], [{"Number": {"value": 1, "suit": "Black"}}, {"Special": "Fa"}, {"Special": "Fa"}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Black"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}

View File

@@ -0,0 +1 @@
{"field": [[{"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 7, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 4, "suit": "Red"}}, {"Special": "Zhong"}], [{"Special": "Zhong"}, {"Number": {"value": 3, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Green"}}], [{"Number": {"value": 1, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 2, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 3, "suit": "Black"}}], [{"Special": "Bai"}, {"Number": {"value": 7, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Green"}}], [{"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Special": "Zhong"}, {"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Black"}}], [{"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Black"}}, "Hua", {"Number": {"value": 7, "suit": "Red"}}, {"Special": "Bai"}], [{"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 6, "suit": "Red"}}], [{"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Red"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}

View File

@@ -0,0 +1,19 @@
{
"field": [
[],
[],
["Hua", { "Number": { "value": 9, "suit": "Black" } }],
[{ "Special": "Zhong" }],
[{ "Special": "Zhong" }],
[{ "Special": "Zhong" }],
[{ "Special": "Zhong" }],
[]
],
"goal": [
{ "value": 8, "suit": "Black" },
{ "value": 9, "suit": "Green" },
{ "value": 9, "suit": "Red" }
],
"hua_set": false,
"bunker": [{ "Blocked": null }, { "Blocked": null }, "Empty"]
}

View File

@@ -0,0 +1,19 @@
{
"field": [
[],
[],
["Hua", { "Number": { "value": 9, "suit": "Black" } }],
[],
[],
[],
[],
[]
],
"goal": [
{ "value": 8, "suit": "Black" },
{ "value": 9, "suit": "Green" },
{ "value": 9, "suit": "Red" }
],
"hua_set": false,
"bunker": [{ "Blocked": null }, { "Blocked": null }, { "Blocked": null }]
}

View File

@@ -0,0 +1,10 @@
{
"field": [[], [], [], [], [], [], [], []],
"goal": [
{ "value": 9, "suit": "Black" },
{ "value": 9, "suit": "Green" },
{ "value": 9, "suit": "Red" }
],
"hua_set": true,
"bunker": [{ "Blocked": null }, { "Blocked": null }, { "Blocked": null }]
}

37
solver-rs/clippy.cfg Normal file
View File

@@ -0,0 +1,37 @@
#![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::result_unwrap_used,
// clippy::wildcard_enum_match_arm
)]
#![allow(clippy::trivially_copy_pass_by_ref)]
#![allow(dead_code)]

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

Some files were not shown because too many files have changed in this diff Show More