Compare commits
6 Commits
master
...
28546cf5e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28546cf5e9 | ||
|
|
cd1bc39bad | ||
|
|
5af5282aab | ||
|
|
de72585989 | ||
|
|
a6cca47d99 | ||
|
|
8cd39b10c2 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
test_config/
|
||||
build/
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "shenzhen_solitaire/c++/cmake"]
|
||||
path = shenzhen_solitaire/c++/cmake
|
||||
url = git@github.com:corrodedHash/cmake-framework.git
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -9,12 +9,6 @@
|
||||
"python.testing.pytestEnabled": false,
|
||||
"python.testing.nosetestsEnabled": false,
|
||||
"python.testing.unittestEnabled": true,
|
||||
"python.linting.mypyArgs": [
|
||||
"--ignore-missing-imports",
|
||||
"--follow-imports=silent",
|
||||
"--show-column-numbers",
|
||||
"--strict"
|
||||
],
|
||||
"python.linting.mypyEnabled": true,
|
||||
"python.pythonPath": "/home/lukas/.local/share/virtualenvs/shenzhen-solitaire-nsu5dgrx/bin/python",
|
||||
"python.formatting.provider": "black"
|
||||
}
|
||||
BIN
16_10_conf.zip
BIN
16_10_conf.zip
Binary file not shown.
BIN
laptop_conf.zip
BIN
laptop_conf.zip
Binary file not shown.
21
setup.py
Normal file
21
setup.py
Normal file
@@ -0,0 +1,21 @@
|
||||
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],
|
||||
)
|
||||
|
||||
@@ -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,27 +50,6 @@ 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"""
|
||||
@@ -80,53 +59,63 @@ 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
|
||||
@@ -140,12 +129,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
|
||||
@@ -155,12 +144,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()
|
||||
|
||||
@@ -180,12 +169,12 @@ class Board:
|
||||
SpecialCard.Hua: 0,
|
||||
}
|
||||
|
||||
if self.flower_gone:
|
||||
if self._flower_gone == True:
|
||||
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
|
||||
@@ -208,12 +197,3 @@ 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)
|
||||
|
||||
1
shenzhen_solitaire/c++/.gitignore
vendored
Normal file
1
shenzhen_solitaire/c++/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build*/
|
||||
11
shenzhen_solitaire/c++/CMakeLists.txt
Normal file
11
shenzhen_solitaire/c++/CMakeLists.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
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)
|
||||
12
shenzhen_solitaire/c++/auxiliary/CMakeLists.txt
Normal file
12
shenzhen_solitaire/c++/auxiliary/CMakeLists.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
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)
|
||||
57
shenzhen_solitaire/c++/auxiliary/swig.i
Normal file
57
shenzhen_solitaire/c++/auxiliary/swig.i
Normal file
@@ -0,0 +1,57 @@
|
||||
%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;
|
||||
};
|
||||
}
|
||||
1
shenzhen_solitaire/c++/cmake
Submodule
1
shenzhen_solitaire/c++/cmake
Submodule
Submodule shenzhen_solitaire/c++/cmake added at c888dc5541
35
shenzhen_solitaire/c++/include/board.hpp
Normal file
35
shenzhen_solitaire/c++/include/board.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#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
|
||||
15
shenzhen_solitaire/c++/include/card.hpp
Normal file
15
shenzhen_solitaire/c++/include/card.hpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#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
|
||||
20
shenzhen_solitaire/c++/include/goal.hpp
Normal file
20
shenzhen_solitaire/c++/include/goal.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
#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
|
||||
1
shenzhen_solitaire/c++/lib/board.cpp
Normal file
1
shenzhen_solitaire/c++/lib/board.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "board.hpp"
|
||||
15
shenzhen_solitaire/c++/lib/card.cpp
Normal file
15
shenzhen_solitaire/c++/lib/card.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#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
|
||||
66
shenzhen_solitaire/c++/lib/goal.cpp
Normal file
66
shenzhen_solitaire/c++/lib/goal.cpp
Normal file
@@ -0,0 +1,66 @@
|
||||
|
||||
#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
|
||||
@@ -6,7 +6,6 @@ from typing import Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy
|
||||
import math
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -17,8 +16,8 @@ class Adjustment:
|
||||
y: int = 0
|
||||
w: int = 0
|
||||
h: int = 0
|
||||
dx: float = 0
|
||||
dy: float = 0
|
||||
dx: int = 0
|
||||
dy: int = 0
|
||||
|
||||
|
||||
def get_square(
|
||||
@@ -26,10 +25,10 @@ def get_square(
|
||||
) -> Tuple[int, int, int, int]:
|
||||
"""Get one square from index and adjustment"""
|
||||
return (
|
||||
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),
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@@ -42,53 +41,61 @@ 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, speed_mod: str) -> None:
|
||||
def _adjustment_step(keycode: int) -> None:
|
||||
assert adjustment is not None
|
||||
x_keys = {104: -1, 115: +1}
|
||||
y_keys = {116: -1, 110: +1}
|
||||
x_keys = {81: -1, 83: +1, 104: -10, 115: +10}
|
||||
y_keys = {82: -1, 84: +1, 116: -10, 110: +10}
|
||||
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] * cur_high_speed_fac
|
||||
adjustment.x += x_keys[keycode]
|
||||
elif keycode in y_keys:
|
||||
adjustment.y += y_keys[keycode] * cur_high_speed_fac
|
||||
adjustment.y += y_keys[keycode]
|
||||
elif keycode in w_keys:
|
||||
adjustment.w += w_keys[keycode] * cur_high_speed_fac
|
||||
adjustment.w += w_keys[keycode]
|
||||
elif keycode in h_keys:
|
||||
adjustment.h += h_keys[keycode] * cur_high_speed_fac
|
||||
adjustment.h += h_keys[keycode]
|
||||
elif keycode in dx_keys:
|
||||
adjustment.dx += dx_keys[keycode] * cur_high_speed_fac * 1 / 8
|
||||
adjustment.dx += dx_keys[keycode]
|
||||
elif keycode in dy_keys:
|
||||
adjustment.dy += dy_keys[keycode] * cur_high_speed_fac * 1 / 8
|
||||
|
||||
cv2.namedWindow("Window", flags=cv2.WINDOW_NORMAL)
|
||||
adjustment.dy += dy_keys[keycode]
|
||||
|
||||
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,
|
||||
(math.floor(square[0]), math.floor(square[1])),
|
||||
(math.floor(square[2]), math.floor(square[3])),
|
||||
(0, 0, 0),
|
||||
working_image, (square[0], square[1]), (square[2], square[3]), (0, 0, 0)
|
||||
)
|
||||
cv2.imshow("Window", working_image)
|
||||
keycode = cv2.waitKey(0)
|
||||
print(keycode)
|
||||
if keycode == 27:
|
||||
break
|
||||
if keycode == 229:
|
||||
speed_mod = speed_mods[(speed_mods.index(speed_mod) + 1) % len(speed_mods)]
|
||||
continue
|
||||
_adjustment_step(keycode, speed_mod)
|
||||
_adjustment_step(keycode)
|
||||
|
||||
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)
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
@@ -36,6 +35,7 @@ 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,6 +49,7 @@ 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))
|
||||
@@ -62,6 +63,7 @@ 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
|
||||
@@ -223,16 +225,8 @@ def parse_goal(image: np.ndarray, conf: Configuration) -> List[Optional[NumberCa
|
||||
|
||||
def parse_board(image: np.ndarray, conf: Configuration) -> Board:
|
||||
result = Board()
|
||||
result.field = parse_field(image, conf)
|
||||
result.setField(parse_field(image, conf))
|
||||
result.setGoal(parse_goal(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
|
||||
@@ -32,6 +32,7 @@ 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")
|
||||
@@ -50,7 +51,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(100)
|
||||
cv2.waitKey(1)
|
||||
card_id = input("Card ID:").lower()
|
||||
card_type: Optional[Card] = None
|
||||
if len(card_id) == 1:
|
||||
|
||||
@@ -99,67 +99,12 @@ 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)
|
||||
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),
|
||||
ADJUSTMENT_FILE_NAME, json.dumps(adjustment),
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
"""Save configuration to zip archive"""
|
||||
zip_stream = io.BytesIO()
|
||||
@@ -167,8 +112,7 @@ 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)
|
||||
_save_special_images(zip_file, conf)
|
||||
_save_special_button_images(zip_file, conf.special_buttons)
|
||||
# TODO: Save card_borders and emtpy_card and green_card and special_buttons and card_back
|
||||
with open(filename, "wb") as zip_archive:
|
||||
zip_archive.write(zip_stream.getvalue())
|
||||
|
||||
|
||||
@@ -1,173 +1,139 @@
|
||||
import time
|
||||
from typing import List, Tuple, Dict, Any, Union
|
||||
from typing import List, Tuple
|
||||
|
||||
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.mouseDown()
|
||||
time.sleep(DRAG_DURATION / 3)
|
||||
pyautogui.moveTo(
|
||||
x=dst[0] + offset[0], y=dst[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.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(CLICK_DURATION / 3)
|
||||
time.sleep(0.2)
|
||||
pyautogui.mouseUp()
|
||||
time.sleep(CLICK_DURATION / 3)
|
||||
time.sleep(DRAGON_WAIT)
|
||||
|
||||
|
||||
@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 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,
|
||||
)
|
||||
|
||||
|
||||
def parse_action(
|
||||
action: Dict[str, Any],
|
||||
conf: configuration.Configuration,
|
||||
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,
|
||||
)
|
||||
if str(info["to_bunker"]).lower() == "true":
|
||||
return DragAction(source=field, destination=bunker)
|
||||
else:
|
||||
return DragAction(source=bunker, destination=field)
|
||||
elif action_name == "move":
|
||||
return DragAction(
|
||||
source=_parse_field(info["source"], conf),
|
||||
destination=_parse_field(info["destination"], conf),
|
||||
)
|
||||
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:
|
||||
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,
|
||||
)
|
||||
return DragAction(source=source, destination=goal)
|
||||
elif action_name == "huakill":
|
||||
return WaitAction(duration=HUA_WAIT)
|
||||
else:
|
||||
assert 0
|
||||
|
||||
|
||||
def handle_actions(
|
||||
actions: List[Dict[str, Dict[str, Any]]],
|
||||
def handle_action(
|
||||
action: board_actions.Action,
|
||||
offset: Tuple[int, int],
|
||||
conf: configuration.Configuration,
|
||||
) -> None:
|
||||
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)
|
||||
if isinstance(action, board_actions.MoveAction):
|
||||
src = adjustment.get_square(
|
||||
conf.field_adjustment,
|
||||
index_x=action.source_id,
|
||||
index_y=action.source_row_index,
|
||||
)
|
||||
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)
|
||||
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),
|
||||
)
|
||||
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,
|
||||
)
|
||||
else:
|
||||
assert action.source_position == board.Position.Bunker
|
||||
src = adjustment.get_square(
|
||||
conf.bunker_adjustment, index_x=action.source_id, index_y=0,
|
||||
)
|
||||
dragSquare(src, dst, offset)
|
||||
return
|
||||
raise AssertionError("You forgot an Action type")
|
||||
|
||||
|
||||
def handle_actions(
|
||||
actions: List[board_actions.Action],
|
||||
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)
|
||||
|
||||
0
shenzhen_solitaire/solver/__init__.py
Normal file
0
shenzhen_solitaire/solver/__init__.py
Normal file
217
shenzhen_solitaire/solver/board_actions.py
Normal file
217
shenzhen_solitaire/solver/board_actions.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""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
|
||||
235
shenzhen_solitaire/solver/board_possibilities.py
Normal file
235
shenzhen_solitaire/solver/board_possibilities.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""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
|
||||
129
shenzhen_solitaire/solver/solver.py
Normal file
129
shenzhen_solitaire/solver/solver.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""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
1
solver-rs/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/target
|
||||
46
solver-rs/.vscode/launch.json
vendored
46
solver-rs/.vscode/launch.json
vendored
@@ -1,46 +0,0 @@
|
||||
{
|
||||
// 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
26
solver-rs/.vscode/tasks.json
vendored
@@ -1,26 +0,0 @@
|
||||
{
|
||||
// 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
375
solver-rs/Cargo.lock
generated
@@ -1,375 +0,0 @@
|
||||
# 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"
|
||||
@@ -1,17 +0,0 @@
|
||||
[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 +0,0 @@
|
||||
# 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
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1 +0,0 @@
|
||||
{"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]}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"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 }]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"field": [[], [], [], [], [], [], [], []],
|
||||
"goal": [
|
||||
{ "value": 9, "suit": "Black" },
|
||||
{ "value": 9, "suit": "Green" },
|
||||
{ "value": 9, "suit": "Red" }
|
||||
],
|
||||
"hua_set": true,
|
||||
"bunker": [{ "Blocked": null }, { "Blocked": null }, { "Blocked": null }]
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#![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)]
|
||||
@@ -1,15 +0,0 @@
|
||||
[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"}
|
||||
@@ -1,98 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
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"))
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
#![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;
|
||||
@@ -1,526 +0,0 @@
|
||||
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));
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
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(¤t_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;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// 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)));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
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(());
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
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));
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[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"}
|
||||
@@ -1,405 +0,0 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#![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;
|
||||
@@ -1,178 +0,0 @@
|
||||
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<_>>()
|
||||
)
|
||||
}
|
||||
@@ -1,373 +0,0 @@
|
||||
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();
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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(());
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
[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
4
solver-rs/lib/board/fuzz/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
216
solver-rs/lib/board/fuzz/Cargo.lock
generated
216
solver-rs/lib/board/fuzz/Cargo.lock
generated
@@ -1,216 +0,0 @@
|
||||
# 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"
|
||||
@@ -1,26 +0,0 @@
|
||||
|
||||
[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"
|
||||
@@ -1,127 +0,0 @@
|
||||
#![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);
|
||||
}
|
||||
});
|
||||
@@ -1,403 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#![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;
|
||||
@@ -1,85 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// 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)))
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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
Reference in New Issue
Block a user