Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fdf1602eb | ||
|
|
a9ca38e812 | ||
|
|
1f086a515c | ||
|
|
7efa290295 | ||
|
|
11919bb13c | ||
|
|
fc2e3aca4c | ||
|
|
1fb5a92de4 | ||
|
|
63d4348f94 | ||
|
|
b5d74d1ac0 | ||
|
|
f4ac445f61 | ||
|
|
6565792030 | ||
|
|
9a38c60488 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1 @@
|
|||||||
test_config/
|
test_config/
|
||||||
build/
|
|
||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[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,6 +9,12 @@
|
|||||||
"python.testing.pytestEnabled": false,
|
"python.testing.pytestEnabled": false,
|
||||||
"python.testing.nosetestsEnabled": false,
|
"python.testing.nosetestsEnabled": false,
|
||||||
"python.testing.unittestEnabled": true,
|
"python.testing.unittestEnabled": true,
|
||||||
"python.pythonPath": "/home/lukas/.local/share/virtualenvs/shenzhen-solitaire-nsu5dgrx/bin/python",
|
"python.linting.mypyArgs": [
|
||||||
|
"--ignore-missing-imports",
|
||||||
|
"--follow-imports=silent",
|
||||||
|
"--show-column-numbers",
|
||||||
|
"--strict"
|
||||||
|
],
|
||||||
|
"python.linting.mypyEnabled": true,
|
||||||
"python.formatting.provider": "black"
|
"python.formatting.provider": "black"
|
||||||
}
|
}
|
||||||
BIN
16_10_conf.zip
Normal file
BIN
16_10_conf.zip
Normal file
Binary file not shown.
BIN
laptop_conf.zip
Normal file
BIN
laptop_conf.zip
Normal file
Binary file not shown.
21
setup.py
21
setup.py
@@ -1,21 +0,0 @@
|
|||||||
from distutils.core import setup, Extension
|
|
||||||
|
|
||||||
module1 = Extension(
|
|
||||||
"shenzhen_solitaire._shenzhen_solitaire",
|
|
||||||
sources=["shenzhen_solitaire/c++/main.i", "shenzhen_solitaire/c++/main.cpp",],
|
|
||||||
swig_opts=["-c++", "-py3"],
|
|
||||||
)
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="shenzhen_solitaire",
|
|
||||||
version="1.0",
|
|
||||||
description="This is a demo package",
|
|
||||||
packages=[
|
|
||||||
"shenzhen_solitaire",
|
|
||||||
"shenzhen_solitaire.card_detection",
|
|
||||||
"shenzhen_solitaire.clicker",
|
|
||||||
"shenzhen_solitaire.solver",
|
|
||||||
],
|
|
||||||
ext_modules=[module1],
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ import enum
|
|||||||
import itertools
|
import itertools
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Optional, Set, Tuple, Union
|
from typing import Dict, List, Optional, Set, Tuple, Union
|
||||||
|
import json
|
||||||
|
|
||||||
class SpecialCard(enum.Enum):
|
class SpecialCard(enum.Enum):
|
||||||
"""Different types of special cards"""
|
"""Different types of special cards"""
|
||||||
@@ -50,6 +50,27 @@ class Position(enum.Enum):
|
|||||||
Bunker = enum.auto()
|
Bunker = enum.auto()
|
||||||
Goal = 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:
|
class Board:
|
||||||
"""Solitaire board"""
|
"""Solitaire board"""
|
||||||
@@ -59,63 +80,53 @@ class Board:
|
|||||||
MAX_COLUMN_SIZE = 8
|
MAX_COLUMN_SIZE = 8
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._field: List[List[Card]] = [[]] * Board.MAX_COLUMN_SIZE
|
self.field: List[List[Card]] = [[]] * Board.MAX_COLUMN_SIZE
|
||||||
self._bunker: List[Union[Tuple[SpecialCard, int], Optional[Card]]] = [None] * 3
|
self.bunker: List[Union[Tuple[SpecialCard, int], Optional[Card]]] = [None] * 3
|
||||||
self._goal: List[Optional[NumberCard]] = [None] * 3
|
self.goal: List[Optional[NumberCard]] = [None] * 3
|
||||||
self._flower_gone: bool = False
|
self.flower_gone: bool = False
|
||||||
|
|
||||||
def getGoal(self, suit: NumberCard.Suit) -> int:
|
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:
|
if card is not None and card.suit == suit:
|
||||||
return card.number
|
return card.number
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def setField(self, field: List[List[Card]]) -> None:
|
|
||||||
assert len(field) == 8
|
|
||||||
self._field = field
|
|
||||||
|
|
||||||
def getGoalId(self, suit: NumberCard.Suit) -> int:
|
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:
|
if card is not None and card.suit == suit:
|
||||||
return index
|
return index
|
||||||
else:
|
else:
|
||||||
return self._goal.index(None)
|
return self.goal.index(None)
|
||||||
|
|
||||||
def setGoal(self, suit: NumberCard.Suit, value: int) -> None:
|
def setGoal(self, suit: NumberCard.Suit, value: int) -> None:
|
||||||
assert len(self._goal) == 3
|
assert len(self.goal) == 3
|
||||||
assert 0 <= value
|
assert 0 <= value
|
||||||
assert value <= 9
|
assert value <= 9
|
||||||
if value == 0:
|
if value == 0:
|
||||||
self._goal[self.getGoalId(suit)] = None
|
self.goal[self.getGoalId(suit)] = None
|
||||||
else:
|
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:
|
def incGoal(self, suit: NumberCard.Suit) -> None:
|
||||||
self.setGoal(suit, self.getGoal(suit) + 1)
|
self.setGoal(suit, self.getGoal(suit) + 1)
|
||||||
|
|
||||||
def solved(self) -> bool:
|
def solved(self) -> bool:
|
||||||
"""Returns true if the board is solved"""
|
"""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
|
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
|
return False
|
||||||
if not self._flower_gone:
|
if not self.flower_gone:
|
||||||
return False
|
return False
|
||||||
assert all(not x for x in self._field)
|
assert all(not x for x in self.field)
|
||||||
return True
|
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
|
@property
|
||||||
def state_identifier(self) -> int:
|
def state_identifier(self) -> int:
|
||||||
"""Returns a unique identifier to represent the board state"""
|
"""Returns a unique identifier to represent the board state"""
|
||||||
result: int = 0
|
result: int = 0
|
||||||
for card in self._bunker:
|
for card in self.bunker:
|
||||||
result <<= 2
|
result <<= 2
|
||||||
if isinstance(card, tuple):
|
if isinstance(card, tuple):
|
||||||
result |= 0
|
result |= 0
|
||||||
@@ -129,12 +140,12 @@ class Board:
|
|||||||
result |= card.identifier()
|
result |= card.identifier()
|
||||||
|
|
||||||
result <<= 1
|
result <<= 1
|
||||||
if self._flower_gone:
|
if self.flower_gone:
|
||||||
result |= 1
|
result |= 1
|
||||||
|
|
||||||
assert len(self._goal) == 3
|
assert len(self.goal) == 3
|
||||||
suit_sequence = list(NumberCard.Suit)
|
suit_sequence = list(NumberCard.Suit)
|
||||||
for card in self._goal:
|
for card in self.goal:
|
||||||
result <<= 5
|
result <<= 5
|
||||||
if card is None:
|
if card is None:
|
||||||
result |= len(suit_sequence) * 10
|
result |= len(suit_sequence) * 10
|
||||||
@@ -144,12 +155,12 @@ class Board:
|
|||||||
# Max stack size is 13
|
# Max stack size is 13
|
||||||
# (4 random cards from the start, plus a stack from 9 to 1)
|
# (4 random cards from the start, plus a stack from 9 to 1)
|
||||||
# So 4 bits are sufficient
|
# So 4 bits are sufficient
|
||||||
for stack in self._field:
|
for stack in self.field:
|
||||||
assert len(stack) == len(stack) & 0b1111
|
assert len(stack) == len(stack) & 0b1111
|
||||||
result <<= 4
|
result <<= 4
|
||||||
result |= len(stack)
|
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 <<= 5
|
||||||
result |= field_card.identifier()
|
result |= field_card.identifier()
|
||||||
|
|
||||||
@@ -169,12 +180,12 @@ class Board:
|
|||||||
SpecialCard.Hua: 0,
|
SpecialCard.Hua: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self._flower_gone == True:
|
if self.flower_gone:
|
||||||
special_cards[SpecialCard.Hua] += 1
|
special_cards[SpecialCard.Hua] += 1
|
||||||
|
|
||||||
for card in itertools.chain(
|
for card in itertools.chain(
|
||||||
self._bunker,
|
self.bunker,
|
||||||
itertools.chain.from_iterable(stack for stack in self._field if stack),
|
itertools.chain.from_iterable(stack for stack in self.field if stack),
|
||||||
):
|
):
|
||||||
if isinstance(card, tuple):
|
if isinstance(card, tuple):
|
||||||
special_cards[card[0]] += 4
|
special_cards[card[0]] += 4
|
||||||
@@ -197,3 +208,12 @@ class Board:
|
|||||||
if count != 4:
|
if count != 4:
|
||||||
return False
|
return False
|
||||||
return True
|
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
1
shenzhen_solitaire/c++/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
build*/
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
project("solitaire" LANGUAGES CXX)
|
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.16)
|
|
||||||
|
|
||||||
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
|
||||||
include( "common" )
|
|
||||||
|
|
||||||
add_library(board STATIC lib/board.cpp lib/card.cpp lib/goal.cpp)
|
|
||||||
set_property(TARGET board PROPERTY CXX_STANDARD 17)
|
|
||||||
target_include_directories(board PUBLIC include)
|
|
||||||
add_subdirectory(auxiliary)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
find_package(SWIG REQUIRED)
|
|
||||||
include(${SWIG_USE_FILE})
|
|
||||||
|
|
||||||
find_package(PythonLibs REQUIRED)
|
|
||||||
|
|
||||||
set_property(SOURCE swig.i PROPERTY CPLUSPLUS ON)
|
|
||||||
swig_add_library(shenzhen_python TYPE SHARED LANGUAGE PYTHON SOURCES swig.i)
|
|
||||||
|
|
||||||
target_include_directories(shenzhen_python PUBLIC ${PYTHON_INCLUDE_PATH})
|
|
||||||
target_link_libraries(shenzhen_python PUBLIC board)
|
|
||||||
|
|
||||||
set_property(TARGET shenzhen_python PROPERTY CXX_STANDARD 17)
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
%module shenzhen
|
|
||||||
%{
|
|
||||||
#include "board.hpp"
|
|
||||||
#include "card.hpp"
|
|
||||||
#include "goal.hpp"
|
|
||||||
%}
|
|
||||||
|
|
||||||
%include "std_array.i"
|
|
||||||
%include "std_string.i"
|
|
||||||
|
|
||||||
namespace solitaire {
|
|
||||||
enum class CardType : int { Zhong, Bai, Fa, Hua, Red, Green, Black };
|
|
||||||
|
|
||||||
auto isNormalCardType(CardType type) -> bool;
|
|
||||||
|
|
||||||
struct Card {
|
|
||||||
CardType type;
|
|
||||||
int value;
|
|
||||||
auto toString() const noexcept -> std::string;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Goal {
|
|
||||||
std::array<std::optional<Card>, 3> goal{};
|
|
||||||
auto getEmptyId() -> std::optional<int>;
|
|
||||||
|
|
||||||
public:
|
|
||||||
auto getId(CardType suit) const noexcept -> std::optional<int>;
|
|
||||||
auto get(CardType suit) const noexcept -> std::optional<int>;
|
|
||||||
|
|
||||||
void set(CardType suit, int value) noexcept;
|
|
||||||
void inc(CardType suit) noexcept;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct BunkerField {
|
|
||||||
std::optional<Card> card;
|
|
||||||
bool empty{};
|
|
||||||
bool closed{};
|
|
||||||
};
|
|
||||||
|
|
||||||
inline constexpr int MAX_ROW_SIZE = 13;
|
|
||||||
inline constexpr int MAX_COLUMN_SIZE = 8;
|
|
||||||
|
|
||||||
class Stack {
|
|
||||||
std::array<std::optional<Card>, MAX_COLUMN_SIZE> values{};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Board {
|
|
||||||
std::array<Stack, MAX_ROW_SIZE> field{};
|
|
||||||
std::array<BunkerField, 3> bunker{};
|
|
||||||
Goal goal{};
|
|
||||||
bool flower_gone{};
|
|
||||||
|
|
||||||
auto solved() const noexcept -> bool;
|
|
||||||
auto hash() const noexcept -> std::size_t;
|
|
||||||
auto correct() const noexcept -> bool;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Submodule shenzhen_solitaire/c++/cmake deleted from c888dc5541
@@ -1,35 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "card.hpp"
|
|
||||||
#include "goal.hpp"
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cassert>
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
namespace solitaire {
|
|
||||||
|
|
||||||
struct BunkerField {
|
|
||||||
std::optional<Card> card;
|
|
||||||
bool empty{};
|
|
||||||
bool closed{};
|
|
||||||
};
|
|
||||||
|
|
||||||
inline constexpr int MAX_ROW_SIZE = 13;
|
|
||||||
inline constexpr int MAX_COLUMN_SIZE = 8;
|
|
||||||
|
|
||||||
class Stack {
|
|
||||||
std::array<std::optional<Card>, MAX_COLUMN_SIZE> values{};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Board {
|
|
||||||
std::array<Stack, MAX_ROW_SIZE> field{};
|
|
||||||
std::array<BunkerField, 3> bunker{};
|
|
||||||
Goal goal{};
|
|
||||||
bool flower_gone{};
|
|
||||||
|
|
||||||
[[nodiscard]] auto solved() const noexcept -> bool;
|
|
||||||
[[nodiscard]] auto hash() const noexcept -> std::size_t;
|
|
||||||
[[nodiscard]] auto correct() const noexcept -> bool;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace solitaire
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <cassert>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace solitaire {
|
|
||||||
enum class CardType : int { Zhong, Bai, Fa, Hua, Red, Green, Black };
|
|
||||||
|
|
||||||
auto isNormalCardType(CardType type) -> bool;
|
|
||||||
|
|
||||||
struct Card {
|
|
||||||
CardType type;
|
|
||||||
int value;
|
|
||||||
[[nodiscard]] auto toString() const noexcept -> std::string;
|
|
||||||
};
|
|
||||||
} // namespace solitaire
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
|
|
||||||
#pragma once
|
|
||||||
#include "card.hpp"
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
namespace solitaire {
|
|
||||||
class Goal {
|
|
||||||
std::array<std::optional<Card>, 3> goal{};
|
|
||||||
auto getEmptyId() -> std::optional<int>;
|
|
||||||
|
|
||||||
public:
|
|
||||||
[[nodiscard]] auto getId(CardType suit) const noexcept -> std::optional<int>;
|
|
||||||
[[nodiscard]] auto get(CardType suit) const noexcept -> std::optional<int>;
|
|
||||||
|
|
||||||
void set(CardType suit, int value) noexcept;
|
|
||||||
void inc(CardType suit) noexcept;
|
|
||||||
};
|
|
||||||
} // namespace solitaire
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
#include "board.hpp"
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#include "card.hpp"
|
|
||||||
namespace solitaire {
|
|
||||||
auto
|
|
||||||
isNormalCardType(CardType type) -> bool {
|
|
||||||
switch (type) {
|
|
||||||
case CardType::Red:
|
|
||||||
case CardType::Green:
|
|
||||||
case CardType::Black:
|
|
||||||
return true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace solitaire
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
|
|
||||||
#include "goal.hpp"
|
|
||||||
|
|
||||||
namespace solitaire {
|
|
||||||
auto
|
|
||||||
Goal::getEmptyId() -> std::optional<int> {
|
|
||||||
int counter = 0;
|
|
||||||
for (const auto& slot : goal) {
|
|
||||||
if (!slot) {
|
|
||||||
return counter;
|
|
||||||
}
|
|
||||||
++counter;
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] auto
|
|
||||||
Goal::getId(CardType suit) const noexcept -> std::optional<int> {
|
|
||||||
int counter = 0;
|
|
||||||
for (const auto& slot : goal) {
|
|
||||||
if (slot && slot->type == suit) {
|
|
||||||
return counter;
|
|
||||||
}
|
|
||||||
++counter;
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] auto
|
|
||||||
Goal::get(CardType suit) const noexcept -> std::optional<int> {
|
|
||||||
if (auto index = getId(suit); index) {
|
|
||||||
return goal[*index]->value;
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Goal::set(CardType suit, int value) noexcept {
|
|
||||||
assert(value >= 0);
|
|
||||||
assert(value <= 9);
|
|
||||||
|
|
||||||
const auto card = [&]() -> std::optional<Card> {
|
|
||||||
if (value == 0) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return Card{suit, value};
|
|
||||||
}();
|
|
||||||
|
|
||||||
const int goal_index = [&]() -> int {
|
|
||||||
if (auto index = getId(suit); index) {
|
|
||||||
return *index;
|
|
||||||
}
|
|
||||||
return *getEmptyId();
|
|
||||||
}();
|
|
||||||
|
|
||||||
goal[goal_index] = card;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Goal::inc(CardType suit) noexcept {
|
|
||||||
auto get_value = get(suit);
|
|
||||||
int new_value = get_value ? (*get_value) + 1 : 1;
|
|
||||||
set(suit, new_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace solitaire
|
|
||||||
@@ -6,6 +6,7 @@ from typing import Optional, Tuple
|
|||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy
|
import numpy
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -16,8 +17,8 @@ class Adjustment:
|
|||||||
y: int = 0
|
y: int = 0
|
||||||
w: int = 0
|
w: int = 0
|
||||||
h: int = 0
|
h: int = 0
|
||||||
dx: int = 0
|
dx: float = 0
|
||||||
dy: int = 0
|
dy: float = 0
|
||||||
|
|
||||||
|
|
||||||
def get_square(
|
def get_square(
|
||||||
@@ -25,10 +26,10 @@ def get_square(
|
|||||||
) -> Tuple[int, int, int, int]:
|
) -> Tuple[int, int, int, int]:
|
||||||
"""Get one square from index and adjustment"""
|
"""Get one square from index and adjustment"""
|
||||||
return (
|
return (
|
||||||
adjustment.x + adjustment.dx * index_x,
|
math.floor(adjustment.x + adjustment.dx * index_x),
|
||||||
adjustment.y + adjustment.dy * index_y,
|
math.floor(adjustment.y + adjustment.dy * index_y),
|
||||||
adjustment.x + adjustment.w + adjustment.dx * index_x,
|
math.floor(adjustment.x + adjustment.w + adjustment.dx * index_x),
|
||||||
adjustment.y + adjustment.h + adjustment.dy * index_y,
|
math.floor(adjustment.y + adjustment.h + adjustment.dy * index_y),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -41,61 +42,53 @@ def adjust_squares(
|
|||||||
|
|
||||||
if not adjustment:
|
if not adjustment:
|
||||||
adjustment = Adjustment(w=10, h=10)
|
adjustment = Adjustment(w=10, h=10)
|
||||||
|
speed_mod = "n"
|
||||||
|
speed_mods = ["n", "s", "h"]
|
||||||
|
|
||||||
def _adjustment_step(keycode: int) -> None:
|
def _adjustment_step(keycode: int, speed_mod: str) -> None:
|
||||||
assert adjustment is not None
|
assert adjustment is not None
|
||||||
x_keys = {81: -1, 83: +1, 104: -10, 115: +10}
|
x_keys = {104: -1, 115: +1}
|
||||||
y_keys = {82: -1, 84: +1, 116: -10, 110: +10}
|
y_keys = {116: -1, 110: +1}
|
||||||
w_keys = {97: -1, 117: +1}
|
w_keys = {97: -1, 117: +1}
|
||||||
h_keys = {111: -1, 101: +1}
|
h_keys = {111: -1, 101: +1}
|
||||||
dx_keys = {59: -1, 112: +1}
|
dx_keys = {59: -1, 112: +1}
|
||||||
dy_keys = {44: -1, 46: +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:
|
if keycode in x_keys:
|
||||||
adjustment.x += x_keys[keycode]
|
adjustment.x += x_keys[keycode] * cur_high_speed_fac
|
||||||
elif keycode in y_keys:
|
elif keycode in y_keys:
|
||||||
adjustment.y += y_keys[keycode]
|
adjustment.y += y_keys[keycode] * cur_high_speed_fac
|
||||||
elif keycode in w_keys:
|
elif keycode in w_keys:
|
||||||
adjustment.w += w_keys[keycode]
|
adjustment.w += w_keys[keycode] * cur_high_speed_fac
|
||||||
elif keycode in h_keys:
|
elif keycode in h_keys:
|
||||||
adjustment.h += h_keys[keycode]
|
adjustment.h += h_keys[keycode] * cur_high_speed_fac
|
||||||
elif keycode in dx_keys:
|
elif keycode in dx_keys:
|
||||||
adjustment.dx += dx_keys[keycode]
|
adjustment.dx += dx_keys[keycode] * cur_high_speed_fac * 1 / 8
|
||||||
elif keycode in dy_keys:
|
elif keycode in dy_keys:
|
||||||
adjustment.dy += dy_keys[keycode]
|
adjustment.dy += dy_keys[keycode] * cur_high_speed_fac * 1 / 8
|
||||||
|
|
||||||
|
cv2.namedWindow("Window", flags=cv2.WINDOW_NORMAL)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
working_image = image.copy()
|
working_image = image.copy()
|
||||||
for index_x, index_y in itertools.product(range(count_x), range(count_y)):
|
for index_x, index_y in itertools.product(range(count_x), range(count_y)):
|
||||||
square = get_square(adjustment, index_x, index_y)
|
square = get_square(adjustment, index_x, index_y)
|
||||||
cv2.rectangle(
|
cv2.rectangle(
|
||||||
working_image, (square[0], square[1]), (square[2], square[3]), (0, 0, 0)
|
working_image,
|
||||||
|
(math.floor(square[0]), math.floor(square[1])),
|
||||||
|
(math.floor(square[2]), math.floor(square[3])),
|
||||||
|
(0, 0, 0),
|
||||||
)
|
)
|
||||||
cv2.imshow("Window", working_image)
|
cv2.imshow("Window", working_image)
|
||||||
keycode = cv2.waitKey(0)
|
keycode = cv2.waitKey(0)
|
||||||
print(keycode)
|
print(keycode)
|
||||||
if keycode == 27:
|
if keycode == 27:
|
||||||
break
|
break
|
||||||
_adjustment_step(keycode)
|
if keycode == 229:
|
||||||
|
speed_mod = speed_mods[(speed_mods.index(speed_mod) + 1) % len(speed_mods)]
|
||||||
|
continue
|
||||||
|
_adjustment_step(keycode, speed_mod)
|
||||||
|
|
||||||
cv2.destroyWindow("Window")
|
cv2.destroyWindow("Window")
|
||||||
return adjustment
|
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,6 +6,7 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
|||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import json
|
||||||
|
|
||||||
from ..board import Board, Card, NumberCard, SpecialCard
|
from ..board import Board, Card, NumberCard, SpecialCard
|
||||||
from . import adjustment, card_finder
|
from . import adjustment, card_finder
|
||||||
@@ -35,7 +36,6 @@ def get_field_square_iterator(
|
|||||||
"""Return iterator for both the square, as well as the matching card border"""
|
"""Return iterator for both the square, as well as the matching card border"""
|
||||||
my_adj = fake_adjustment(conf.field_adjustment)
|
my_adj = fake_adjustment(conf.field_adjustment)
|
||||||
my_border_adj = fake_adjustment(conf.border_adjustment)
|
my_border_adj = fake_adjustment(conf.border_adjustment)
|
||||||
|
|
||||||
squares = card_finder.get_field_squares(
|
squares = card_finder.get_field_squares(
|
||||||
image, my_adj, count_x=row_count, count_y=column_count
|
image, my_adj, count_x=row_count, count_y=column_count
|
||||||
)
|
)
|
||||||
@@ -49,7 +49,6 @@ def get_field_square_iterator(
|
|||||||
|
|
||||||
def match_template(template: np.ndarray, search_image: np.ndarray) -> float:
|
def match_template(template: np.ndarray, search_image: np.ndarray) -> float:
|
||||||
"""Return matchiness for the template on the search image"""
|
"""Return matchiness for the template on the search image"""
|
||||||
|
|
||||||
res = cv2.matchTemplate(search_image, template, cv2.TM_CCOEFF_NORMED)
|
res = cv2.matchTemplate(search_image, template, cv2.TM_CCOEFF_NORMED)
|
||||||
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
|
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
|
||||||
assert isinstance(max_val, (int, float))
|
assert isinstance(max_val, (int, float))
|
||||||
@@ -63,7 +62,6 @@ def parse_field_square(
|
|||||||
(match_template(template, square), name) for template, name in conf.catalogue
|
(match_template(template, square), name) for template, name in conf.catalogue
|
||||||
]
|
]
|
||||||
best_val, best_name = max(square_fits, key=lambda x: x[0])
|
best_val, best_name = max(square_fits, key=lambda x: x[0])
|
||||||
|
|
||||||
best_border = max(
|
best_border = max(
|
||||||
match_template(template=template, search_image=border)
|
match_template(template=template, search_image=border)
|
||||||
for template in conf.card_border
|
for template in conf.card_border
|
||||||
@@ -225,8 +223,16 @@ def parse_goal(image: np.ndarray, conf: Configuration) -> List[Optional[NumberCa
|
|||||||
|
|
||||||
def parse_board(image: np.ndarray, conf: Configuration) -> Board:
|
def parse_board(image: np.ndarray, conf: Configuration) -> Board:
|
||||||
result = Board()
|
result = Board()
|
||||||
result.setField(parse_field(image, conf))
|
result.field = parse_field(image, conf)
|
||||||
result.setGoal(parse_goal(image, conf))
|
|
||||||
result.flower_gone = parse_hua(image, conf)
|
result.flower_gone = parse_hua(image, conf)
|
||||||
result.bunker = parse_bunker(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
|
return result
|
||||||
@@ -32,7 +32,6 @@ def get_field_squares(
|
|||||||
def catalogue_cards(squares: List[np.ndarray]) -> List[Tuple[np.ndarray, Card]]:
|
def catalogue_cards(squares: List[np.ndarray]) -> List[Tuple[np.ndarray, Card]]:
|
||||||
"""Run manual cataloging for given squares"""
|
"""Run manual cataloging for given squares"""
|
||||||
cv2.namedWindow("Catalogue", cv2.WINDOW_NORMAL)
|
cv2.namedWindow("Catalogue", cv2.WINDOW_NORMAL)
|
||||||
cv2.waitKey(1)
|
|
||||||
result: List[Tuple[np.ndarray, Card]] = []
|
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("Card ID is [B]ai, [Z]hong, [F]a, [H]ua, [R]ed, [G]reen, [B]lack")
|
||||||
print("Numbercard e.g. R3")
|
print("Numbercard e.g. R3")
|
||||||
@@ -51,7 +50,7 @@ def catalogue_cards(squares: List[np.ndarray]) -> List[Tuple[np.ndarray, Card]]:
|
|||||||
for square in squares:
|
for square in squares:
|
||||||
while True:
|
while True:
|
||||||
cv2.imshow("Catalogue", cv2.resize(square, (500, 500)))
|
cv2.imshow("Catalogue", cv2.resize(square, (500, 500)))
|
||||||
cv2.waitKey(1)
|
cv2.waitKey(100)
|
||||||
card_id = input("Card ID:").lower()
|
card_id = input("Card ID:").lower()
|
||||||
card_type: Optional[Card] = None
|
card_type: Optional[Card] = None
|
||||||
if len(card_id) == 1:
|
if len(card_id) == 1:
|
||||||
|
|||||||
@@ -99,9 +99,64 @@ def _save_adjustments(zip_file: zipfile.ZipFile, conf: Configuration) -> None:
|
|||||||
adjustments = {}
|
adjustments = {}
|
||||||
adjustments[FIELD_ADJUSTMENT_KEY] = dataclasses.asdict(conf.field_adjustment)
|
adjustments[FIELD_ADJUSTMENT_KEY] = dataclasses.asdict(conf.field_adjustment)
|
||||||
adjustments[BORDER_ADJUSTMENT_KEY] = dataclasses.asdict(conf.border_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(
|
zip_file.writestr(
|
||||||
ADJUSTMENT_FILE_NAME, json.dumps(adjustment),
|
ADJUSTMENT_FILE_NAME, json.dumps(adjustments),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _save_special_images(zip_file: zipfile.ZipFile, conf: Configuration) -> None:
|
||||||
|
def _save_special_image(
|
||||||
|
zip_file: zipfile.ZipFile, images: List[np.ndarray], directory: str
|
||||||
|
) -> None:
|
||||||
|
for index, image in enumerate(images):
|
||||||
|
fd, myfile = tempfile.mkstemp(suffix=f".{PICTURE_EXTENSION}")
|
||||||
|
cv2.imwrite(myfile, image)
|
||||||
|
file_name = ""
|
||||||
|
zip_file.write(
|
||||||
|
myfile, arcname=f"{directory}/{index:03}.{PICTURE_EXTENSION}"
|
||||||
|
)
|
||||||
|
|
||||||
|
_save_special_image(zip_file, conf.card_border, CARD_BORDER_DIRECTORY)
|
||||||
|
_save_special_image(zip_file, conf.empty_card, EMPTY_CARD_DIRECTORY)
|
||||||
|
_save_special_image(zip_file, conf.green_card, GREEN_CARD_DIRECTORY)
|
||||||
|
_save_special_image(zip_file, conf.card_back, CARD_BACK_DIRECTORY)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_special_button_filename(
|
||||||
|
state: ButtonState, special_card: board.SpecialCard
|
||||||
|
) -> str:
|
||||||
|
state_char_map = {
|
||||||
|
ButtonState.normal: "n",
|
||||||
|
ButtonState.greyed: "g",
|
||||||
|
ButtonState.shiny: "s",
|
||||||
|
}
|
||||||
|
special_card_char_map = {
|
||||||
|
board.SpecialCard.Fa: "f",
|
||||||
|
board.SpecialCard.Zhong: "z",
|
||||||
|
board.SpecialCard.Bai: "b",
|
||||||
|
}
|
||||||
|
return f"{state_char_map[state]}{special_card_char_map[special_card]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _save_special_button_images(
|
||||||
|
zip_file: zipfile.ZipFile,
|
||||||
|
special_button_images: List[Tuple[ButtonState, board.SpecialCard, np.ndarray]],
|
||||||
|
):
|
||||||
|
for index, (state, card, image) in enumerate(special_button_images):
|
||||||
|
fd, myfile = tempfile.mkstemp(suffix=f".{PICTURE_EXTENSION}")
|
||||||
|
cv2.imwrite(myfile, image)
|
||||||
|
file_name = ""
|
||||||
|
zip_file.write(
|
||||||
|
myfile,
|
||||||
|
arcname=f"{SPECIAL_BUTTON_DIRECTORY}/"
|
||||||
|
f"{_generate_special_button_filename(state,card)}"
|
||||||
|
f"{index:03}.{PICTURE_EXTENSION}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -112,7 +167,8 @@ def save(conf: Configuration, filename: str) -> None:
|
|||||||
with zipfile.ZipFile(zip_stream, "w") as zip_file:
|
with zipfile.ZipFile(zip_stream, "w") as zip_file:
|
||||||
_save_adjustments(zip_file, conf)
|
_save_adjustments(zip_file, conf)
|
||||||
_save_catalogue(zip_file, conf.catalogue)
|
_save_catalogue(zip_file, conf.catalogue)
|
||||||
# TODO: Save card_borders and emtpy_card and green_card and special_buttons and card_back
|
_save_special_images(zip_file, conf)
|
||||||
|
_save_special_button_images(zip_file, conf.special_buttons)
|
||||||
with open(filename, "wb") as zip_archive:
|
with open(filename, "wb") as zip_archive:
|
||||||
zip_archive.write(zip_stream.getvalue())
|
zip_archive.write(zip_stream.getvalue())
|
||||||
|
|
||||||
|
|||||||
@@ -1,139 +1,173 @@
|
|||||||
import time
|
import time
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple, Dict, Any, Union
|
||||||
|
|
||||||
import pyautogui
|
import pyautogui
|
||||||
import shenzhen_solitaire.board as board
|
import shenzhen_solitaire.board as board
|
||||||
import shenzhen_solitaire.card_detection.adjustment as adjustment
|
import shenzhen_solitaire.card_detection.adjustment as adjustment
|
||||||
import shenzhen_solitaire.card_detection.configuration as configuration
|
import shenzhen_solitaire.card_detection.configuration as configuration
|
||||||
import shenzhen_solitaire.solver.board_actions as board_actions
|
|
||||||
import warnings
|
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(
|
def drag(
|
||||||
src: Tuple[int, int], dst: Tuple[int, int], offset: Tuple[int, int] = (0, 0)
|
src: Tuple[int, int], dst: Tuple[int, int], offset: Tuple[int, int] = (0, 0)
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
time.sleep(DRAG_DURATION / 3)
|
||||||
pyautogui.moveTo(x=src[0] + offset[0], y=src[1] + offset[1])
|
pyautogui.moveTo(x=src[0] + offset[0], y=src[1] + offset[1])
|
||||||
pyautogui.dragTo(
|
pyautogui.mouseDown()
|
||||||
x=dst[0] + offset[0],
|
time.sleep(DRAG_DURATION / 3)
|
||||||
y=dst[1] + offset[1],
|
pyautogui.moveTo(
|
||||||
duration=0.4,
|
x=dst[0] + offset[0], y=dst[1] + offset[1],
|
||||||
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:
|
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.moveTo(x=point[0] + offset[0], y=point[1] + offset[1])
|
||||||
pyautogui.mouseDown()
|
pyautogui.mouseDown()
|
||||||
time.sleep(0.2)
|
time.sleep(CLICK_DURATION / 3)
|
||||||
pyautogui.mouseUp()
|
pyautogui.mouseUp()
|
||||||
|
time.sleep(CLICK_DURATION / 3)
|
||||||
|
time.sleep(DRAGON_WAIT)
|
||||||
|
|
||||||
|
|
||||||
def clickSquare(
|
@dataclass
|
||||||
field: Tuple[int, int, int, int], offset: Tuple[int, int] = (0, 0)
|
class DragAction:
|
||||||
) -> None:
|
source: Tuple[int, int]
|
||||||
click(
|
destination: Tuple[int, int]
|
||||||
(field[0] + (field[2] - field[0]) // 2, field[1] + (field[3] - field[1]) // 2),
|
|
||||||
offset,
|
|
||||||
|
@dataclass
|
||||||
|
class ClickAction:
|
||||||
|
destination: Tuple[int, int]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WaitAction:
|
||||||
|
duration: float
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_field(
|
||||||
|
field: Dict[str, Any], conf: configuration.Configuration
|
||||||
|
) -> Tuple[int, int]:
|
||||||
|
return (
|
||||||
|
int(field["column"]) * conf.field_adjustment.dx
|
||||||
|
+ conf.field_adjustment.x
|
||||||
|
+ conf.field_adjustment.w // 2,
|
||||||
|
int(field["row"]) * conf.field_adjustment.dy
|
||||||
|
+ conf.field_adjustment.y
|
||||||
|
+ conf.field_adjustment.h // 2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def handle_action(
|
def parse_action(
|
||||||
action: board_actions.Action,
|
action: Dict[str, Any],
|
||||||
offset: Tuple[int, int],
|
|
||||||
conf: configuration.Configuration,
|
conf: configuration.Configuration,
|
||||||
) -> None:
|
goal_values: Dict[str, int],
|
||||||
if isinstance(action, board_actions.MoveAction):
|
) -> Union[DragAction, ClickAction, WaitAction]:
|
||||||
src = adjustment.get_square(
|
assert len(action) == 1
|
||||||
conf.field_adjustment,
|
action_name, info = next(iter(action.items()))
|
||||||
index_x=action.source_id,
|
action_name = action_name.lower()
|
||||||
index_y=action.source_row_index,
|
if action_name == "bunkerize":
|
||||||
|
field = _parse_field(info["field_position"], conf)
|
||||||
|
bunker = (
|
||||||
|
int(info["bunker_slot_index"]) * conf.bunker_adjustment.dx
|
||||||
|
+ conf.bunker_adjustment.x
|
||||||
|
+ conf.bunker_adjustment.w // 2,
|
||||||
|
conf.bunker_adjustment.y + conf.bunker_adjustment.h // 2,
|
||||||
)
|
)
|
||||||
dst = adjustment.get_square(
|
if str(info["to_bunker"]).lower() == "true":
|
||||||
conf.field_adjustment,
|
return DragAction(source=field, destination=bunker)
|
||||||
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:
|
else:
|
||||||
dragSquare(bunker, field, offset)
|
return DragAction(source=bunker, destination=field)
|
||||||
return
|
elif action_name == "move":
|
||||||
if isinstance(action, board_actions.DragonKillAction):
|
return DragAction(
|
||||||
dragon_sequence = [
|
source=_parse_field(info["source"], conf),
|
||||||
board.SpecialCard.Zhong,
|
destination=_parse_field(info["destination"], conf),
|
||||||
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(
|
elif action_name == "dragonkill":
|
||||||
field, offset,
|
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,
|
||||||
)
|
)
|
||||||
time.sleep(1)
|
|
||||||
return
|
|
||||||
if isinstance(action, board_actions.GoalAction):
|
|
||||||
dst = adjustment.get_square(
|
|
||||||
conf.goal_adjustment, index_x=action.goal_id, index_y=0,
|
|
||||||
)
|
|
||||||
if action.source_position == board.Position.Field:
|
|
||||||
assert action.source_row_index is not None
|
|
||||||
src = adjustment.get_square(
|
|
||||||
conf.field_adjustment,
|
|
||||||
index_x=action.source_id,
|
|
||||||
index_y=action.source_row_index,
|
|
||||||
)
|
)
|
||||||
|
elif action_name == "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:
|
else:
|
||||||
assert action.source_position == board.Position.Bunker
|
obvious = False
|
||||||
src = adjustment.get_square(
|
|
||||||
conf.bunker_adjustment, index_x=action.source_id, index_y=0,
|
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,
|
||||||
)
|
)
|
||||||
dragSquare(src, dst, offset)
|
if "Field" in info["source"]:
|
||||||
return
|
source = _parse_field(info["source"]["Field"], conf)
|
||||||
raise AssertionError("You forgot an Action type")
|
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(
|
def handle_actions(
|
||||||
actions: List[board_actions.Action],
|
actions: List[Dict[str, Dict[str, Any]]],
|
||||||
offset: Tuple[int, int],
|
offset: Tuple[int, int],
|
||||||
conf: configuration.Configuration,
|
conf: configuration.Configuration,
|
||||||
) -> None:
|
) -> None:
|
||||||
automatic_count = 0
|
goal_values = {"red": 0, "black": 0, "green": 0}
|
||||||
for action in actions:
|
action_tuples = (
|
||||||
print(action)
|
(action, parse_action(action, conf, goal_values)) for action in actions
|
||||||
if isinstance(action, board_actions.HuaKillAction):
|
)
|
||||||
automatic_count += 1
|
for name, action in action_tuples:
|
||||||
else:
|
print(name)
|
||||||
time.sleep(0.5 * automatic_count)
|
if isinstance(action, DragAction):
|
||||||
automatic_count = 0
|
drag(action.source, action.destination, offset)
|
||||||
handle_action(action, offset, conf)
|
elif isinstance(action, ClickAction):
|
||||||
time.sleep(0.5 * automatic_count)
|
click(action.destination, offset)
|
||||||
|
elif isinstance(action, WaitAction):
|
||||||
|
time.sleep(action.duration)
|
||||||
|
|||||||
@@ -1,217 +0,0 @@
|
|||||||
"""Contains actions that can be used on the board"""
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
|
|
||||||
from .. import board
|
|
||||||
|
|
||||||
|
|
||||||
class Action:
|
|
||||||
"""Base class for a card move action on a solitaire board"""
|
|
||||||
|
|
||||||
_before_state: int = 0
|
|
||||||
_after_state: int = 0
|
|
||||||
|
|
||||||
def _apply(self, action_board: board.Board) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _undo(self, action_board: board.Board) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def apply(self, action_board: board.Board) -> None:
|
|
||||||
"""Apply action to board"""
|
|
||||||
if __debug__:
|
|
||||||
self._before_state = action_board.state_identifier
|
|
||||||
self._apply(action_board)
|
|
||||||
if __debug__:
|
|
||||||
self._after_state = action_board.state_identifier
|
|
||||||
|
|
||||||
def undo(self, action_board: board.Board) -> None:
|
|
||||||
"""Undo action to board"""
|
|
||||||
assert action_board.state_identifier == self._after_state
|
|
||||||
self._undo(action_board)
|
|
||||||
assert action_board.state_identifier == self._before_state
|
|
||||||
|
|
||||||
def automatic(self) -> bool:
|
|
||||||
if isinstance(self, HuaKillAction):
|
|
||||||
return True
|
|
||||||
if isinstance(self, GoalAction) and self.obvious:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class GoalAction(Action):
|
|
||||||
"""Move card from field to goal"""
|
|
||||||
|
|
||||||
card: board.NumberCard
|
|
||||||
source_id: int
|
|
||||||
source_row_index: Optional[int]
|
|
||||||
source_position: board.Position
|
|
||||||
goal_id: int
|
|
||||||
obvious: bool
|
|
||||||
|
|
||||||
def _apply(self, action_board: board.Board) -> None:
|
|
||||||
"""Do action"""
|
|
||||||
assert action_board.getGoalId(self.card.suit) == self.goal_id
|
|
||||||
assert action_board.getGoal(self.card.suit) + 1 == self.card.number
|
|
||||||
if self.source_position == board.Position.Field:
|
|
||||||
assert action_board.field[self.source_id][-1] == self.card
|
|
||||||
action_board.field[self.source_id].pop()
|
|
||||||
action_board.incGoal(self.card.suit)
|
|
||||||
elif self.source_position == board.Position.Bunker:
|
|
||||||
assert action_board.bunker[self.source_id] == self.card
|
|
||||||
action_board.bunker[self.source_id] = None
|
|
||||||
action_board.incGoal(self.card.suit)
|
|
||||||
else:
|
|
||||||
raise RuntimeError("Unknown position")
|
|
||||||
|
|
||||||
def _undo(self, action_board: board.Board) -> None:
|
|
||||||
"""Undo action"""
|
|
||||||
assert action_board.getGoalId(self.card.suit) == self.goal_id
|
|
||||||
assert action_board.getGoal(self.card.suit) == self.card.number
|
|
||||||
if self.source_position == board.Position.Field:
|
|
||||||
action_board.field[self.source_id].append(self.card)
|
|
||||||
elif self.source_position == board.Position.Bunker:
|
|
||||||
assert action_board.bunker[self.source_id] is None
|
|
||||||
action_board.bunker[self.source_id] = self.card
|
|
||||||
else:
|
|
||||||
raise RuntimeError("Unknown position")
|
|
||||||
action_board.setGoal(self.card.suit, action_board.getGoal(self.card.suit) - 1)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BunkerizeAction(Action):
|
|
||||||
"""Move card from bunker to field"""
|
|
||||||
|
|
||||||
card: board.Card
|
|
||||||
bunker_id: int
|
|
||||||
field_id: int
|
|
||||||
field_row_index: int
|
|
||||||
to_bunker: bool
|
|
||||||
|
|
||||||
def _move_from_bunker(self, action_board: board.Board) -> None:
|
|
||||||
assert action_board.bunker[self.bunker_id] == self.card
|
|
||||||
action_board.bunker[self.bunker_id] = None
|
|
||||||
action_board.field[self.field_id].append(self.card)
|
|
||||||
|
|
||||||
def _move_to_bunker(self, action_board: board.Board) -> None:
|
|
||||||
assert action_board.field[self.field_id][-1] == self.card
|
|
||||||
assert action_board.bunker[self.bunker_id] is None
|
|
||||||
action_board.bunker[self.bunker_id] = self.card
|
|
||||||
action_board.field[self.field_id].pop()
|
|
||||||
|
|
||||||
def _apply(self, action_board: board.Board) -> None:
|
|
||||||
"""Do action"""
|
|
||||||
if self.to_bunker:
|
|
||||||
self._move_to_bunker(action_board)
|
|
||||||
else:
|
|
||||||
self._move_from_bunker(action_board)
|
|
||||||
|
|
||||||
def _undo(self, action_board: board.Board) -> None:
|
|
||||||
"""Undo action"""
|
|
||||||
if self.to_bunker:
|
|
||||||
self._move_from_bunker(action_board)
|
|
||||||
else:
|
|
||||||
self._move_to_bunker(action_board)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MoveAction(Action):
|
|
||||||
"""Moving a card from one field stack to another"""
|
|
||||||
|
|
||||||
cards: List[board.Card]
|
|
||||||
source_id: int
|
|
||||||
source_row_index: int
|
|
||||||
destination_id: int
|
|
||||||
destination_row_index: int
|
|
||||||
|
|
||||||
def _shift(self, action_board: board.Board, source: int, dest: int) -> None:
|
|
||||||
"""Shift a card from the field id 'source' to field id 'dest'"""
|
|
||||||
|
|
||||||
for stack_offset, card in enumerate(self.cards, start=-len(self.cards)):
|
|
||||||
assert action_board.getField()[source][stack_offset] == card
|
|
||||||
|
|
||||||
action_board.getField()[source] = action_board.getField()[source][: -len(self.cards)]
|
|
||||||
action_board.getField()[dest].extend(self.cards)
|
|
||||||
|
|
||||||
def _apply(self, action_board: board.Board) -> None:
|
|
||||||
"""Do action"""
|
|
||||||
if action_board.getField()[self.destination_id]:
|
|
||||||
dest_card = action_board.getField()[self.destination_id][-1]
|
|
||||||
if not all(isinstance(x, board.NumberCard) for x in self.cards):
|
|
||||||
raise AssertionError()
|
|
||||||
if not isinstance(dest_card, board.NumberCard):
|
|
||||||
raise AssertionError()
|
|
||||||
if not isinstance(self.cards[0], board.NumberCard):
|
|
||||||
raise AssertionError()
|
|
||||||
if dest_card.suit == self.cards[0].suit:
|
|
||||||
raise AssertionError()
|
|
||||||
if dest_card.number != self.cards[0].number + 1:
|
|
||||||
raise AssertionError()
|
|
||||||
self._shift(action_board, self.source_id, self.destination_id)
|
|
||||||
|
|
||||||
def _undo(self, action_board: board.Board) -> None:
|
|
||||||
"""Undo action"""
|
|
||||||
self._shift(action_board, self.destination_id, self.source_id)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DragonKillAction(Action):
|
|
||||||
"""Removing four dragons from the top of the stacks to a bunker"""
|
|
||||||
|
|
||||||
dragon: board.SpecialCard
|
|
||||||
source_stacks: List[Tuple[board.Position, int]]
|
|
||||||
destination_bunker_id: int
|
|
||||||
|
|
||||||
def _apply(self, action_board: board.Board) -> None:
|
|
||||||
"""Do action"""
|
|
||||||
assert (
|
|
||||||
action_board.bunker[self.destination_bunker_id] is None
|
|
||||||
or action_board.bunker[self.destination_bunker_id] == self.dragon
|
|
||||||
)
|
|
||||||
assert len(self.source_stacks) == 4
|
|
||||||
for position, index in self.source_stacks:
|
|
||||||
if position == board.Position.Field:
|
|
||||||
assert action_board.field[index]
|
|
||||||
assert action_board.field[index][-1] == self.dragon
|
|
||||||
action_board.field[index].pop()
|
|
||||||
elif position == board.Position.Bunker:
|
|
||||||
assert action_board.bunker[index] == self.dragon
|
|
||||||
action_board.bunker[index] = None
|
|
||||||
else:
|
|
||||||
raise RuntimeError("Can only kill dragons in field and bunker")
|
|
||||||
action_board.bunker[self.destination_bunker_id] = (self.dragon, 4)
|
|
||||||
|
|
||||||
def _undo(self, action_board: board.Board) -> None:
|
|
||||||
"""Undo action"""
|
|
||||||
assert action_board.bunker[self.destination_bunker_id] == (self.dragon, 4)
|
|
||||||
assert len(self.source_stacks) == 4
|
|
||||||
action_board.bunker[self.destination_bunker_id] = None
|
|
||||||
for position, index in self.source_stacks:
|
|
||||||
if position == board.Position.Field:
|
|
||||||
action_board.field[index].append(self.dragon)
|
|
||||||
elif position == board.Position.Bunker:
|
|
||||||
action_board.bunker[index] = self.dragon
|
|
||||||
else:
|
|
||||||
raise RuntimeError("Can only kill dragons in field and bunker")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HuaKillAction(Action):
|
|
||||||
"""Remove the flower card"""
|
|
||||||
|
|
||||||
source_field_id: int
|
|
||||||
source_field_row_index: int
|
|
||||||
|
|
||||||
def _apply(self, action_board: board.Board) -> None:
|
|
||||||
"""Do action"""
|
|
||||||
assert not action_board.flower_gone
|
|
||||||
assert action_board.field[self.source_field_id][-1] == board.SpecialCard.Hua
|
|
||||||
action_board.field[self.source_field_id].pop()
|
|
||||||
action_board.flower_gone = True
|
|
||||||
|
|
||||||
def _undo(self, action_board: board.Board) -> None:
|
|
||||||
"""Undo action"""
|
|
||||||
assert action_board.flower_gone
|
|
||||||
action_board.field[self.source_field_id].append(board.SpecialCard.Hua)
|
|
||||||
action_board.flower_gone = False
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
"""Contains function to iterate different kinds of possible actions"""
|
|
||||||
from typing import Iterator, List, Tuple
|
|
||||||
|
|
||||||
from .. import board
|
|
||||||
from . import board_actions
|
|
||||||
|
|
||||||
|
|
||||||
def possible_huakill_action(
|
|
||||||
search_board: board.Board,
|
|
||||||
) -> Iterator[board_actions.HuaKillAction]:
|
|
||||||
"""Check if the flowercard can be eliminated"""
|
|
||||||
for index, stack in enumerate(search_board.getField()):
|
|
||||||
if stack and stack[-1] == board.SpecialCard.Hua:
|
|
||||||
yield board_actions.HuaKillAction(
|
|
||||||
source_field_id=index, source_field_row_index=len(stack) - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def possible_dragonkill_actions(
|
|
||||||
search_board: board.Board,
|
|
||||||
) -> Iterator[board_actions.DragonKillAction]:
|
|
||||||
"""Enumerate all possible dragon kills"""
|
|
||||||
possible_dragons = [
|
|
||||||
board.SpecialCard.Zhong,
|
|
||||||
board.SpecialCard.Fa,
|
|
||||||
board.SpecialCard.Bai,
|
|
||||||
]
|
|
||||||
if not any(x is None for x in search_board.getBunker()):
|
|
||||||
new_possible_dragons = []
|
|
||||||
for dragon in possible_dragons:
|
|
||||||
if any(x == dragon for x in search_board.getBunker()):
|
|
||||||
new_possible_dragons.append(dragon)
|
|
||||||
possible_dragons = new_possible_dragons
|
|
||||||
|
|
||||||
for dragon in possible_dragons:
|
|
||||||
bunker_dragons = [i for i, d in enumerate(search_board.getBunker()) if d == dragon]
|
|
||||||
field_dragons = [
|
|
||||||
i for i, f in enumerate(search_board.getField()) if f if f[-1] == dragon
|
|
||||||
]
|
|
||||||
if len(bunker_dragons) + len(field_dragons) != 4:
|
|
||||||
continue
|
|
||||||
destination_bunker_id = 0
|
|
||||||
if bunker_dragons:
|
|
||||||
destination_bunker_id = bunker_dragons[0]
|
|
||||||
else:
|
|
||||||
destination_bunker_id = [
|
|
||||||
i for i, x in enumerate(search_board.getBunker()) if x is None
|
|
||||||
][0]
|
|
||||||
|
|
||||||
source_stacks = [(board.Position.Bunker, i) for i in bunker_dragons]
|
|
||||||
source_stacks.extend([(board.Position.Field, i) for i in field_dragons])
|
|
||||||
|
|
||||||
yield board_actions.DragonKillAction(
|
|
||||||
dragon=dragon,
|
|
||||||
source_stacks=source_stacks,
|
|
||||||
destination_bunker_id=destination_bunker_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def possible_bunkerize_actions(
|
|
||||||
search_board: board.Board,
|
|
||||||
) -> Iterator[board_actions.BunkerizeAction]:
|
|
||||||
"""Enumerates all possible card moves from the field to the bunker"""
|
|
||||||
open_bunker_list = [i for i, x in enumerate(search_board.getBunker()) if x is None]
|
|
||||||
|
|
||||||
if not open_bunker_list:
|
|
||||||
return
|
|
||||||
|
|
||||||
open_bunker = open_bunker_list[0]
|
|
||||||
for index, stack in enumerate(search_board.getField()):
|
|
||||||
if not stack:
|
|
||||||
continue
|
|
||||||
yield board_actions.BunkerizeAction(
|
|
||||||
card=stack[-1],
|
|
||||||
field_id=index,
|
|
||||||
field_row_index=len(stack) - 1,
|
|
||||||
bunker_id=open_bunker,
|
|
||||||
to_bunker=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def possible_debunkerize_actions(
|
|
||||||
search_board: board.Board,
|
|
||||||
) -> Iterator[board_actions.BunkerizeAction]:
|
|
||||||
"""Enumerates all possible card moves from the bunker to the field"""
|
|
||||||
bunker_number_cards = [
|
|
||||||
(i, x)
|
|
||||||
for i, x in enumerate(search_board.getBunker())
|
|
||||||
if isinstance(x, board.NumberCard)
|
|
||||||
]
|
|
||||||
for index, card in bunker_number_cards:
|
|
||||||
for other_index, other_stack in enumerate(search_board.getField()):
|
|
||||||
if not other_stack:
|
|
||||||
continue
|
|
||||||
if not isinstance(other_stack[-1], board.NumberCard):
|
|
||||||
continue
|
|
||||||
if other_stack[-1].suit == card.suit:
|
|
||||||
continue
|
|
||||||
if other_stack[-1].number != card.number + 1:
|
|
||||||
continue
|
|
||||||
yield board_actions.BunkerizeAction(
|
|
||||||
card=card,
|
|
||||||
bunker_id=index,
|
|
||||||
field_id=other_index,
|
|
||||||
field_row_index=len(other_stack),
|
|
||||||
to_bunker=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def possible_goal_move_actions(
|
|
||||||
search_board: board.Board,
|
|
||||||
) -> Iterator[board_actions.GoalAction]:
|
|
||||||
"""Enumerates all possible moves from anywhere to the goal"""
|
|
||||||
field_cards = [
|
|
||||||
(board.Position.Field, index, stack[-1])
|
|
||||||
for index, stack in enumerate(search_board.getField())
|
|
||||||
if stack
|
|
||||||
]
|
|
||||||
bunker_cards = [
|
|
||||||
(board.Position.Bunker, index, card)
|
|
||||||
for index, card in enumerate(search_board.getBunker())
|
|
||||||
]
|
|
||||||
top_cards = [
|
|
||||||
x for x in field_cards + bunker_cards if isinstance(x[2], board.NumberCard)
|
|
||||||
]
|
|
||||||
top_cards = [
|
|
||||||
x for x in top_cards if x[2].number == search_board.getGoal(x[2].suit) + 1
|
|
||||||
]
|
|
||||||
|
|
||||||
result = []
|
|
||||||
for source, index, card in top_cards:
|
|
||||||
assert isinstance(card, board.NumberCard)
|
|
||||||
obvious = all(
|
|
||||||
search_board.getGoal(other_suit) >= card.number - 2
|
|
||||||
for other_suit in set(board.NumberCard.Suit) - {card.suit}
|
|
||||||
)
|
|
||||||
result.append(
|
|
||||||
board_actions.GoalAction(
|
|
||||||
card=card,
|
|
||||||
source_id=index,
|
|
||||||
source_row_index=len(search_board.getField()[index]) - 1
|
|
||||||
if source == board.Position.Field
|
|
||||||
else None,
|
|
||||||
source_position=source,
|
|
||||||
goal_id=search_board.getGoalId(card.suit),
|
|
||||||
obvious=obvious,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
break
|
|
||||||
yield from sorted(result, key=lambda x: x.card.number)
|
|
||||||
|
|
||||||
|
|
||||||
def _can_stack(bottom: board.Card, top: board.Card) -> bool:
|
|
||||||
if not isinstance(bottom, board.NumberCard):
|
|
||||||
return False
|
|
||||||
if not isinstance(top, board.NumberCard):
|
|
||||||
return False
|
|
||||||
if bottom.suit == top.suit:
|
|
||||||
return False
|
|
||||||
if bottom.number != top.number + 1:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _get_cardstacks(search_board: board.Board) -> List[List[board.Card]]:
|
|
||||||
"""Returns all cards on one stack that can be moved at once"""
|
|
||||||
result: List[List[board.Card]] = []
|
|
||||||
for stack in search_board.getField():
|
|
||||||
result.append([])
|
|
||||||
if not stack:
|
|
||||||
continue
|
|
||||||
result[-1].append(stack[-1])
|
|
||||||
for card in stack[-2::-1]:
|
|
||||||
if not _can_stack(card, result[-1][0]):
|
|
||||||
break
|
|
||||||
if not isinstance(card, board.NumberCard):
|
|
||||||
break
|
|
||||||
result[-1].insert(0, card)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def possible_field_move_actions(
|
|
||||||
search_board: board.Board,
|
|
||||||
) -> Iterator[board_actions.MoveAction]:
|
|
||||||
"""Enumerate all possible move actions
|
|
||||||
from one field stack to another field stack"""
|
|
||||||
first_empty_field_id = -1
|
|
||||||
cardstacks = [x for x in enumerate(_get_cardstacks(search_board)) if x[1]]
|
|
||||||
cardstacks = sorted(cardstacks, key=lambda x: len(x[1]))
|
|
||||||
substacks: List[Tuple[int, List[board.Card]]] = []
|
|
||||||
|
|
||||||
for index, stack in cardstacks:
|
|
||||||
substacks.extend(
|
|
||||||
(index, substack) for substack in (stack[i:] for i in range(len(stack)))
|
|
||||||
)
|
|
||||||
|
|
||||||
for source_index, source_substack in substacks:
|
|
||||||
for destination_index, destination_stack in enumerate(search_board.getField()):
|
|
||||||
if source_index == destination_index:
|
|
||||||
continue
|
|
||||||
if destination_stack:
|
|
||||||
if not _can_stack(destination_stack[-1], source_substack[0]):
|
|
||||||
continue
|
|
||||||
elif len(source_substack) == len(search_board.getField()[source_index]):
|
|
||||||
continue
|
|
||||||
elif first_empty_field_id == -1:
|
|
||||||
first_empty_field_id = destination_index
|
|
||||||
elif destination_index != first_empty_field_id:
|
|
||||||
continue
|
|
||||||
yield board_actions.MoveAction(
|
|
||||||
cards=source_substack,
|
|
||||||
source_id=source_index,
|
|
||||||
source_row_index=len(search_board.getField()[source_index])
|
|
||||||
- len(source_substack),
|
|
||||||
destination_id=destination_index,
|
|
||||||
destination_row_index=len(destination_stack),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def possible_actions(search_board: board.Board) -> List[board_actions.Action]:
|
|
||||||
"""Enumerate all possible actions on the current search_board"""
|
|
||||||
result: List[board_actions.Action] = [
|
|
||||||
*list(possible_huakill_action(search_board)),
|
|
||||||
*list(possible_dragonkill_actions(search_board)),
|
|
||||||
*list(possible_goal_move_actions(search_board)),
|
|
||||||
*list(possible_debunkerize_actions(search_board)),
|
|
||||||
*list(possible_field_move_actions(search_board)),
|
|
||||||
*list(possible_bunkerize_actions(search_board)),
|
|
||||||
]
|
|
||||||
|
|
||||||
for action in result:
|
|
||||||
if action.automatic():
|
|
||||||
return [action]
|
|
||||||
|
|
||||||
return result
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
"""Contains solver for solitaire"""
|
|
||||||
import typing
|
|
||||||
from typing import Iterator, List, Optional
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from ..board import Board
|
|
||||||
from . import board_actions
|
|
||||||
from .board_actions import DragonKillAction, GoalAction, HuaKillAction, MoveAction
|
|
||||||
from .board_possibilities import possible_actions
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ActionStackFrame:
|
|
||||||
iterator: Iterator[board_actions.Action]
|
|
||||||
last_action: Optional[board_actions.Action]
|
|
||||||
state: int
|
|
||||||
|
|
||||||
def next(self) -> Optional[board_actions.Action]:
|
|
||||||
"""Get next iteration of top action iterator"""
|
|
||||||
try:
|
|
||||||
self.last_action = next(self.iterator)
|
|
||||||
except StopIteration:
|
|
||||||
return None
|
|
||||||
return self.last_action
|
|
||||||
|
|
||||||
|
|
||||||
class ActionStack:
|
|
||||||
"""Stack of chosen actions on the board"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.frames: List[ActionStackFrame] = []
|
|
||||||
|
|
||||||
def push(self, board: Board) -> None:
|
|
||||||
"""Append another board state to stack"""
|
|
||||||
self.frames.append(
|
|
||||||
ActionStackFrame(
|
|
||||||
iterator=iter(possible_actions(board)),
|
|
||||||
last_action=None,
|
|
||||||
state=board.state_identifier,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def top(self) -> ActionStackFrame:
|
|
||||||
"""Get next iteration of top action iterator"""
|
|
||||||
return self.frames[-1]
|
|
||||||
|
|
||||||
def pop(self) -> None:
|
|
||||||
"""Pop one action from stack"""
|
|
||||||
self.frames.pop()
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self.frames)
|
|
||||||
|
|
||||||
|
|
||||||
def solve(
|
|
||||||
board: Board, *, timeout: Optional[float] = None, verbose: bool = False
|
|
||||||
) -> Iterator[List[board_actions.Action]]:
|
|
||||||
"""Solve a solitaire puzzle"""
|
|
||||||
state_set = {board.state_identifier}
|
|
||||||
stack = ActionStack()
|
|
||||||
stack.push(board)
|
|
||||||
|
|
||||||
def _limit_stack_size(stack_size: int) -> None:
|
|
||||||
if len(stack) == stack_size:
|
|
||||||
stack.pop()
|
|
||||||
assert stack.top.last_action is not None
|
|
||||||
stack.top.last_action.undo(board)
|
|
||||||
assert board.state_identifier in state_set
|
|
||||||
|
|
||||||
def _backtrack_action() -> None:
|
|
||||||
stack.pop()
|
|
||||||
assert stack.top.last_action is not None
|
|
||||||
stack.top.last_action.undo(board)
|
|
||||||
assert board.state_identifier in state_set
|
|
||||||
|
|
||||||
def _skip_loop_move(action: board_actions.Action) -> bool:
|
|
||||||
if not isinstance(action, MoveAction):
|
|
||||||
return False
|
|
||||||
for frame in stack.frames[-2::-1]:
|
|
||||||
if not isinstance(frame.last_action, MoveAction):
|
|
||||||
continue
|
|
||||||
if frame.last_action.cards == action.cards:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
iter_start = time.time()
|
|
||||||
count = 0
|
|
||||||
while len(stack) > 0:
|
|
||||||
|
|
||||||
count += 1
|
|
||||||
if count > 5000:
|
|
||||||
count = 0
|
|
||||||
if verbose:
|
|
||||||
print(f"{time.time() - iter_start} {len(stack)} {board.goal}")
|
|
||||||
if timeout is not None and time.time() - iter_start > timeout:
|
|
||||||
return
|
|
||||||
|
|
||||||
# _limit_stack_size(80)
|
|
||||||
|
|
||||||
assert board.state_identifier == stack.top.state
|
|
||||||
action = stack.top.next()
|
|
||||||
|
|
||||||
if action is None:
|
|
||||||
_backtrack_action()
|
|
||||||
continue
|
|
||||||
|
|
||||||
if _skip_loop_move(action):
|
|
||||||
continue
|
|
||||||
|
|
||||||
action.apply(board)
|
|
||||||
|
|
||||||
if board.solved():
|
|
||||||
assert all(x.last_action is not None for x in stack.frames)
|
|
||||||
yield [
|
|
||||||
typing.cast(board_actions.Action, x.last_action) for x in stack.frames
|
|
||||||
]
|
|
||||||
iter_start = time.time()
|
|
||||||
action.undo(board)
|
|
||||||
assert board.state_identifier in state_set
|
|
||||||
continue
|
|
||||||
|
|
||||||
if board.state_identifier in state_set:
|
|
||||||
action.undo(board)
|
|
||||||
assert board.state_identifier in state_set
|
|
||||||
continue
|
|
||||||
|
|
||||||
state_set.add(board.state_identifier)
|
|
||||||
stack.push(board)
|
|
||||||
1
solver-rs/.gitignore
vendored
Normal file
1
solver-rs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
46
solver-rs/.vscode/launch.json
vendored
Normal file
46
solver-rs/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug unit tests in library 'board'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--no-run",
|
||||||
|
"--lib",
|
||||||
|
"--package=board"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "board",
|
||||||
|
"kind": "lib"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug unit tests in library 'solver'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--no-run",
|
||||||
|
"--lib",
|
||||||
|
"--package=solver"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "solver",
|
||||||
|
"kind": "lib"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
26
solver-rs/.vscode/tasks.json
vendored
Normal file
26
solver-rs/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the tasks.json format
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "cargo",
|
||||||
|
"subcommand": "build",
|
||||||
|
"problemMatcher": [
|
||||||
|
"$rustc"
|
||||||
|
],
|
||||||
|
"group": "build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "cargo",
|
||||||
|
"subcommand": "check",
|
||||||
|
"problemMatcher": [
|
||||||
|
"$rustc"
|
||||||
|
],
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
375
solver-rs/Cargo.lock
generated
Normal file
375
solver-rs/Cargo.lock
generated
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
[[package]]
|
||||||
|
name = "action_optimization"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"actions",
|
||||||
|
"board",
|
||||||
|
"petgraph",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actions"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"board",
|
||||||
|
"enum-iterator",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ansi_term"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atty"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "board"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"enum-iterator",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "2.33.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
|
||||||
|
dependencies = [
|
||||||
|
"ansi_term",
|
||||||
|
"atty",
|
||||||
|
"bitflags",
|
||||||
|
"strsim",
|
||||||
|
"textwrap",
|
||||||
|
"unicode-width",
|
||||||
|
"vec_map",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum-iterator"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c79a6321a1197d7730510c7e3f6cb80432dfefecb32426de8cea0aa19b4bb8d7"
|
||||||
|
dependencies = [
|
||||||
|
"enum-iterator-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum-iterator-derive"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e94aa31f7c0dc764f57896dc615ddd76fc13b0d5dca7eb6cc5e018a5a09ec06"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fixedbitset"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.71"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "petgraph"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7"
|
||||||
|
dependencies = [
|
||||||
|
"fixedbitset",
|
||||||
|
"indexmap",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error-attr",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error-attr"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"syn-mid",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e707fbbf255b8fc8c3b99abb91e7257a622caeb20a9818cbadbeeede4e0932ff"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac5d00fc561ba2724df6758a17de23df5914f20e41cb00f94d5b7ae42fffaff8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.50"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78a7a12c167809363ec3bd7329fc0a3369056996de43c4b37ef3cd54a6ce4867"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shenzhen"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"action_optimization",
|
||||||
|
"actions",
|
||||||
|
"board",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"solving",
|
||||||
|
"structopt",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "solving"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"action_optimization",
|
||||||
|
"actions",
|
||||||
|
"board",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "structopt"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "863246aaf5ddd0d6928dfeb1a9ca65f505599e4e1b399935ef7e75107516b4ef"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"lazy_static",
|
||||||
|
"structopt-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "structopt-derive"
|
||||||
|
version = "0.4.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d239ca4b13aee7a2142e6795cbd69e457665ff8037aed33b3effdc430d2f927a"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro-error",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn-mid"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "textwrap"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vec_map"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
17
solver-rs/Cargo.toml
Normal file
17
solver-rs/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "shenzhen"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Lukas Wölfer <lukas.woelfer@rwth-aachen.de>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
board = {path = "lib/board"}
|
||||||
|
actions = {path = "lib/actions"}
|
||||||
|
action_optimization = {path = "lib/action_optimization"}
|
||||||
|
solving = {path = "lib/solving"}
|
||||||
|
|
||||||
|
serde = {version="1.0.105",features=["derive"]}
|
||||||
|
serde_json = "1.0"
|
||||||
|
structopt = "0.3.14"
|
||||||
1
solver-rs/README.md
Normal file
1
solver-rs/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# SHENZHEN I/O Solitaire Solver
|
||||||
1
solver-rs/aux/actions/01.json
Normal file
1
solver-rs/aux/actions/01.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/02.json
Normal file
1
solver-rs/aux/actions/02.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/03.json
Normal file
1
solver-rs/aux/actions/03.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/04.json
Normal file
1
solver-rs/aux/actions/04.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/05.json
Normal file
1
solver-rs/aux/actions/05.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/06.json
Normal file
1
solver-rs/aux/actions/06.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/07.json
Normal file
1
solver-rs/aux/actions/07.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/08.json
Normal file
1
solver-rs/aux/actions/08.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/09.json
Normal file
1
solver-rs/aux/actions/09.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/10.json
Normal file
1
solver-rs/aux/actions/10.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/11.json
Normal file
1
solver-rs/aux/actions/11.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/12.json
Normal file
1
solver-rs/aux/actions/12.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/13.json
Normal file
1
solver-rs/aux/actions/13.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/14.json
Normal file
1
solver-rs/aux/actions/14.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/15.json
Normal file
1
solver-rs/aux/actions/15.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/16.json
Normal file
1
solver-rs/aux/actions/16.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/17.json
Normal file
1
solver-rs/aux/actions/17.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/actions/18.json
Normal file
1
solver-rs/aux/actions/18.json
Normal file
File diff suppressed because one or more lines are too long
1
solver-rs/aux/boards/normal/01.json
Normal file
1
solver-rs/aux/boards/normal/01.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 3, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 1, "suit": "Green"}}, {"Special": "Fa"}, {"Special": "Zhong"}], [{"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Special": "Zhong"}], [{"Special": "Zhong"}, {"Special": "Bai"}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Black"}}], [{"Special": "Fa"}, {"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Black"}}], [{"Number": {"value": 7, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Black"}}], [{"Number": {"value": 2, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 3, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 8, "suit": "Black"}}], [{"Number": {"value": 4, "suit": "Green"}}, "Hua", {"Number": {"value": 7, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 9, "suit": "Red"}}], [{"Number": {"value": 8, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Green"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/02.json
Normal file
1
solver-rs/aux/boards/normal/02.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 8, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 3, "suit": "Green"}}, "Hua", {"Number": {"value": 8, "suit": "Green"}}], [{"Special": "Bai"}, {"Number": {"value": 1, "suit": "Green"}}, {"Special": "Bai"}, {"Special": "Zhong"}, {"Number": {"value": 2, "suit": "Green"}}], [{"Special": "Fa"}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Green"}}], [{"Number": {"value": 1, "suit": "Black"}}, {"Number": {"value": 7, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Special": "Zhong"}], [{"Special": "Bai"}, {"Number": {"value": 1, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Red"}}], [{"Number": {"value": 5, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Black"}}, {"Special": "Zhong"}], [{"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 3, "suit": "Red"}}], [{"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Green"}}, {"Number": {"value": 2, "suit": "Black"}}, {"Special": "Zhong"}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/03.json
Normal file
1
solver-rs/aux/boards/normal/03.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 4, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Green"}}], [{"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Red"}}], [{"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Special": "Fa"}, {"Number": {"value": 6, "suit": "Black"}}, {"Special": "Zhong"}], [{"Special": "Fa"}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Special": "Zhong"}, {"Special": "Zhong"}], [{"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 2, "suit": "Green"}}], [{"Number": {"value": 9, "suit": "Green"}}, "Hua", {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Special": "Fa"}], [{"Number": {"value": 3, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 4, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 7, "suit": "Black"}}], [{"Number": {"value": 7, "suit": "Green"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Special": "Bai"}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/04.json
Normal file
1
solver-rs/aux/boards/normal/04.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Special": "Bai"}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Green"}}, {"Special": "Bai"}], [{"Number": {"value": 9, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Black"}}], [{"Number": {"value": 7, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Special": "Zhong"}], [{"Special": "Fa"}, {"Number": {"value": 6, "suit": "Red"}}, {"Special": "Zhong"}, {"Special": "Fa"}, {"Number": {"value": 4, "suit": "Green"}}], [{"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Black"}}, {"Special": "Fa"}, {"Special": "Zhong"}, {"Number": {"value": 5, "suit": "Black"}}], [{"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Special": "Zhong"}], [{"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 8, "suit": "Green"}}], ["Hua", {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Red"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Red"}, {"value": 1, "suit": "Black"}, null]}
|
||||||
1
solver-rs/aux/boards/normal/05.json
Normal file
1
solver-rs/aux/boards/normal/05.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Special": "Fa"}, {"Number": {"value": 8, "suit": "Red"}}, {"Special": "Zhong"}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Green"}}], [{"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Black"}}], [{"Number": {"value": 3, "suit": "Green"}}, {"Special": "Zhong"}, {"Special": "Fa"}, {"Special": "Bai"}], [{"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Green"}}], [{"Special": "Bai"}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 2, "suit": "Black"}}, {"Special": "Fa"}], [{"Number": {"value": 6, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 2, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 9, "suit": "Red"}}], [{"Number": {"value": 7, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Red"}}], [{"Special": "Zhong"}, {"Number": {"value": 1, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Red"}}]], "hua_set": true, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Black"}, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/06.json
Normal file
1
solver-rs/aux/boards/normal/06.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 9, "suit": "Green"}}], [{"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Red"}}], [{"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 3, "suit": "Green"}}], [{"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 2, "suit": "Red"}}], [{"Number": {"value": 7, "suit": "Green"}}, {"Special": "Bai"}, {"Special": "Zhong"}, {"Special": "Fa"}, {"Special": "Bai"}], [{"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 5, "suit": "Green"}}], [{"Special": "Fa"}, "Hua", {"Number": {"value": 3, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Black"}}], [{"Special": "Fa"}, {"Special": "Fa"}, {"Special": "Zhong"}, {"Number": {"value": 4, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Black"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Green"}, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/07.json
Normal file
1
solver-rs/aux/boards/normal/07.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 7, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Special": "Bai"}], [{"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 3, "suit": "Black"}}, {"Special": "Fa"}], [{"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Black"}}], [{"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Special": "Fa"}, {"Special": "Bai"}, {"Number": {"value": 7, "suit": "Black"}}], ["Hua", {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Black"}}, {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Red"}}], [{"Special": "Zhong"}, {"Special": "Zhong"}, {"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}], [{"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 3, "suit": "Red"}}], [{"Special": "Bai"}, {"Special": "Zhong"}, {"Number": {"value": 6, "suit": "Green"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Black"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/08.json
Normal file
1
solver-rs/aux/boards/normal/08.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Special": "Bai"}, {"Number": {"value": 7, "suit": "Black"}}], [{"Special": "Zhong"}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Green"}}, "Hua", {"Number": {"value": 8, "suit": "Red"}}], [{"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Special": "Bai"}, {"Special": "Fa"}], [{"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Red"}}], [{"Special": "Zhong"}, {"Special": "Fa"}, {"Number": {"value": 1, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 8, "suit": "Green"}}], [{"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}], [{"Special": "Bai"}, {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Special": "Zhong"}, {"Number": {"value": 9, "suit": "Black"}}], [{"Number": {"value": 1, "suit": "Red"}}, {"Special": "Fa"}, {"Special": "Fa"}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Black"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 2, "suit": "Green"}, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/09.json
Normal file
1
solver-rs/aux/boards/normal/09.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 7, "suit": "Green"}}, {"Special": "Zhong"}, {"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Black"}}], [{"Special": "Bai"}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Bai"}, {"Number": {"value": 3, "suit": "Black"}}], [{"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Black"}}, {"Special": "Fa"}, {"Special": "Zhong"}, {"Number": {"value": 3, "suit": "Green"}}], [{"Number": {"value": 2, "suit": "Green"}}, {"Special": "Zhong"}, {"Number": {"value": 1, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Red"}}], [{"Number": {"value": 2, "suit": "Black"}}, {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Red"}}], [{"Number": {"value": 5, "suit": "Black"}}, {"Special": "Fa"}, {"Number": {"value": 1, "suit": "Green"}}, {"Special": "Zhong"}, {"Special": "Fa"}], [{"Special": "Bai"}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Special": "Bai"}], [{"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}]], "hua_set": true, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Red"}, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/10.json
Normal file
1
solver-rs/aux/boards/normal/10.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Special": "Fa"}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Special": "Zhong"}, {"Special": "Fa"}], [{"Number": {"value": 1, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Black"}}], [{"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Special": "Zhong"}, {"Number": {"value": 8, "suit": "Black"}}], [{"Special": "Zhong"}, {"Special": "Zhong"}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Bai"}], [{"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Red"}}], [{"Special": "Fa"}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Red"}}, "Hua", {"Number": {"value": 5, "suit": "Green"}}], [{"Special": "Fa"}, {"Special": "Bai"}, {"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Black"}}], [{"Special": "Bai"}, {"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Green"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Black"}, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/11.json
Normal file
1
solver-rs/aux/boards/normal/11.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Red"}}], [{"Special": "Bai"}, "Hua", {"Special": "Bai"}, {"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Green"}}], [{"Number": {"value": 1, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 5, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 9, "suit": "Red"}}], [{"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Green"}}], [{"Special": "Bai"}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 9, "suit": "Black"}}], [{"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 7, "suit": "Black"}}], [{"Number": {"value": 2, "suit": "Black"}}, {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Special": "Zhong"}], [{"Special": "Zhong"}, {"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Special": "Zhong"}, {"Special": "Zhong"}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/12.json
Normal file
1
solver-rs/aux/boards/normal/12.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Green"}}, "Hua", {"Number": {"value": 3, "suit": "Black"}}], [{"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Bai"}], [{"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Black"}}], [{"Special": "Bai"}, {"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 2, "suit": "Green"}}], [{"Special": "Fa"}, {"Special": "Fa"}, {"Special": "Fa"}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Red"}}], [{"Special": "Zhong"}, {"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 9, "suit": "Red"}}], [{"Number": {"value": 2, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Special": "Fa"}], [{"Special": "Bai"}, {"Special": "Zhong"}, {"Number": {"value": 7, "suit": "Green"}}, {"Number": {"value": 8, "suit": "Red"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Black"}, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/13.json
Normal file
1
solver-rs/aux/boards/normal/13.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Special": "Fa"}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Red"}}], [{"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 7, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Special": "Bai"}], [{"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Special": "Bai"}, {"Special": "Bai"}, {"Special": "Fa"}], [{"Special": "Fa"}, {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Red"}}], [{"Special": "Zhong"}, {"Special": "Zhong"}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 3, "suit": "Green"}}], [{"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 7, "suit": "Green"}}], [{"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Red"}}], [{"Special": "Bai"}, {"Special": "Fa"}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Green"}}, {"Special": "Zhong"}]], "hua_set": true, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/14.json
Normal file
1
solver-rs/aux/boards/normal/14.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 1, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Red"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Green"}}], ["Hua", {"Special": "Zhong"}, {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 3, "suit": "Red"}}], [{"Special": "Bai"}, {"Special": "Fa"}, {"Special": "Bai"}, {"Number": {"value": 7, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Green"}}], [{"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 9, "suit": "Green"}}], [{"Special": "Zhong"}, {"Number": {"value": 6, "suit": "Red"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Black"}}], [{"Number": {"value": 9, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Green"}}], [{"Special": "Bai"}, {"Number": {"value": 1, "suit": "Green"}}, {"Special": "Bai"}, {"Special": "Fa"}, {"Number": {"value": 7, "suit": "Red"}}], [{"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Special": "Zhong"}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/15.json
Normal file
1
solver-rs/aux/boards/normal/15.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Special": "Zhong"}, {"Number": {"value": 7, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 3, "suit": "Black"}}], ["Hua", {"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Black"}}], [{"Number": {"value": 7, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 2, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 3, "suit": "Green"}}], [{"Special": "Zhong"}, {"Number": {"value": 5, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 3, "suit": "Red"}}, {"Special": "Bai"}], [{"Number": {"value": 6, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Special": "Fa"}], [{"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Special": "Zhong"}], [{"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 8, "suit": "Green"}}, {"Special": "Zhong"}, {"Number": {"value": 8, "suit": "Black"}}], [{"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Bai"}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Red"}, {"value": 2, "suit": "Black"}, null]}
|
||||||
1
solver-rs/aux/boards/normal/16.json
Normal file
1
solver-rs/aux/boards/normal/16.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 6, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Red"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Fa"}], [{"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 7, "suit": "Green"}}], [{"Special": "Zhong"}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Green"}}, {"Special": "Bai"}, {"Special": "Zhong"}], [{"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Black"}}, {"Special": "Fa"}, {"Special": "Fa"}], [{"Number": {"value": 8, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 7, "suit": "Red"}}], [{"Special": "Fa"}, {"Special": "Bai"}, {"Number": {"value": 2, "suit": "Red"}}, "Hua", {"Special": "Bai"}], [{"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 9, "suit": "Red"}}], [{"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Red"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [{"value": 1, "suit": "Red"}, {"value": 1, "suit": "Black"}, null]}
|
||||||
1
solver-rs/aux/boards/normal/17.json
Normal file
1
solver-rs/aux/boards/normal/17.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 3, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Green"}}], ["Hua", {"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 9, "suit": "Red"}}, {"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 7, "suit": "Green"}}], [{"Number": {"value": 2, "suit": "Black"}}, {"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Black"}}, {"Number": {"value": 4, "suit": "Green"}}, {"Special": "Zhong"}], [{"Number": {"value": 3, "suit": "Red"}}, {"Number": {"value": 6, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Red"}}, {"Special": "Fa"}], [{"Special": "Bai"}, {"Number": {"value": 7, "suit": "Black"}}, {"Special": "Fa"}, {"Special": "Bai"}, {"Number": {"value": 6, "suit": "Green"}}], [{"Number": {"value": 9, "suit": "Black"}}, {"Special": "Zhong"}, {"Number": {"value": 5, "suit": "Green"}}, {"Special": "Bai"}, {"Special": "Zhong"}], [{"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Black"}}, {"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 1, "suit": "Red"}}, {"Number": {"value": 7, "suit": "Red"}}], [{"Number": {"value": 1, "suit": "Black"}}, {"Special": "Fa"}, {"Special": "Fa"}, {"Number": {"value": 5, "suit": "Red"}}, {"Number": {"value": 8, "suit": "Black"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}
|
||||||
1
solver-rs/aux/boards/normal/18.json
Normal file
1
solver-rs/aux/boards/normal/18.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"field": [[{"Number": {"value": 5, "suit": "Black"}}, {"Number": {"value": 7, "suit": "Green"}}, {"Special": "Fa"}, {"Number": {"value": 4, "suit": "Red"}}, {"Special": "Zhong"}], [{"Special": "Zhong"}, {"Number": {"value": 3, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 9, "suit": "Black"}}, {"Number": {"value": 9, "suit": "Green"}}], [{"Number": {"value": 1, "suit": "Red"}}, {"Special": "Bai"}, {"Number": {"value": 2, "suit": "Red"}}, {"Special": "Zhong"}, {"Number": {"value": 3, "suit": "Black"}}], [{"Special": "Bai"}, {"Number": {"value": 7, "suit": "Black"}}, {"Number": {"value": 2, "suit": "Black"}}, {"Special": "Bai"}, {"Number": {"value": 4, "suit": "Green"}}], [{"Number": {"value": 3, "suit": "Green"}}, {"Number": {"value": 6, "suit": "Green"}}, {"Special": "Zhong"}, {"Number": {"value": 8, "suit": "Black"}}, {"Number": {"value": 6, "suit": "Black"}}], [{"Number": {"value": 2, "suit": "Green"}}, {"Number": {"value": 4, "suit": "Black"}}, "Hua", {"Number": {"value": 7, "suit": "Red"}}, {"Special": "Bai"}], [{"Number": {"value": 1, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Green"}}, {"Number": {"value": 5, "suit": "Red"}}, {"Special": "Fa"}, {"Number": {"value": 6, "suit": "Red"}}], [{"Number": {"value": 8, "suit": "Green"}}, {"Number": {"value": 1, "suit": "Black"}}, {"Special": "Fa"}, {"Number": {"value": 8, "suit": "Red"}}, {"Number": {"value": 9, "suit": "Red"}}]], "hua_set": false, "bunker": ["Empty", "Empty", "Empty"], "goal": [null, null, null]}
|
||||||
19
solver-rs/aux/boards/specific/dragonkill.json
Normal file
19
solver-rs/aux/boards/specific/dragonkill.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"field": [
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
["Hua", { "Number": { "value": 9, "suit": "Black" } }],
|
||||||
|
[{ "Special": "Zhong" }],
|
||||||
|
[{ "Special": "Zhong" }],
|
||||||
|
[{ "Special": "Zhong" }],
|
||||||
|
[{ "Special": "Zhong" }],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"goal": [
|
||||||
|
{ "value": 8, "suit": "Black" },
|
||||||
|
{ "value": 9, "suit": "Green" },
|
||||||
|
{ "value": 9, "suit": "Red" }
|
||||||
|
],
|
||||||
|
"hua_set": false,
|
||||||
|
"bunker": [{ "Blocked": null }, { "Blocked": null }, "Empty"]
|
||||||
|
}
|
||||||
19
solver-rs/aux/boards/specific/scarce.json
Normal file
19
solver-rs/aux/boards/specific/scarce.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"field": [
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
["Hua", { "Number": { "value": 9, "suit": "Black" } }],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
"goal": [
|
||||||
|
{ "value": 8, "suit": "Black" },
|
||||||
|
{ "value": 9, "suit": "Green" },
|
||||||
|
{ "value": 9, "suit": "Red" }
|
||||||
|
],
|
||||||
|
"hua_set": false,
|
||||||
|
"bunker": [{ "Blocked": null }, { "Blocked": null }, { "Blocked": null }]
|
||||||
|
}
|
||||||
10
solver-rs/aux/boards/specific/solved.json
Normal file
10
solver-rs/aux/boards/specific/solved.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"field": [[], [], [], [], [], [], [], []],
|
||||||
|
"goal": [
|
||||||
|
{ "value": 9, "suit": "Black" },
|
||||||
|
{ "value": 9, "suit": "Green" },
|
||||||
|
{ "value": 9, "suit": "Red" }
|
||||||
|
],
|
||||||
|
"hua_set": true,
|
||||||
|
"bunker": [{ "Blocked": null }, { "Blocked": null }, { "Blocked": null }]
|
||||||
|
}
|
||||||
37
solver-rs/clippy.cfg
Normal file
37
solver-rs/clippy.cfg
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#![warn(
|
||||||
|
clippy::all,
|
||||||
|
clippy::restriction,
|
||||||
|
clippy::pedantic,
|
||||||
|
clippy::nursery,
|
||||||
|
clippy::cargo
|
||||||
|
)]
|
||||||
|
#![allow(clippy::cargo)]
|
||||||
|
// Style choices
|
||||||
|
#![allow(
|
||||||
|
clippy::missing_docs_in_private_items,
|
||||||
|
clippy::needless_return,
|
||||||
|
clippy::get_unwrap,
|
||||||
|
clippy::indexing_slicing,
|
||||||
|
clippy::explicit_iter_loop
|
||||||
|
)]
|
||||||
|
// Way too pedantic
|
||||||
|
#![allow(clippy::integer_arithmetic)]
|
||||||
|
// Useless
|
||||||
|
#![allow(clippy::missing_inline_in_public_items, clippy::missing_const_for_fn)]
|
||||||
|
// Useful for production
|
||||||
|
#![allow(
|
||||||
|
clippy::use_debug,
|
||||||
|
clippy::print_stdout,
|
||||||
|
clippy::dbg_macro,
|
||||||
|
clippy::panic
|
||||||
|
)]
|
||||||
|
// Useful for improving code robustness
|
||||||
|
#![allow(
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_possible_wrap,
|
||||||
|
clippy::option_unwrap_used,
|
||||||
|
// clippy::result_unwrap_used,
|
||||||
|
// clippy::wildcard_enum_match_arm
|
||||||
|
)]
|
||||||
|
#![allow(clippy::trivially_copy_pass_by_ref)]
|
||||||
|
#![allow(dead_code)]
|
||||||
15
solver-rs/lib/action_optimization/Cargo.toml
Normal file
15
solver-rs/lib/action_optimization/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "action_optimization"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Lukas Wölfer <lukas.woelfer@rwth-aachen.de>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = {version="1.0.105",features=["derive"]}
|
||||||
|
serde_json = "1.0"
|
||||||
|
petgraph = "0.5.1"
|
||||||
|
|
||||||
|
board = {path = "../board"}
|
||||||
|
actions = {path = "../actions"}
|
||||||
98
solver-rs/lib/action_optimization/src/drawing.rs
Normal file
98
solver-rs/lib/action_optimization/src/drawing.rs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
use super::graph_entity::{to_graph, ActionGraph, RelationType};
|
||||||
|
use std::{
|
||||||
|
path::Path,
|
||||||
|
process::{Command, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn dot_actions(actions: &[actions::All]) -> String {
|
||||||
|
return dot_actiongraph(&to_graph(actions));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn dot_actiongraph(graph: &ActionGraph) -> String {
|
||||||
|
let edge_attr = |relation_type: &RelationType| {
|
||||||
|
let edge_style = match relation_type {
|
||||||
|
RelationType::Move => "bold",
|
||||||
|
RelationType::Unblock
|
||||||
|
| RelationType::Clear
|
||||||
|
| RelationType::Socket
|
||||||
|
| RelationType::Goal => "solid",
|
||||||
|
};
|
||||||
|
let edge_color = match relation_type {
|
||||||
|
RelationType::Move => "black",
|
||||||
|
RelationType::Unblock => "green",
|
||||||
|
RelationType::Clear => "grey",
|
||||||
|
RelationType::Socket => "red",
|
||||||
|
RelationType::Goal => "blue",
|
||||||
|
};
|
||||||
|
return format!("style=\"{}\" color=\"{}\"", edge_style, edge_color);
|
||||||
|
};
|
||||||
|
let node_attr = |action: &actions::All| {
|
||||||
|
let node_color = match action {
|
||||||
|
actions::All::Bunkerize(_) | actions::All::Move(_) => "white",
|
||||||
|
actions::All::DragonKill(_) => "silver",
|
||||||
|
actions::All::Goal(_) => "blue",
|
||||||
|
actions::All::HuaKill(_) => "gold",
|
||||||
|
};
|
||||||
|
return format!(
|
||||||
|
r#"style="filled" fillcolor="{}" label="{}" shape="rect""#,
|
||||||
|
node_color,
|
||||||
|
action.to_string().replace(r#"""#, r#"\""#)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let dot_rep = petgraph::dot::Dot::with_attr_getters(
|
||||||
|
&graph,
|
||||||
|
&[
|
||||||
|
petgraph::dot::Config::EdgeNoLabel,
|
||||||
|
petgraph::dot::Config::NodeNoLabel,
|
||||||
|
],
|
||||||
|
&|_mygraph, myedge| return edge_attr(myedge.weight()),
|
||||||
|
&|_mygraph, (_index, action)| {
|
||||||
|
return node_attr(action);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.to_string();
|
||||||
|
return dot_rep;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_graph(graph: &ActionGraph, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! # Errors
|
||||||
|
//! File write error
|
||||||
|
let input = dot_actiongraph(graph);
|
||||||
|
let mut child = Command::new("dot")
|
||||||
|
.args(&["-Tsvg", "-o", path.to_string_lossy().as_ref()])
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
std::io::Write::write_all(
|
||||||
|
child
|
||||||
|
.stdin
|
||||||
|
.as_mut()
|
||||||
|
.ok_or("Child process stdin has not been captured!")?,
|
||||||
|
input.as_bytes(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let output = child.wait_with_output()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
println!(
|
||||||
|
"Dot failed\n{}\n{}",
|
||||||
|
std::str::from_utf8(&output.stdout).unwrap(),
|
||||||
|
std::str::from_utf8(&output.stderr).unwrap()
|
||||||
|
);
|
||||||
|
// No idea how to return a custom error here
|
||||||
|
}
|
||||||
|
return Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_actions(
|
||||||
|
actions: &[actions::All],
|
||||||
|
path: &Path,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! # Errors
|
||||||
|
//! File write error
|
||||||
|
let graph = to_graph(actions);
|
||||||
|
return draw_graph(&graph, path);
|
||||||
|
}
|
||||||
87
solver-rs/lib/action_optimization/src/graph_entity.rs
Normal file
87
solver-rs/lib/action_optimization/src/graph_entity.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use super::relation::{
|
||||||
|
get_clear_parents, get_destination_parent, get_goal_parent, get_move_parents,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub enum RelationType {
|
||||||
|
Move,
|
||||||
|
Unblock,
|
||||||
|
Clear,
|
||||||
|
Socket,
|
||||||
|
Goal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RelationType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let stringed = match self {
|
||||||
|
Self::Move => "Move",
|
||||||
|
Self::Unblock => "Unblock",
|
||||||
|
Self::Clear => "Clear",
|
||||||
|
Self::Socket => "Socket",
|
||||||
|
Self::Goal => "Goal",
|
||||||
|
};
|
||||||
|
return write!(f, "{}", stringed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub type ActionGraph = petgraph::stable_graph::StableDiGraph<actions::All, RelationType>;
|
||||||
|
|
||||||
|
pub fn to_graph(actions: &[actions::All]) -> ActionGraph {
|
||||||
|
let mut x = ActionGraph::new();
|
||||||
|
|
||||||
|
macro_rules! relations {
|
||||||
|
($actions:expr, $index: expr, $( $x:expr, $y: expr ),*) => {{
|
||||||
|
[
|
||||||
|
$(
|
||||||
|
($x)($actions, $index).into_iter().map(|b| return (b, $y)).collect::<Vec<(usize, RelationType)>>(),
|
||||||
|
)*
|
||||||
|
]
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
// can you ActionGraph::from_elements here
|
||||||
|
for (index, action) in actions.iter().enumerate() {
|
||||||
|
let current_node = x.add_node(action.clone());
|
||||||
|
|
||||||
|
let relations = relations!(
|
||||||
|
actions,
|
||||||
|
index,
|
||||||
|
get_move_parents,
|
||||||
|
RelationType::Move,
|
||||||
|
get_clear_parents,
|
||||||
|
RelationType::Clear,
|
||||||
|
get_goal_parent,
|
||||||
|
RelationType::Goal
|
||||||
|
);
|
||||||
|
for (parent, relation) in relations.iter().flatten() {
|
||||||
|
x.add_edge(
|
||||||
|
petgraph::stable_graph::NodeIndex::new(*parent),
|
||||||
|
current_node,
|
||||||
|
*relation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Option::Some((parent, relation)) = get_destination_parent(actions, index) {
|
||||||
|
x.add_edge(
|
||||||
|
petgraph::stable_graph::NodeIndex::new(parent),
|
||||||
|
current_node,
|
||||||
|
relation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_graph(graph: &ActionGraph) -> Vec<actions::All> {
|
||||||
|
match petgraph::algo::toposort(graph, Option::None) {
|
||||||
|
Ok(topo_actions) => {
|
||||||
|
let topo_actions = topo_actions
|
||||||
|
.into_iter()
|
||||||
|
.map(|index| return graph.node_weight(index).unwrap().clone())
|
||||||
|
.collect::<Vec<actions::All>>();
|
||||||
|
return topo_actions;
|
||||||
|
}
|
||||||
|
Err(c) => panic!(
|
||||||
|
"Could not toposort the graph, {:#?}, Graph: {:?}",
|
||||||
|
c,
|
||||||
|
super::draw_graph(graph, std::path::Path::new("cycle_graph.svg"))
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
52
solver-rs/lib/action_optimization/src/lib.rs
Normal file
52
solver-rs/lib/action_optimization/src/lib.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#![warn(
|
||||||
|
clippy::all,
|
||||||
|
clippy::restriction,
|
||||||
|
clippy::pedantic,
|
||||||
|
clippy::nursery,
|
||||||
|
clippy::cargo
|
||||||
|
)]
|
||||||
|
#![allow(clippy::cargo)]
|
||||||
|
// Style choices
|
||||||
|
#![allow(
|
||||||
|
clippy::missing_docs_in_private_items,
|
||||||
|
clippy::needless_return,
|
||||||
|
clippy::get_unwrap,
|
||||||
|
clippy::indexing_slicing,
|
||||||
|
clippy::explicit_iter_loop
|
||||||
|
)]
|
||||||
|
// Way too pedantic
|
||||||
|
#![allow(clippy::integer_arithmetic)]
|
||||||
|
// Useless
|
||||||
|
#![allow(clippy::missing_inline_in_public_items, clippy::missing_const_for_fn)]
|
||||||
|
// Useful for production
|
||||||
|
#![allow(
|
||||||
|
clippy::use_debug,
|
||||||
|
clippy::print_stdout,
|
||||||
|
clippy::dbg_macro,
|
||||||
|
clippy::panic
|
||||||
|
)]
|
||||||
|
// Useful for improving code robustness
|
||||||
|
#![allow(
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_possible_wrap,
|
||||||
|
clippy::option_unwrap_used,
|
||||||
|
clippy::option_expect_used,
|
||||||
|
clippy::as_conversions,
|
||||||
|
clippy::result_unwrap_used,
|
||||||
|
// clippy::wildcard_enum_match_arm
|
||||||
|
)]
|
||||||
|
#![allow(clippy::trivially_copy_pass_by_ref)]
|
||||||
|
#![allow(dead_code)]
|
||||||
|
mod drawing;
|
||||||
|
mod graph_entity;
|
||||||
|
mod optimize;
|
||||||
|
pub use optimize::optimize;
|
||||||
|
mod relation;
|
||||||
|
mod util;
|
||||||
|
// mod graph_check;
|
||||||
|
pub use drawing::*;
|
||||||
|
pub mod test_actions;
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
526
solver-rs/lib/action_optimization/src/optimize.rs
Normal file
526
solver-rs/lib/action_optimization/src/optimize.rs
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
use super::{
|
||||||
|
graph_entity::{from_graph, to_graph, ActionGraph, RelationType},
|
||||||
|
util::{get_all_cards, get_all_sources},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actions::{Bunkerize, DragonKill, Goal, Move};
|
||||||
|
use board::PositionNoGoal;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use petgraph::visit::{EdgeRef, IntoNodeReferences};
|
||||||
|
|
||||||
|
pub fn merge_actions(
|
||||||
|
descendant_action: &actions::All,
|
||||||
|
parent_action: &actions::All,
|
||||||
|
) -> Result<actions::All, String> {
|
||||||
|
debug_assert_eq!(
|
||||||
|
get_all_cards(descendant_action),
|
||||||
|
get_all_cards(parent_action)
|
||||||
|
);
|
||||||
|
match descendant_action {
|
||||||
|
actions::All::Bunkerize(action) => {
|
||||||
|
let parent_source = get_all_sources(parent_action.clone());
|
||||||
|
if parent_source.len() != 1 {
|
||||||
|
return Result::Err("Only operates on parents with one source".to_string());
|
||||||
|
}
|
||||||
|
let parent_source = &parent_source[0];
|
||||||
|
if action.to_bunker {
|
||||||
|
match parent_source {
|
||||||
|
PositionNoGoal::Field(parent_field) => {
|
||||||
|
return Result::Ok(actions::All::Bunkerize(Bunkerize {
|
||||||
|
field_position: *parent_field,
|
||||||
|
..action.clone()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
PositionNoGoal::Bunker { .. } => {
|
||||||
|
return Result::Err("Cannot merge non field move to bunkerize".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match parent_source {
|
||||||
|
PositionNoGoal::Field(parent_field) => {
|
||||||
|
return Result::Ok(actions::All::Move(Move::new(
|
||||||
|
*parent_field,
|
||||||
|
action.field_position,
|
||||||
|
&[action.card.add_hua()],
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
PositionNoGoal::Bunker { .. } => panic!(
|
||||||
|
"How can you have two debunkerize actions after following each other?"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actions::All::DragonKill(_) => return Result::Err("Not implemented".to_string()),
|
||||||
|
actions::All::Goal(action) => {
|
||||||
|
let parent_source = get_all_sources(parent_action.clone());
|
||||||
|
if parent_source.len() != 1 {
|
||||||
|
return Result::Err("Only operates on parents with one source".to_string());
|
||||||
|
}
|
||||||
|
let parent_source = parent_source.into_iter().next().unwrap();
|
||||||
|
return Result::Ok(actions::All::Goal(Goal {
|
||||||
|
source: parent_source,
|
||||||
|
..action.clone()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
actions::All::HuaKill(_) => {
|
||||||
|
panic!("How do you have a move parent for a hua kill?");
|
||||||
|
}
|
||||||
|
actions::All::Move(action) => {
|
||||||
|
let parent_source = get_all_sources(parent_action.clone());
|
||||||
|
if parent_source.len() != 1 {
|
||||||
|
return Result::Err("Only operates on parents with one source".to_string());
|
||||||
|
}
|
||||||
|
let parent_source = parent_source.into_iter().next().unwrap();
|
||||||
|
match parent_source {
|
||||||
|
PositionNoGoal::Field(parent_field) => {
|
||||||
|
let mut result_action = action.clone();
|
||||||
|
result_action.source = parent_field;
|
||||||
|
return Result::Ok(actions::All::Move(result_action));
|
||||||
|
}
|
||||||
|
PositionNoGoal::Bunker { slot_index } => {
|
||||||
|
assert!(action.stack_len() == 1);
|
||||||
|
return Result::Ok(actions::All::Bunkerize(Bunkerize {
|
||||||
|
bunker_slot_index: slot_index,
|
||||||
|
card: action.cards()[0].remove_hua(),
|
||||||
|
field_position: action.destination,
|
||||||
|
to_bunker: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parents(
|
||||||
|
graph: &ActionGraph,
|
||||||
|
index: petgraph::stable_graph::NodeIndex,
|
||||||
|
) -> Vec<petgraph::stable_graph::NodeIndex> {
|
||||||
|
let parent = graph
|
||||||
|
.edges_directed(index, petgraph::Direction::Incoming)
|
||||||
|
.filter_map(|x| {
|
||||||
|
if x.weight() == &RelationType::Move {
|
||||||
|
return Option::Some(x.source());
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return parent.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_for(
|
||||||
|
graph: &ActionGraph,
|
||||||
|
index: petgraph::stable_graph::NodeIndex,
|
||||||
|
) -> Vec<petgraph::stable_graph::NodeIndex> {
|
||||||
|
return graph
|
||||||
|
.edges_directed(index, petgraph::Direction::Outgoing)
|
||||||
|
.filter_map(|x| {
|
||||||
|
if x.weight() == &RelationType::Socket {
|
||||||
|
return Option::Some(x.target());
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Relations when merging nodes
|
||||||
|
/// - To parent:
|
||||||
|
/// - Clearing -> To merged node, needs to be cleared no matter destination
|
||||||
|
/// - Unblocking -> remove
|
||||||
|
/// - Socketing -> remove
|
||||||
|
/// - From parent:
|
||||||
|
/// - Clearing -> From merged node, clears no matter of destination
|
||||||
|
/// - Unblocking -> From merged node, when to child both nodes can be removed
|
||||||
|
/// - Socketing -> Abort merging, probably needs to be still there
|
||||||
|
/// - To child:
|
||||||
|
/// - Clearing -> Shouldn't happen when there is no traffic between parent
|
||||||
|
/// - Unblocking -> Still required, keep
|
||||||
|
/// - Socketing -> Still required, keep
|
||||||
|
/// - From child:
|
||||||
|
/// - Clearing -> Should cancel the socket to parent, remove
|
||||||
|
/// - Unblocking -> Join with incoming unblocking of parent, otherwise cell was always empty
|
||||||
|
/// - Socketing -> keep, destination stays the same, as such still sockets the target of the relation
|
||||||
|
fn merge_edges(
|
||||||
|
graph: &mut ActionGraph,
|
||||||
|
parent: petgraph::stable_graph::NodeIndex,
|
||||||
|
child: petgraph::stable_graph::NodeIndex,
|
||||||
|
) {
|
||||||
|
let mut addable_edges = Vec::new();
|
||||||
|
let mut removable_edges = Vec::new();
|
||||||
|
let mut unblock_parent = Option::None;
|
||||||
|
let mut socket_parent = Option::None;
|
||||||
|
for parent_dependent in graph.edges_directed(parent, petgraph::Direction::Incoming) {
|
||||||
|
match parent_dependent.weight() {
|
||||||
|
RelationType::Socket => socket_parent = Option::Some(parent_dependent.source()),
|
||||||
|
RelationType::Unblock => unblock_parent = Option::Some(parent_dependent.source()),
|
||||||
|
RelationType::Move | RelationType::Clear => {
|
||||||
|
addable_edges.push((parent_dependent.source(), child, *parent_dependent.weight()));
|
||||||
|
}
|
||||||
|
RelationType::Goal => panic!("Merging actions does not work on goal actions"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for parent_dependent in graph.edges_directed(parent, petgraph::Direction::Outgoing) {
|
||||||
|
match parent_dependent.weight() {
|
||||||
|
RelationType::Move => {
|
||||||
|
debug_assert_eq!(parent_dependent.target(), child);
|
||||||
|
}
|
||||||
|
RelationType::Unblock | RelationType::Clear => {
|
||||||
|
if parent_dependent.target() != child {
|
||||||
|
addable_edges.push((
|
||||||
|
child,
|
||||||
|
parent_dependent.target(),
|
||||||
|
*parent_dependent.weight(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RelationType::Socket => {
|
||||||
|
panic!("Cannot merge a parent which provides a socket for an action")
|
||||||
|
}
|
||||||
|
RelationType::Goal => panic!("Merging actions does not work on goal actions"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for child_dependent in graph.edges_directed(child, petgraph::Direction::Incoming) {
|
||||||
|
match child_dependent.weight() {
|
||||||
|
RelationType::Move => {
|
||||||
|
if get_all_cards(&graph[child]).len() == 1 {
|
||||||
|
debug_assert_eq!(child_dependent.source(), parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RelationType::Clear => {
|
||||||
|
if get_all_cards(&graph[child]).len() == 1 {
|
||||||
|
panic!("What is being cleared between the parent and the child when no other interaction happened in between?\n{:?} {}\n{:?} {}",
|
||||||
|
parent, graph.node_weight(parent).unwrap(), child, graph.node_weight(child).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RelationType::Unblock | RelationType::Socket | RelationType::Goal => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for child_dependent in graph.edges_directed(child, petgraph::Direction::Outgoing) {
|
||||||
|
match child_dependent.weight() {
|
||||||
|
RelationType::Goal | RelationType::Move | RelationType::Socket => {}
|
||||||
|
RelationType::Clear => removable_edges.push(child_dependent.id()),
|
||||||
|
RelationType::Unblock => {
|
||||||
|
debug_assert!(
|
||||||
|
!(unblock_parent.is_some() && socket_parent.is_some()),
|
||||||
|
"Both unblock {:?} and socket {:?} parent for {:?}",
|
||||||
|
unblock_parent.unwrap(),
|
||||||
|
socket_parent.unwrap(),
|
||||||
|
child
|
||||||
|
);
|
||||||
|
if let Option::Some(parent_unblocker) = unblock_parent {
|
||||||
|
addable_edges.push((
|
||||||
|
parent_unblocker,
|
||||||
|
child_dependent.target(),
|
||||||
|
*child_dependent.weight(),
|
||||||
|
));
|
||||||
|
removable_edges.push(child_dependent.id());
|
||||||
|
}
|
||||||
|
if let Option::Some(parent_socket) = socket_parent {
|
||||||
|
addable_edges.push((
|
||||||
|
parent_socket,
|
||||||
|
child_dependent.target(),
|
||||||
|
RelationType::Socket,
|
||||||
|
));
|
||||||
|
removable_edges.push(child_dependent.id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (source, target, weight) in addable_edges {
|
||||||
|
graph.add_edge(source, target, weight);
|
||||||
|
}
|
||||||
|
for edge in removable_edges {
|
||||||
|
graph.remove_edge(edge);
|
||||||
|
}
|
||||||
|
graph.remove_node(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_merge(
|
||||||
|
graph: &mut ActionGraph,
|
||||||
|
parent: petgraph::stable_graph::NodeIndex,
|
||||||
|
child: petgraph::stable_graph::NodeIndex,
|
||||||
|
) -> bool {
|
||||||
|
if let Result::Ok(new_action) = merge_actions(
|
||||||
|
graph.node_weight(child).unwrap(),
|
||||||
|
graph.node_weight(parent).unwrap(),
|
||||||
|
) {
|
||||||
|
*graph.node_weight_mut(child).unwrap() = new_action;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
merge_edges(graph, parent, child);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove an action from the graph which has no impact on the board
|
||||||
|
pub fn delete_null_node(graph: &mut ActionGraph, null_node: petgraph::stable_graph::NodeIndex) {
|
||||||
|
let join_edge = |graph: &mut ActionGraph, reltype: RelationType| {
|
||||||
|
let incoming_edge = graph
|
||||||
|
.edges_directed(null_node, petgraph::Direction::Incoming)
|
||||||
|
.find_map(|x| {
|
||||||
|
if x.weight() == &reltype {
|
||||||
|
return Option::Some(x.source());
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let outgoing_edge = graph
|
||||||
|
.edges_directed(null_node, petgraph::Direction::Outgoing)
|
||||||
|
.find_map(|x| {
|
||||||
|
if x.weight() == &reltype {
|
||||||
|
return Option::Some(x.target());
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Option::Some(incoming_edge) = incoming_edge {
|
||||||
|
if let Option::Some(outgoing_edge) = outgoing_edge {
|
||||||
|
graph.add_edge(incoming_edge, outgoing_edge, reltype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
join_edge(graph, RelationType::Move);
|
||||||
|
join_edge(graph, RelationType::Unblock);
|
||||||
|
for weird_edge in graph
|
||||||
|
.edges_directed(null_node, petgraph::Direction::Incoming)
|
||||||
|
.chain(graph.edges_directed(null_node, petgraph::Direction::Outgoing))
|
||||||
|
.filter(|x| {
|
||||||
|
return x.weight() != &RelationType::Move && x.weight() != &RelationType::Unblock;
|
||||||
|
})
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"Weird edge while deleting null node\n{}\n{:?} {}\n{:?} {}\n{:?} {}",
|
||||||
|
weird_edge.weight(),
|
||||||
|
null_node,
|
||||||
|
graph.node_weight(null_node).unwrap(),
|
||||||
|
weird_edge.source(),
|
||||||
|
graph.node_weight(weird_edge.source()).unwrap(),
|
||||||
|
weird_edge.target(),
|
||||||
|
graph.node_weight(weird_edge.target()).unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
graph.remove_node(null_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_replace_bunker_slot(
|
||||||
|
graph: &mut ActionGraph,
|
||||||
|
index: petgraph::stable_graph::NodeIndex,
|
||||||
|
parent_slot: u8,
|
||||||
|
child_slot: u8,
|
||||||
|
) {
|
||||||
|
let swap_slot = |slot| {
|
||||||
|
if slot == child_slot {
|
||||||
|
return parent_slot;
|
||||||
|
} else if slot == parent_slot {
|
||||||
|
return child_slot;
|
||||||
|
} else {
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match graph.node_weight_mut(index).unwrap() {
|
||||||
|
actions::All::Bunkerize(Bunkerize {
|
||||||
|
bunker_slot_index, ..
|
||||||
|
}) => {
|
||||||
|
*bunker_slot_index = swap_slot(*bunker_slot_index);
|
||||||
|
}
|
||||||
|
actions::All::DragonKill(DragonKill {
|
||||||
|
source,
|
||||||
|
destination_slot_index,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let slot_index = source.iter_mut().filter_map(|x| {
|
||||||
|
if let board::PositionNoGoal::Bunker { slot_index } = x {
|
||||||
|
return Option::Some(slot_index);
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for current_slot in slot_index {
|
||||||
|
*current_slot = swap_slot(*current_slot);
|
||||||
|
}
|
||||||
|
*destination_slot_index = swap_slot(*destination_slot_index);
|
||||||
|
}
|
||||||
|
actions::All::Goal(Goal { source, .. }) => {
|
||||||
|
if let PositionNoGoal::Bunker { slot_index } = source {
|
||||||
|
*slot_index = swap_slot(*slot_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actions::All::HuaKill(_) | actions::All::Move(_) => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flip_bunker_slots(
|
||||||
|
graph: &mut ActionGraph,
|
||||||
|
index: petgraph::stable_graph::NodeIndex,
|
||||||
|
parent_slot: u8,
|
||||||
|
child_slot: u8,
|
||||||
|
) {
|
||||||
|
let unblock_move_graph = petgraph::visit::EdgeFiltered::from_fn(
|
||||||
|
&*graph,
|
||||||
|
&|x: petgraph::stable_graph::EdgeReference<RelationType>| match x.weight() {
|
||||||
|
RelationType::Move | RelationType::Unblock => return true,
|
||||||
|
RelationType::Clear | RelationType::Socket | RelationType::Goal => return false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let mut visitor = petgraph::visit::Dfs::new(&unblock_move_graph, index);
|
||||||
|
|
||||||
|
while let Option::Some(index) = visitor.next(&*graph) {
|
||||||
|
try_replace_bunker_slot(graph, index, parent_slot, child_slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_bunker_loop(
|
||||||
|
graph: &ActionGraph,
|
||||||
|
parent: petgraph::stable_graph::NodeIndex,
|
||||||
|
child: petgraph::stable_graph::NodeIndex,
|
||||||
|
) -> bool {
|
||||||
|
if let actions::All::Bunkerize(Bunkerize {
|
||||||
|
to_bunker: parent_to_bunker,
|
||||||
|
..
|
||||||
|
}) = graph.node_weight(parent).unwrap()
|
||||||
|
{
|
||||||
|
if let actions::All::Bunkerize(Bunkerize { to_bunker, .. }) =
|
||||||
|
graph.node_weight(child).unwrap()
|
||||||
|
{
|
||||||
|
if !parent_to_bunker && *to_bunker {
|
||||||
|
// if *parent_slot == *bunker_slot_index {
|
||||||
|
// return Option::Some((*parent_slot, *bunker_slot_index));
|
||||||
|
// }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_field_loop(
|
||||||
|
graph: &ActionGraph,
|
||||||
|
parent: petgraph::stable_graph::NodeIndex,
|
||||||
|
child: petgraph::stable_graph::NodeIndex,
|
||||||
|
) -> bool {
|
||||||
|
if let actions::All::Move(move_action) = graph.node_weight(parent).unwrap() {
|
||||||
|
if let actions::All::Move(child_move_action) = graph.node_weight(child).unwrap() {
|
||||||
|
debug_assert_eq!(move_action.cards(), child_move_action.cards());
|
||||||
|
debug_assert_eq!(move_action.destination, child_move_action.source);
|
||||||
|
debug_assert_eq!(move_action.stack_len(), 1);
|
||||||
|
return move_action.source == child_move_action.destination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge_step(mut graph: ActionGraph) -> ActionGraph {
|
||||||
|
let mut used_nodes = HashSet::new();
|
||||||
|
let mut mergeable = Vec::new();
|
||||||
|
let mut loop_deletion = Vec::new();
|
||||||
|
let mut bunker_loop_deletion = Vec::new();
|
||||||
|
for (index, _action) in graph.node_references() {
|
||||||
|
if used_nodes.contains(&index) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parents = get_parents(&graph, index);
|
||||||
|
if parents.len() != 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parent = parents.into_iter().next().unwrap();
|
||||||
|
if used_nodes.contains(&parent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if get_all_cards(graph.node_weight(parent).unwrap()).len() > 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if get_all_cards(graph.node_weight(index).unwrap()).len() > 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if socket_for(&graph, parent)
|
||||||
|
.into_iter()
|
||||||
|
.any(|x| return x != index)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let filtered_graph = petgraph::visit::EdgeFiltered::from_fn(&graph, |x| {
|
||||||
|
return !(x.source() == parent && x.target() == index);
|
||||||
|
});
|
||||||
|
if petgraph::algo::has_path_connecting(&filtered_graph, parent, index, Option::None) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if is_bunker_loop(&graph, parent, index) {
|
||||||
|
bunker_loop_deletion.push((parent, index));
|
||||||
|
} else if is_field_loop(&graph, parent, index) {
|
||||||
|
loop_deletion.push((parent, index));
|
||||||
|
} else {
|
||||||
|
mergeable.push((parent, index));
|
||||||
|
}
|
||||||
|
used_nodes.insert(parent);
|
||||||
|
used_nodes.insert(index);
|
||||||
|
}
|
||||||
|
for (parent, child) in mergeable {
|
||||||
|
try_merge(&mut graph, parent, child);
|
||||||
|
}
|
||||||
|
for (parent, child) in loop_deletion {
|
||||||
|
merge_edges(&mut graph, parent, child);
|
||||||
|
delete_null_node(&mut graph, child);
|
||||||
|
}
|
||||||
|
for (parent, child) in bunker_loop_deletion {
|
||||||
|
let parent_slot = if let actions::All::Bunkerize(Bunkerize {
|
||||||
|
bunker_slot_index,
|
||||||
|
to_bunker,
|
||||||
|
..
|
||||||
|
}) = &graph[parent]
|
||||||
|
{
|
||||||
|
assert!(!*to_bunker);
|
||||||
|
*bunker_slot_index
|
||||||
|
} else {
|
||||||
|
panic!("Should be bunkerize action")
|
||||||
|
};
|
||||||
|
let child_slot = if let actions::All::Bunkerize(Bunkerize {
|
||||||
|
bunker_slot_index,
|
||||||
|
to_bunker,
|
||||||
|
..
|
||||||
|
}) = &graph[child]
|
||||||
|
{
|
||||||
|
assert!(*to_bunker);
|
||||||
|
*bunker_slot_index
|
||||||
|
} else {
|
||||||
|
panic!("Should be bunkerize action")
|
||||||
|
};
|
||||||
|
flip_bunker_slots(&mut graph, parent, parent_slot, child_slot);
|
||||||
|
merge_edges(&mut graph, parent, child);
|
||||||
|
delete_null_node(&mut graph, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fix_dragonkill_destination(actions: &[actions::All]) -> Vec<actions::All> {
|
||||||
|
let graph = to_graph(actions);
|
||||||
|
let result = graph
|
||||||
|
.node_indices()
|
||||||
|
.map(|node| return graph.node_weight(node).unwrap().clone())
|
||||||
|
.collect();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fix_goal_ordering(actions: &[actions::All]) -> Vec<actions::All> {
|
||||||
|
return actions.to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn optimize(actions: &[actions::All]) -> Vec<actions::All> {
|
||||||
|
let mut graph = to_graph(actions);
|
||||||
|
let mut last_length = graph.node_count();
|
||||||
|
loop {
|
||||||
|
graph = merge_step(graph);
|
||||||
|
if graph.node_count() == last_length {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
last_length = graph.node_count();
|
||||||
|
}
|
||||||
|
|
||||||
|
let optimized_sequence = from_graph(&graph);
|
||||||
|
return fix_goal_ordering(&fix_dragonkill_destination(&optimized_sequence));
|
||||||
|
}
|
||||||
149
solver-rs/lib/action_optimization/src/relation.rs
Normal file
149
solver-rs/lib/action_optimization/src/relation.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
use super::{
|
||||||
|
graph_entity::RelationType,
|
||||||
|
util::{
|
||||||
|
get_all_bottom_sources, get_all_destinations, get_all_sources, get_all_top_sources,
|
||||||
|
get_destination, get_top_destination, search_parent_tree, top_card,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use actions::{Goal, Move};
|
||||||
|
use board::PositionNoGoal;
|
||||||
|
pub fn get_move_parents(actions: &[actions::All], current_action: usize) -> Vec<usize> {
|
||||||
|
let result = get_all_sources(actions[current_action].clone())
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|cur_source_pos| {
|
||||||
|
let is_move_parent = |other_action: &actions::All| {
|
||||||
|
let destinations =
|
||||||
|
get_all_destinations(other_action.clone())
|
||||||
|
.into_iter()
|
||||||
|
.any(|cur_dest_pos| {
|
||||||
|
return cur_dest_pos == cur_source_pos;
|
||||||
|
});
|
||||||
|
return destinations;
|
||||||
|
};
|
||||||
|
|
||||||
|
let source_action = search_parent_tree(actions, current_action, is_move_parent);
|
||||||
|
return source_action.map(|(index, _)| return index);
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
fn get_unblocking_parent(actions: &[actions::All], current_action: usize) -> Option<usize> {
|
||||||
|
let destination = get_destination(&actions[current_action])?;
|
||||||
|
let is_unblocking = |other_action: &actions::All| {
|
||||||
|
return get_all_sources(other_action.clone())
|
||||||
|
.into_iter()
|
||||||
|
.any(|source| return source == destination);
|
||||||
|
};
|
||||||
|
return search_parent_tree(actions, current_action, is_unblocking)
|
||||||
|
.filter(|&(_, found_action)| {
|
||||||
|
if let actions::All::Move(Move { ref source, .. }) = found_action {
|
||||||
|
return board::Position::Field(*source) == destination;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(|(index, _)| return index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_socket_parent(actions: &[actions::All], current_action: usize) -> Option<usize> {
|
||||||
|
let top_action = get_destination(&actions[current_action]);
|
||||||
|
|
||||||
|
if let Option::Some(board::Position::Field(top_action)) = top_action {
|
||||||
|
let is_socket = |action: &actions::All| {
|
||||||
|
let socket_destination = get_top_destination(action.clone());
|
||||||
|
if let Option::Some(board::Position::Field(destination)) = socket_destination {
|
||||||
|
return top_card(&destination) == top_action;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let added_socket =
|
||||||
|
search_parent_tree(actions, current_action, is_socket).map(|(index, _)| {
|
||||||
|
return index;
|
||||||
|
});
|
||||||
|
let unblocking_parent = get_unblocking_parent(actions, current_action);
|
||||||
|
if added_socket < unblocking_parent {
|
||||||
|
return Option::None;
|
||||||
|
} else {
|
||||||
|
return added_socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_destination_parent(
|
||||||
|
actions: &[actions::All],
|
||||||
|
current_action: usize,
|
||||||
|
) -> Option<(usize, RelationType)> {
|
||||||
|
let socket_parent = get_socket_parent(actions, current_action);
|
||||||
|
let unblock_parent = get_unblocking_parent(actions, current_action);
|
||||||
|
if socket_parent.is_none() && unblock_parent.is_none() {
|
||||||
|
return Option::None;
|
||||||
|
} else if socket_parent > unblock_parent {
|
||||||
|
return Option::Some((socket_parent.unwrap(), RelationType::Socket));
|
||||||
|
} else {
|
||||||
|
return Option::Some((unblock_parent.unwrap(), RelationType::Unblock));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actions which moved cards on top of other cards away
|
||||||
|
pub fn get_clear_parents(actions: &[actions::All], current_action: usize) -> Vec<usize> {
|
||||||
|
let filter_fields = |x: PositionNoGoal| {
|
||||||
|
if let PositionNoGoal::Field(f) = x {
|
||||||
|
return Some(f);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let source_positions = get_all_top_sources(&actions[current_action]);
|
||||||
|
|
||||||
|
let parents: Vec<usize> = source_positions
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|current_source_pos| {
|
||||||
|
let current_source_pos = filter_fields(current_source_pos)?;
|
||||||
|
let latest_moves = get_move_parents(actions, current_action);
|
||||||
|
let latest_move = if let actions::All::DragonKill(_) = actions[current_action] {
|
||||||
|
latest_moves
|
||||||
|
.into_iter()
|
||||||
|
.find(|index| {
|
||||||
|
return get_destination(&actions[*index])
|
||||||
|
== Option::Some(board::Position::Field(current_source_pos));
|
||||||
|
})
|
||||||
|
.unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
latest_moves.into_iter().max().unwrap_or(0)
|
||||||
|
};
|
||||||
|
let is_clearing = move |other_action: &actions::All| {
|
||||||
|
let sources = get_all_bottom_sources(other_action);
|
||||||
|
let clear_parent = sources
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(filter_fields)
|
||||||
|
.any(|cur_dest_pos| return top_card(¤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;
|
||||||
|
}
|
||||||
20
solver-rs/lib/action_optimization/src/test_actions.rs
Normal file
20
solver-rs/lib/action_optimization/src/test_actions.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// This is incredibly shit, as other crates call this macro with _their_ CARGO_MANIFEST_DIR. Ideally we would move
|
||||||
|
// the boards into the board crate, and use the path of the board crate. But it seems to be really hard to get this done with
|
||||||
|
// macros, and const variables can't be used by macros, so we're using this hack for now.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! TEST_ACTION_ROOT {
|
||||||
|
() => {
|
||||||
|
concat!(env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/../../aux/actions/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! load_test_action {
|
||||||
|
( $relpath:expr ) => {
|
||||||
|
{
|
||||||
|
return serde_json::from_str::<Vec<actions::All>>(include_str!(concat!($crate::TEST_ACTION_ROOT!(),
|
||||||
|
$relpath)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
66
solver-rs/lib/action_optimization/src/tests.rs
Normal file
66
solver-rs/lib/action_optimization/src/tests.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use crate::{draw_graph, graph_entity::to_graph};
|
||||||
|
use std::str::FromStr;
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
pub fn optimize_bunker_loop() {
|
||||||
|
use actions::{All, Bunkerize, Goal};
|
||||||
|
use board::FieldPosition;
|
||||||
|
let numbercard = board::NumberCard {
|
||||||
|
suit: board::NumberCardColor::Red,
|
||||||
|
value: 1,
|
||||||
|
};
|
||||||
|
let zhong_card = board::CardType::Number(numbercard.clone());
|
||||||
|
let actions = vec![
|
||||||
|
All::Bunkerize(Bunkerize {
|
||||||
|
bunker_slot_index: 0,
|
||||||
|
card: zhong_card.remove_hua(),
|
||||||
|
to_bunker: false,
|
||||||
|
field_position: FieldPosition::new(2, 0),
|
||||||
|
}),
|
||||||
|
All::Bunkerize(Bunkerize {
|
||||||
|
bunker_slot_index: 2,
|
||||||
|
card: zhong_card.remove_hua(),
|
||||||
|
to_bunker: true,
|
||||||
|
field_position: FieldPosition::new(2, 0),
|
||||||
|
}),
|
||||||
|
All::Goal(Goal {
|
||||||
|
card: numbercard,
|
||||||
|
goal_slot_index: 0,
|
||||||
|
source: board::PositionNoGoal::Bunker { slot_index: 2 },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
let graph = to_graph(&actions);
|
||||||
|
draw_graph(&graph, std::path::Path::new("unopt_bunker.svg")).unwrap();
|
||||||
|
let graph = crate::optimize::merge_step(graph);
|
||||||
|
draw_graph(&graph, std::path::Path::new("opt_bunker.svg")).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn all_boards_correct() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
for i in 1..19 {
|
||||||
|
let action_string =
|
||||||
|
std::fs::read_to_string(std::format!("{}/{:02}.json", crate::TEST_ACTION_ROOT!(), i))?;
|
||||||
|
|
||||||
|
let actions: Vec<actions::All> = serde_json::from_str(&action_string)?;
|
||||||
|
|
||||||
|
let board_string = std::fs::read_to_string(std::format!(
|
||||||
|
"{}/normal/{:02}.json",
|
||||||
|
board::TEST_BOARD_ROOT!(),
|
||||||
|
i
|
||||||
|
))?;
|
||||||
|
let src_board = board::Board::from_str(&board_string)?;
|
||||||
|
let mut board = src_board.clone();
|
||||||
|
for action in actions.iter() {
|
||||||
|
action.apply(&mut board);
|
||||||
|
}
|
||||||
|
assert!(board.solved());
|
||||||
|
let actions = crate::optimize(&actions);
|
||||||
|
let mut board = src_board;
|
||||||
|
for (index, action) in actions.into_iter().enumerate() {
|
||||||
|
println!("{}", index);
|
||||||
|
action.apply(&mut board);
|
||||||
|
}
|
||||||
|
assert!(board.solved());
|
||||||
|
}
|
||||||
|
return Result::Ok(());
|
||||||
|
}
|
||||||
173
solver-rs/lib/action_optimization/src/util.rs
Normal file
173
solver-rs/lib/action_optimization/src/util.rs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
use actions::{Bunkerize, DragonKill, Goal, HuaKill, Move};
|
||||||
|
use board::{CardType, FieldPosition};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
fn node_name(index: usize) -> String {
|
||||||
|
return format!("action_{:04}", index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Position on top of this position (increments `position.row_index` by one)
|
||||||
|
pub fn top_card(position: &FieldPosition) -> FieldPosition {
|
||||||
|
return FieldPosition::new(position.column(), position.row() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn column_range(position: &FieldPosition, count: usize) -> Vec<FieldPosition> {
|
||||||
|
return (0..count)
|
||||||
|
.map(|i| {
|
||||||
|
return FieldPosition::new(
|
||||||
|
position.column(),
|
||||||
|
position.row() + u8::try_from(i).unwrap(),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_sources(action: actions::All) -> Vec<board::PositionNoGoal> {
|
||||||
|
match action {
|
||||||
|
actions::All::Bunkerize(Bunkerize {
|
||||||
|
bunker_slot_index,
|
||||||
|
to_bunker,
|
||||||
|
field_position,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
if to_bunker {
|
||||||
|
return vec![board::PositionNoGoal::Field(field_position)];
|
||||||
|
} else {
|
||||||
|
return vec![board::PositionNoGoal::Bunker {
|
||||||
|
slot_index: bunker_slot_index,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actions::All::DragonKill(DragonKill { source, .. }) => {
|
||||||
|
return source.to_vec();
|
||||||
|
}
|
||||||
|
actions::All::Goal(Goal { source, .. }) => {
|
||||||
|
return vec![source];
|
||||||
|
}
|
||||||
|
actions::All::HuaKill(HuaKill { field_position }) => {
|
||||||
|
return vec![board::PositionNoGoal::Field(field_position)]
|
||||||
|
}
|
||||||
|
actions::All::Move(move_action) => {
|
||||||
|
return column_range(&move_action.source, usize::from(move_action.stack_len()))
|
||||||
|
.into_iter()
|
||||||
|
.map(board::PositionNoGoal::Field)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_top_sources(action: &actions::All) -> Vec<board::PositionNoGoal> {
|
||||||
|
if let actions::All::Move(move_action) = &action {
|
||||||
|
return vec![board::PositionNoGoal::Field(FieldPosition::new(
|
||||||
|
move_action.source.column(),
|
||||||
|
move_action.source.row() + move_action.stack_len() - 1,
|
||||||
|
))];
|
||||||
|
} else {
|
||||||
|
return get_all_sources(action.clone());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_bottom_sources(action: &actions::All) -> Vec<board::PositionNoGoal> {
|
||||||
|
if let actions::All::Move(Move { source, .. }) = &action {
|
||||||
|
return vec![board::PositionNoGoal::Field(*source)];
|
||||||
|
} else {
|
||||||
|
return get_all_sources(action.clone());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_cards(action: &actions::All) -> Vec<board::CardType> {
|
||||||
|
match action {
|
||||||
|
actions::All::Bunkerize(Bunkerize { card, .. }) => return vec![card.add_hua()], /* Does this actually work? */
|
||||||
|
actions::All::DragonKill(DragonKill { card, .. }) => {
|
||||||
|
return vec![
|
||||||
|
CardType::Special(card.clone()),
|
||||||
|
CardType::Special(card.clone()),
|
||||||
|
CardType::Special(card.clone()),
|
||||||
|
CardType::Special(card.clone()),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
actions::All::Goal(Goal { card, .. }) => return vec![CardType::Number(card.clone())],
|
||||||
|
actions::All::HuaKill(_) => return vec![CardType::Hua],
|
||||||
|
actions::All::Move(move_action) => return move_action.cards(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_destination(action: &actions::All) -> Option<board::Position> {
|
||||||
|
match action {
|
||||||
|
actions::All::Bunkerize(Bunkerize {
|
||||||
|
field_position,
|
||||||
|
to_bunker,
|
||||||
|
bunker_slot_index,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
if *to_bunker {
|
||||||
|
return Option::Some(board::Position::Bunker {
|
||||||
|
slot_index: *bunker_slot_index,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Option::Some(board::Position::Field(*field_position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actions::All::DragonKill(DragonKill {
|
||||||
|
destination_slot_index,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
return Option::Some(board::Position::Bunker {
|
||||||
|
slot_index: *destination_slot_index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
actions::All::Goal(Goal {
|
||||||
|
goal_slot_index, ..
|
||||||
|
}) => {
|
||||||
|
return Option::Some(board::Position::Goal {
|
||||||
|
slot_index: *goal_slot_index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
actions::All::HuaKill(_) => return Option::None,
|
||||||
|
actions::All::Move(Move { destination, .. }) => {
|
||||||
|
return Option::Some(board::Position::Field(*destination));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the destination of a move, or the topmost card in its destination when moving multiple cards
|
||||||
|
pub fn get_top_destination(action: actions::All) -> Option<board::Position> {
|
||||||
|
if let actions::All::Move(move_action) = action {
|
||||||
|
return Option::Some(board::Position::Field(FieldPosition::new(
|
||||||
|
move_action.destination.column(),
|
||||||
|
move_action.destination.row() + move_action.stack_len() - 1,
|
||||||
|
)));
|
||||||
|
} else {
|
||||||
|
return get_destination(&action);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_destinations(action: actions::All) -> Vec<board::Position> {
|
||||||
|
if let actions::All::Move(move_action) = action {
|
||||||
|
return column_range(
|
||||||
|
&move_action.destination,
|
||||||
|
usize::from(move_action.stack_len()),
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(board::Position::Field)
|
||||||
|
.collect();
|
||||||
|
} else {
|
||||||
|
return get_destination(&action).into_iter().collect();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_parent_tree<F>(
|
||||||
|
actions: &[actions::All],
|
||||||
|
current_action: usize,
|
||||||
|
predicate: F,
|
||||||
|
) -> Option<(usize, &actions::All)>
|
||||||
|
where
|
||||||
|
F: Fn(&actions::All) -> bool,
|
||||||
|
{
|
||||||
|
return actions
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.take(current_action)
|
||||||
|
.rev()
|
||||||
|
.find(|&(_, action)| return predicate(action));
|
||||||
|
}
|
||||||
14
solver-rs/lib/actions/Cargo.toml
Normal file
14
solver-rs/lib/actions/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "actions"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Lukas Wölfer <lukas.woelfer@rwth-aachen.de>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = {version="1.0.105",features=["derive"]}
|
||||||
|
serde_json = "1.0"
|
||||||
|
enum-iterator = "0.6.0"
|
||||||
|
|
||||||
|
board = {path = "../board"}
|
||||||
405
solver-rs/lib/actions/src/base.rs
Normal file
405
solver-rs/lib/actions/src/base.rs
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
use board::{
|
||||||
|
Board, BunkerSlot, CardType, CardTypeNoHua, FieldPosition, NumberCard, PositionNoGoal,
|
||||||
|
SpecialCardType,
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub(super) trait BoardApplication {
|
||||||
|
fn apply(&self, solboard: &mut Board);
|
||||||
|
fn undo(&self, solboard: &mut Board);
|
||||||
|
fn can_apply(&self, solboard: &Board) -> bool;
|
||||||
|
fn can_undo(&self, solboard: &Board) -> bool;
|
||||||
|
fn checked_apply(&self, solboard: &mut Board) -> bool {
|
||||||
|
if self.can_apply(solboard) {
|
||||||
|
self.apply(solboard);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fn checked_undo(&self, solboard: &mut Board) -> bool {
|
||||||
|
if self.can_undo(solboard) {
|
||||||
|
self.undo(solboard);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_pop_top(solboard: &Board, position: &PositionNoGoal, card: &CardType) -> bool {
|
||||||
|
match position {
|
||||||
|
PositionNoGoal::Field(fieldpos) => {
|
||||||
|
if solboard.field[usize::from(fieldpos.column())]
|
||||||
|
.last()
|
||||||
|
.expect("Trying to pop top of empty field stack")
|
||||||
|
!= card
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PositionNoGoal::Bunker { slot_index } => {
|
||||||
|
if solboard.bunker[usize::from(*slot_index)] != BunkerSlot::Stash(card.remove_hua()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_top(solboard: &mut Board, position: &PositionNoGoal, card: &CardType) {
|
||||||
|
debug_assert!(can_pop_top(solboard, position, card));
|
||||||
|
match position {
|
||||||
|
PositionNoGoal::Field(fieldpos) => {
|
||||||
|
solboard
|
||||||
|
.field
|
||||||
|
.get_mut(usize::from(fieldpos.column()))
|
||||||
|
.expect("Column index fucked")
|
||||||
|
.pop();
|
||||||
|
}
|
||||||
|
PositionNoGoal::Bunker { slot_index } => {
|
||||||
|
solboard.bunker[usize::from(*slot_index)] = BunkerSlot::Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Goal {
|
||||||
|
pub card: NumberCard,
|
||||||
|
pub source: PositionNoGoal,
|
||||||
|
pub goal_slot_index: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoardApplication for Goal {
|
||||||
|
fn can_apply(&self, solboard: &Board) -> bool {
|
||||||
|
match &solboard.goal[usize::from(self.goal_slot_index)] {
|
||||||
|
Option::Some(NumberCard { value, suit }) => {
|
||||||
|
if self.card.value != *value + 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.card.suit != *suit {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Option::None => {
|
||||||
|
if self.card.value != 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !can_pop_top(solboard, &self.source, &CardType::Number(self.card.clone())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
fn can_undo(&self, _solboard: &Board) -> bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
fn apply(&self, solboard: &mut Board) {
|
||||||
|
pop_top(solboard, &self.source, &CardType::Number(self.card.clone()));
|
||||||
|
*solboard
|
||||||
|
.goal
|
||||||
|
.get_mut(usize::from(self.goal_slot_index))
|
||||||
|
.expect("Slot index fucked") = Option::Some(self.card.clone());
|
||||||
|
}
|
||||||
|
fn undo(&self, solboard: &mut Board) {
|
||||||
|
match &self.source {
|
||||||
|
PositionNoGoal::Field(position) => {
|
||||||
|
solboard
|
||||||
|
.field
|
||||||
|
.get_mut(usize::from(position.column()))
|
||||||
|
.expect("Column index fucked")
|
||||||
|
.push(CardType::Number(self.card.clone()));
|
||||||
|
}
|
||||||
|
PositionNoGoal::Bunker { slot_index } => {
|
||||||
|
solboard.bunker[usize::from(*slot_index)] =
|
||||||
|
BunkerSlot::Stash(CardTypeNoHua::Number(self.card.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.card.value == 1 {
|
||||||
|
solboard.goal[usize::from(self.goal_slot_index)] = Option::None;
|
||||||
|
} else {
|
||||||
|
solboard.goal[usize::from(self.goal_slot_index)] = Option::Some(NumberCard {
|
||||||
|
suit: self.card.suit.clone(),
|
||||||
|
value: self.card.value - 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Goal {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
return write!(
|
||||||
|
f,
|
||||||
|
"Goal {} from {} to slot #{}",
|
||||||
|
self.card, self.source, self.goal_slot_index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DragonKill {
|
||||||
|
pub card: SpecialCardType,
|
||||||
|
pub source: [PositionNoGoal; 4],
|
||||||
|
pub destination_slot_index: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoardApplication for DragonKill {
|
||||||
|
fn apply(&self, solboard: &mut Board) {
|
||||||
|
for position in &self.source {
|
||||||
|
pop_top(solboard, position, &CardType::Special(self.card.clone()));
|
||||||
|
}
|
||||||
|
solboard.bunker[usize::from(self.destination_slot_index)] =
|
||||||
|
BunkerSlot::Blocked(Option::Some(self.card.clone()));
|
||||||
|
}
|
||||||
|
fn undo(&self, solboard: &mut Board) {
|
||||||
|
solboard.bunker[usize::from(self.destination_slot_index)] = BunkerSlot::Empty;
|
||||||
|
for position in &self.source {
|
||||||
|
match &position {
|
||||||
|
PositionNoGoal::Field(field_position) => {
|
||||||
|
solboard.field[usize::from(field_position.column())]
|
||||||
|
.push(CardType::Special(self.card.clone()));
|
||||||
|
}
|
||||||
|
PositionNoGoal::Bunker { slot_index } => {
|
||||||
|
solboard.bunker[usize::from(*slot_index)] =
|
||||||
|
BunkerSlot::Stash(CardTypeNoHua::Special(self.card.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn can_apply(&self, solboard: &Board) -> bool {
|
||||||
|
if self.destination_slot_index >= 3 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let previous_slot_empty = solboard
|
||||||
|
.bunker
|
||||||
|
.iter()
|
||||||
|
.take(self.destination_slot_index.saturating_sub(1).into())
|
||||||
|
.all(|x| {
|
||||||
|
if let BunkerSlot::Empty = x {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if previous_slot_empty {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
fn can_undo(&self, _solboard: &Board) -> bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DragonKill {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
return write!(
|
||||||
|
f,
|
||||||
|
"Kill {} to bunker #{} from {}, {}, {}, {}",
|
||||||
|
self.card,
|
||||||
|
self.destination_slot_index,
|
||||||
|
self.source[0],
|
||||||
|
self.source[1],
|
||||||
|
self.source[2],
|
||||||
|
self.source[3],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Bunkerize {
|
||||||
|
pub card: CardTypeNoHua,
|
||||||
|
pub bunker_slot_index: u8,
|
||||||
|
pub field_position: FieldPosition,
|
||||||
|
pub to_bunker: bool,
|
||||||
|
}
|
||||||
|
impl Bunkerize {
|
||||||
|
fn can_move_to_bunker(&self, solboard: &Board) -> bool {
|
||||||
|
if self.field_position.row() + 1
|
||||||
|
!= solboard.field[usize::from(self.field_position.column())].len() as u8
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.card.add_hua()
|
||||||
|
!= *solboard.field[usize::from(self.field_position.column())]
|
||||||
|
.last()
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if solboard.bunker[usize::from(self.bunker_slot_index)] != BunkerSlot::Empty {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
fn move_to_bunker(&self, solboard: &mut Board) {
|
||||||
|
debug_assert!(self.can_move_to_bunker(solboard));
|
||||||
|
solboard.field[usize::from(self.field_position.column())].pop();
|
||||||
|
solboard.bunker[usize::from(self.bunker_slot_index)] = BunkerSlot::Stash(self.card.clone());
|
||||||
|
}
|
||||||
|
fn can_move_from_bunker(&self, solboard: &Board) -> bool {
|
||||||
|
if solboard.bunker[usize::from(self.bunker_slot_index)]
|
||||||
|
!= BunkerSlot::Stash(self.card.clone())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.field_position.row()
|
||||||
|
!= solboard.field[usize::from(self.field_position.column())].len() as u8
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
fn move_from_bunker(&self, solboard: &mut Board) {
|
||||||
|
debug_assert!(self.can_move_from_bunker(solboard));
|
||||||
|
solboard.field[usize::from(self.field_position.column())].push(self.card.add_hua());
|
||||||
|
solboard.bunker[usize::from(self.bunker_slot_index)] = BunkerSlot::Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoardApplication for Bunkerize {
|
||||||
|
fn apply(&self, solboard: &mut Board) {
|
||||||
|
if self.to_bunker {
|
||||||
|
self.move_to_bunker(solboard);
|
||||||
|
} else {
|
||||||
|
self.move_from_bunker(solboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn undo(&self, solboard: &mut Board) {
|
||||||
|
if self.to_bunker {
|
||||||
|
self.move_from_bunker(solboard);
|
||||||
|
} else {
|
||||||
|
self.move_to_bunker(solboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn can_apply(&self, solboard: &Board) -> bool {
|
||||||
|
if self.to_bunker {
|
||||||
|
return self.can_move_to_bunker(solboard);
|
||||||
|
} else {
|
||||||
|
return self.can_move_from_bunker(solboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn can_undo(&self, solboard: &Board) -> bool {
|
||||||
|
if self.to_bunker {
|
||||||
|
return self.can_move_from_bunker(solboard);
|
||||||
|
} else {
|
||||||
|
return self.can_move_to_bunker(solboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Bunkerize {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
if self.to_bunker {
|
||||||
|
return write!(
|
||||||
|
f,
|
||||||
|
"Move {} from {} to bunker #{}",
|
||||||
|
self.card, self.field_position, self.bunker_slot_index,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return write!(
|
||||||
|
f,
|
||||||
|
"Move {} from bunker #{} to {}",
|
||||||
|
self.card, self.bunker_slot_index, self.field_position,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
|
pub struct HuaKill {
|
||||||
|
pub field_position: FieldPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoardApplication for HuaKill {
|
||||||
|
fn can_apply(&self, solboard: &Board) -> bool {
|
||||||
|
if solboard.field[usize::from(self.field_position.column())].last()
|
||||||
|
!= Option::Some(&CardType::Hua)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if solboard.field[usize::from(self.field_position.column())].len()
|
||||||
|
!= (self.field_position.row() + 1) as usize
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
fn apply(&self, solboard: &mut Board) {
|
||||||
|
debug_assert!(self.can_apply(solboard));
|
||||||
|
solboard.field[usize::from(self.field_position.column())].pop();
|
||||||
|
solboard.hua_set = true;
|
||||||
|
}
|
||||||
|
fn can_undo(&self, solboard: &Board) -> bool {
|
||||||
|
if solboard.field[usize::from(self.field_position.column())].len()
|
||||||
|
!= self.field_position.row() as usize
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
fn undo(&self, solboard: &mut Board) {
|
||||||
|
debug_assert!(self.can_undo(solboard));
|
||||||
|
solboard.field[usize::from(self.field_position.column())].push(CardType::Hua);
|
||||||
|
solboard.hua_set = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::fmt::Display for HuaKill {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
return write!(f, "Kill hua from {}", self.field_position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
|
pub enum All {
|
||||||
|
Bunkerize(Bunkerize),
|
||||||
|
DragonKill(DragonKill),
|
||||||
|
Goal(Goal),
|
||||||
|
HuaKill(HuaKill),
|
||||||
|
Move(super::Move),
|
||||||
|
}
|
||||||
|
impl std::fmt::Display for All {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Bunkerize(x) => return write!(f, "{}", x),
|
||||||
|
Self::DragonKill(x) => return write!(f, "{}", x),
|
||||||
|
Self::Goal(x) => return write!(f, "{}", x),
|
||||||
|
Self::HuaKill(x) => return write!(f, "{}", x),
|
||||||
|
Self::Move(x) => return write!(f, "{}", x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl All {
|
||||||
|
pub fn apply(&self, solboard: &mut Board) {
|
||||||
|
match self {
|
||||||
|
Self::HuaKill(obj) => {
|
||||||
|
obj.apply(solboard);
|
||||||
|
}
|
||||||
|
Self::DragonKill(obj) => {
|
||||||
|
obj.apply(solboard);
|
||||||
|
}
|
||||||
|
Self::Goal(obj) => {
|
||||||
|
obj.apply(solboard);
|
||||||
|
}
|
||||||
|
Self::Bunkerize(obj) => {
|
||||||
|
obj.apply(solboard);
|
||||||
|
}
|
||||||
|
Self::Move(obj) => obj.apply(solboard),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn undo(&self, solboard: &mut Board) {
|
||||||
|
match self {
|
||||||
|
Self::HuaKill(obj) => {
|
||||||
|
obj.undo(solboard);
|
||||||
|
}
|
||||||
|
Self::DragonKill(obj) => {
|
||||||
|
obj.undo(solboard);
|
||||||
|
}
|
||||||
|
Self::Goal(obj) => {
|
||||||
|
obj.undo(solboard);
|
||||||
|
}
|
||||||
|
Self::Bunkerize(obj) => {
|
||||||
|
obj.undo(solboard);
|
||||||
|
}
|
||||||
|
Self::Move(obj) => obj.undo(solboard),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
solver-rs/lib/actions/src/lib.rs
Normal file
49
solver-rs/lib/actions/src/lib.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#![warn(
|
||||||
|
clippy::all,
|
||||||
|
clippy::restriction,
|
||||||
|
clippy::pedantic,
|
||||||
|
clippy::nursery,
|
||||||
|
clippy::cargo
|
||||||
|
)]
|
||||||
|
#![allow(clippy::cargo)]
|
||||||
|
// Style choices
|
||||||
|
#![allow(
|
||||||
|
clippy::missing_docs_in_private_items,
|
||||||
|
clippy::needless_return,
|
||||||
|
clippy::get_unwrap,
|
||||||
|
clippy::indexing_slicing,
|
||||||
|
clippy::explicit_iter_loop
|
||||||
|
)]
|
||||||
|
// Way too pedantic
|
||||||
|
#![allow(clippy::integer_arithmetic)]
|
||||||
|
// Useless
|
||||||
|
#![allow(clippy::missing_inline_in_public_items, clippy::missing_const_for_fn)]
|
||||||
|
// Useful for production
|
||||||
|
#![allow(
|
||||||
|
clippy::use_debug,
|
||||||
|
clippy::print_stdout,
|
||||||
|
clippy::dbg_macro,
|
||||||
|
clippy::panic
|
||||||
|
)]
|
||||||
|
// Useful for improving code robustness
|
||||||
|
#![allow(
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_possible_wrap,
|
||||||
|
clippy::option_unwrap_used,
|
||||||
|
clippy::option_expect_used,
|
||||||
|
clippy::as_conversions,
|
||||||
|
// clippy::result_unwrap_used,
|
||||||
|
// clippy::wildcard_enum_match_arm
|
||||||
|
)]
|
||||||
|
#![allow(clippy::trivially_copy_pass_by_ref)]
|
||||||
|
#![allow(dead_code)]
|
||||||
|
mod base;
|
||||||
|
pub use base::*;
|
||||||
|
|
||||||
|
mod move_action;
|
||||||
|
pub use move_action::*;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
pub mod possibilities;
|
||||||
178
solver-rs/lib/actions/src/move_action.rs
Normal file
178
solver-rs/lib/actions/src/move_action.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
use board::{Board, CardType, FieldPosition, NumberCard, NumberCardColor};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
const COLOR_SEQUENCE: [NumberCardColor; 3] = [
|
||||||
|
NumberCardColor::Red,
|
||||||
|
NumberCardColor::Green,
|
||||||
|
NumberCardColor::Black,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Move {
|
||||||
|
start_card: CardType,
|
||||||
|
stack_len: u8,
|
||||||
|
pattern: u8,
|
||||||
|
pub source: FieldPosition,
|
||||||
|
pub destination: FieldPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Move {
|
||||||
|
#[must_use]
|
||||||
|
fn alternate_card(bottom_suit: &NumberCardColor, bit: u8) -> NumberCardColor {
|
||||||
|
let pos = COLOR_SEQUENCE
|
||||||
|
.iter()
|
||||||
|
.position(|x| return x == bottom_suit)
|
||||||
|
.unwrap();
|
||||||
|
let shift_value = if bit == 0 { 0 } else { 1 };
|
||||||
|
return COLOR_SEQUENCE[(pos + shift_value + 1) % 3].clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn bit_card(last_card: &board::NumberCardColor, current_card: &board::NumberCardColor) -> u8 {
|
||||||
|
let last_pos = COLOR_SEQUENCE
|
||||||
|
.iter()
|
||||||
|
.position(|x| return x == last_card)
|
||||||
|
.unwrap();
|
||||||
|
let current_pos = COLOR_SEQUENCE
|
||||||
|
.iter()
|
||||||
|
.position(|x| return x == current_card)
|
||||||
|
.unwrap();
|
||||||
|
if (last_pos + 1) % 3 == current_pos {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[must_use]
|
||||||
|
pub fn cards(&self) -> Vec<CardType> {
|
||||||
|
if let CardType::Number(NumberCard { value, .. }) = self.start_card {
|
||||||
|
let mut result = Vec::with_capacity(usize::from(self.stack_len));
|
||||||
|
result.push(self.start_card.clone());
|
||||||
|
for index in 1..self.stack_len {
|
||||||
|
let new_color = if let board::CardType::Number(board::NumberCard {
|
||||||
|
suit: last_suit,
|
||||||
|
..
|
||||||
|
}) = result.last().unwrap()
|
||||||
|
{
|
||||||
|
Self::alternate_card(last_suit, self.pattern & (1 << (index - 1)))
|
||||||
|
} else {
|
||||||
|
panic!("");
|
||||||
|
};
|
||||||
|
result.push(board::CardType::Number(board::NumberCard {
|
||||||
|
suit: new_color,
|
||||||
|
value: value - index,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return vec![self.start_card.clone()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn stack_len(&self) -> u8 {
|
||||||
|
return self.stack_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn new<'a>(
|
||||||
|
source: FieldPosition,
|
||||||
|
destination: FieldPosition,
|
||||||
|
cards: &'a [board::CardType],
|
||||||
|
) -> Self {
|
||||||
|
let mut pattern: u8 = 0;
|
||||||
|
let numbercard_filter = |card: &'a CardType| -> Option<&'a NumberCard> {
|
||||||
|
if let board::CardType::Number(numbercard) = card {
|
||||||
|
return Option::Some(numbercard);
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (index, (last_card, card)) in (0_u8..).zip(
|
||||||
|
cards
|
||||||
|
.iter()
|
||||||
|
.filter_map(numbercard_filter)
|
||||||
|
.zip(cards.iter().skip(1).filter_map(numbercard_filter)),
|
||||||
|
) {
|
||||||
|
pattern |= Self::bit_card(&last_card.suit, &card.suit) << index;
|
||||||
|
debug_assert_eq!(card.value + 1, last_card.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Self {
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
start_card: cards[0].clone(),
|
||||||
|
stack_len: cards.len() as u8,
|
||||||
|
pattern,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::BoardApplication for Move {
|
||||||
|
fn apply(&self, solboard: &mut Board) {
|
||||||
|
solboard.field[usize::from(self.source.column())].truncate(
|
||||||
|
solboard.field[usize::from(self.source.column())].len() - usize::from(self.stack_len()),
|
||||||
|
);
|
||||||
|
solboard.field[usize::from(self.destination.column())].append(&mut self.cards());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn undo(&self, solboard: &mut Board) {
|
||||||
|
solboard.field[usize::from(self.destination.column())].truncate(
|
||||||
|
solboard.field[usize::from(self.destination.column())].len()
|
||||||
|
- usize::from(self.stack_len()),
|
||||||
|
);
|
||||||
|
solboard.field[usize::from(self.source.column())].append(&mut self.cards());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn can_apply(&self, _solboard: &Board) -> bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn can_undo(&self, _solboard: &Board) -> bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Move {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
let card_name = if self.stack_len() == 1 {
|
||||||
|
format!("{}", self.cards()[0])
|
||||||
|
} else {
|
||||||
|
format!("{} cards", self.stack_len())
|
||||||
|
};
|
||||||
|
return write!(
|
||||||
|
f,
|
||||||
|
"Move {} from {} to {}",
|
||||||
|
card_name, self.source, self.destination
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_storage() {
|
||||||
|
let card_stack = vec![
|
||||||
|
board::CardType::Number(NumberCard {
|
||||||
|
value: 5,
|
||||||
|
suit: board::NumberCardColor::Red,
|
||||||
|
}),
|
||||||
|
board::CardType::Number(NumberCard {
|
||||||
|
value: 4,
|
||||||
|
suit: board::NumberCardColor::Black,
|
||||||
|
}),
|
||||||
|
board::CardType::Number(NumberCard {
|
||||||
|
value: 3,
|
||||||
|
suit: board::NumberCardColor::Green,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
let source = FieldPosition::new(0, 0);
|
||||||
|
let destination = FieldPosition::new(0, 1);
|
||||||
|
let my_move = Move::new(source.clone(), destination.clone(), &card_stack);
|
||||||
|
assert_eq!(my_move.cards(), card_stack);
|
||||||
|
let my_move = Move::new(source, destination, &card_stack[0..1]);
|
||||||
|
assert_eq!(
|
||||||
|
my_move.cards().iter().collect::<Vec<_>>(),
|
||||||
|
card_stack.iter().take(1).collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
}
|
||||||
373
solver-rs/lib/actions/src/possibilities.rs
Normal file
373
solver-rs/lib/actions/src/possibilities.rs
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
use board::{
|
||||||
|
Board, BunkerSlot, CardType, CardTypeNoHua, FieldPosition, NumberCard, NumberCardColor,
|
||||||
|
PositionNoGoal, SpecialCardType,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn bunkerize_actions(solboard: &Board) -> Vec<crate::All> {
|
||||||
|
let first_empty_bunker_index = solboard.bunker.iter().position(|x| match x {
|
||||||
|
BunkerSlot::Empty => return true,
|
||||||
|
_ => return false,
|
||||||
|
});
|
||||||
|
if let Option::Some(first_empty_bunker_index) = first_empty_bunker_index {
|
||||||
|
return solboard
|
||||||
|
.field
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, row)| {
|
||||||
|
return row
|
||||||
|
.last()
|
||||||
|
.filter(|card| {
|
||||||
|
if let CardType::Hua = card {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|card| {
|
||||||
|
let field_position = FieldPosition::new(index as u8, (row.len() - 1) as u8);
|
||||||
|
return crate::All::Bunkerize(crate::Bunkerize {
|
||||||
|
field_position,
|
||||||
|
card: card.remove_hua(),
|
||||||
|
bunker_slot_index: first_empty_bunker_index as u8,
|
||||||
|
to_bunker: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn card_fits(source: &NumberCard, dest: &NumberCard) -> bool {
|
||||||
|
return (source.suit != dest.suit) && (source.value + 1 == dest.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fitting_field_number_position(card: &NumberCard, board: &Board) -> Option<FieldPosition> {
|
||||||
|
return board.field.iter().enumerate().find_map(|(index, row)| {
|
||||||
|
if let Option::Some(CardType::Number(top_card)) = row.last() {
|
||||||
|
if card_fits(card, top_card) {
|
||||||
|
return Option::Some(FieldPosition::new(index as u8, (row.len()) as u8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Option::None;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fitting_field_positions(card: &CardType, board: &Board) -> Vec<FieldPosition> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
if let CardType::Number(card) = card {
|
||||||
|
if let Option::Some(position) = fitting_field_number_position(card, board) {
|
||||||
|
result.push(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Option::Some(position) =
|
||||||
|
(0_u8..)
|
||||||
|
.zip(board.field.iter())
|
||||||
|
.find_map(|(column_index, slot)| {
|
||||||
|
if slot.is_empty() {
|
||||||
|
return Option::Some(FieldPosition::new(column_index, 0));
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
result.push(position)
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn debunkerize_actions(solboard: &Board) -> Vec<crate::All> {
|
||||||
|
let number_matching_cards =
|
||||||
|
(0_u8..)
|
||||||
|
.zip(solboard.bunker.iter())
|
||||||
|
.filter_map(|(bunker_slot_index, slot)| {
|
||||||
|
if let BunkerSlot::Stash(CardTypeNoHua::Number(card)) = slot {
|
||||||
|
return fitting_field_number_position(card, solboard).map(|field_position| {
|
||||||
|
return crate::All::Bunkerize(crate::Bunkerize {
|
||||||
|
card: CardTypeNoHua::Number(card.clone()),
|
||||||
|
field_position,
|
||||||
|
bunker_slot_index,
|
||||||
|
to_bunker: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let empty_slot = solboard
|
||||||
|
.field
|
||||||
|
.iter()
|
||||||
|
.position(|row| return row.is_empty())
|
||||||
|
.map(|column_index| {
|
||||||
|
return FieldPosition::new(column_index as u8, 0);
|
||||||
|
});
|
||||||
|
if let Option::Some(field_position) = empty_slot {
|
||||||
|
let empty_slot_cards =
|
||||||
|
(0_u8..)
|
||||||
|
.zip(solboard.bunker.iter())
|
||||||
|
.filter_map(|(bunker_slot_index, slot)| {
|
||||||
|
if let BunkerSlot::Stash(card) = slot {
|
||||||
|
let result = crate::Bunkerize {
|
||||||
|
card: card.clone(),
|
||||||
|
bunker_slot_index,
|
||||||
|
field_position,
|
||||||
|
to_bunker: false,
|
||||||
|
};
|
||||||
|
return Option::Some(crate::All::Bunkerize(result));
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return number_matching_cards.chain(empty_slot_cards).collect();
|
||||||
|
} else {
|
||||||
|
return number_matching_cards.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DragonTracker {
|
||||||
|
dragons: [(u8, [PositionNoGoal; 4]); 3],
|
||||||
|
}
|
||||||
|
impl DragonTracker {
|
||||||
|
fn new() -> Self {
|
||||||
|
return Self {
|
||||||
|
dragons: [(0, [PositionNoGoal::Bunker { slot_index: 0 }; 4]); 3],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dragon_to_id(dragon: &SpecialCardType) -> u8 {
|
||||||
|
return match dragon {
|
||||||
|
SpecialCardType::Zhong => 0,
|
||||||
|
SpecialCardType::Bai => 1,
|
||||||
|
SpecialCardType::Fa => 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fn id_to_dragon(id: u8) -> SpecialCardType {
|
||||||
|
return match id {
|
||||||
|
0 => SpecialCardType::Zhong,
|
||||||
|
1 => SpecialCardType::Bai,
|
||||||
|
2 => SpecialCardType::Fa,
|
||||||
|
_ => panic!("Dragon id too high"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, dragon: &SpecialCardType, position: PositionNoGoal) {
|
||||||
|
let (ref mut count, ref mut cell) = self.dragons[usize::from(Self::dragon_to_id(dragon))];
|
||||||
|
cell[usize::from(*count)] = position;
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn found_dragons(&self) -> impl Iterator<Item = (SpecialCardType, &[PositionNoGoal; 4])> {
|
||||||
|
return (0_u8..)
|
||||||
|
.zip(self.dragons.iter())
|
||||||
|
.filter_map(|(index, (count, positions))| {
|
||||||
|
if *count == 4 {
|
||||||
|
return Option::Some((Self::id_to_dragon(index), positions));
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn dragonkill_actions(solboard: &Board) -> Vec<crate::All> {
|
||||||
|
let mut dragon_position = DragonTracker::new();
|
||||||
|
for (position, card) in solboard.movable_cards() {
|
||||||
|
if let CardType::Special(card) = card {
|
||||||
|
dragon_position.push(&card, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut result: Vec<crate::All> = Vec::new();
|
||||||
|
for (card_type, positions) in dragon_position.found_dragons() {
|
||||||
|
let dragon_destination = solboard.bunker.iter().position(|x| {
|
||||||
|
return match x {
|
||||||
|
BunkerSlot::Empty => true,
|
||||||
|
BunkerSlot::Stash(CardTypeNoHua::Special(special_card_type)) => {
|
||||||
|
special_card_type == &card_type
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if let Option::Some(dragon_destination) = dragon_destination {
|
||||||
|
let mut my_positions: [PositionNoGoal; 4] =
|
||||||
|
[PositionNoGoal::Bunker { slot_index: 0 }; 4];
|
||||||
|
my_positions.clone_from_slice(positions);
|
||||||
|
result.push(crate::All::DragonKill(crate::DragonKill {
|
||||||
|
card: card_type.clone(),
|
||||||
|
source: my_positions,
|
||||||
|
destination_slot_index: dragon_destination as u8,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_max_stack_count(board: &Board) -> [u8; 8] {
|
||||||
|
let mut result = [0; 8];
|
||||||
|
for (index, row) in result.iter_mut().zip(&board.field) {
|
||||||
|
let row_iterator = row.iter().rev();
|
||||||
|
let mut next_row_iterator = row.iter().rev();
|
||||||
|
if next_row_iterator.next().is_none() {
|
||||||
|
*index = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
*index = (row_iterator
|
||||||
|
.zip(next_row_iterator)
|
||||||
|
.take_while(|(card, bottom_card)| {
|
||||||
|
if let (CardType::Number(card), CardType::Number(bottom_card)) = (card, bottom_card)
|
||||||
|
{
|
||||||
|
return card_fits(card, bottom_card);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
+ 1) as u8;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn field_move_actions(solboard: &Board) -> Vec<crate::All> {
|
||||||
|
let max_stack_counts: [u8; 8] = get_max_stack_count(solboard);
|
||||||
|
let required_size: u8 = max_stack_counts.iter().cloned().sum();
|
||||||
|
let mut result = Vec::with_capacity(usize::from(required_size));
|
||||||
|
for ((column_index, row), stack_size) in (0_u8..)
|
||||||
|
.zip(solboard.field.iter())
|
||||||
|
.zip(max_stack_counts.iter())
|
||||||
|
.filter(|(_, size)| return **size > 0)
|
||||||
|
{
|
||||||
|
for row_index in (row.len() - usize::from(*stack_size)) as u8..(row.len()) as u8 {
|
||||||
|
let my_stack = &row
|
||||||
|
.get(usize::from(row_index)..row.len())
|
||||||
|
.expect("Slicing failed");
|
||||||
|
for position in fitting_field_positions(
|
||||||
|
my_stack
|
||||||
|
.first()
|
||||||
|
.expect("Stack should at least have one entry"),
|
||||||
|
solboard,
|
||||||
|
) {
|
||||||
|
result.push(crate::All::Move(crate::Move::new(
|
||||||
|
FieldPosition::new(column_index, row_index),
|
||||||
|
position,
|
||||||
|
my_stack,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn goal_move_actions(solboard: &Board) -> Vec<crate::All> {
|
||||||
|
let suit_to_id = |suit: &NumberCardColor| -> u8 {
|
||||||
|
return match suit {
|
||||||
|
NumberCardColor::Red => 0,
|
||||||
|
NumberCardColor::Green => 1,
|
||||||
|
NumberCardColor::Black => 2,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let first_empty_goal_slot_index = (0_u8..)
|
||||||
|
.zip(solboard.goal.iter())
|
||||||
|
.find_map(|(index, card)| {
|
||||||
|
if card.is_none() {
|
||||||
|
return Option::Some(index);
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(3);
|
||||||
|
let mut goal_desired_pos = [(1_u8, first_empty_goal_slot_index); 3];
|
||||||
|
|
||||||
|
for (slot_id, card) in (0_u8..).zip(solboard.goal.iter()) {
|
||||||
|
match card {
|
||||||
|
Option::Some(NumberCard { value, suit }) => {
|
||||||
|
goal_desired_pos[usize::from(suit_to_id(suit))] = (*value + 1, slot_id);
|
||||||
|
}
|
||||||
|
Option::None => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let mut result = Vec::<crate::All>::new();
|
||||||
|
for (position, card) in solboard.movable_cards() {
|
||||||
|
if let CardType::Number(card) = card {
|
||||||
|
if goal_desired_pos[usize::from(suit_to_id(&card.suit))].0 == card.value {
|
||||||
|
result.push(crate::All::Goal(crate::Goal {
|
||||||
|
card: card.clone(),
|
||||||
|
source: position,
|
||||||
|
goal_slot_index: goal_desired_pos[usize::from(suit_to_id(&card.suit))].1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn huakill_actions(solboard: &Board) -> Vec<crate::All> {
|
||||||
|
for (slot_id, field_column) in (0_u8..).zip(solboard.field.iter()) {
|
||||||
|
if let Option::Some(CardType::Hua) = field_column.last() {
|
||||||
|
return vec![crate::All::HuaKill(crate::HuaKill {
|
||||||
|
field_position: FieldPosition::new(slot_id, (field_column.len() - 1) as u8),
|
||||||
|
})];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn all_actions(solboard: &Board) -> Vec<crate::All> {
|
||||||
|
return [
|
||||||
|
&huakill_actions(solboard)[..],
|
||||||
|
&dragonkill_actions(solboard)[..],
|
||||||
|
&goal_move_actions(solboard)[..],
|
||||||
|
&debunkerize_actions(solboard)[..],
|
||||||
|
&field_move_actions(solboard)[..],
|
||||||
|
&bunkerize_actions(solboard)[..],
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn filter_actions(solboard: &Board) -> Vec<crate::All> {
|
||||||
|
let action_list = all_actions(solboard);
|
||||||
|
let huakill_action = action_list.iter().find(|x| {
|
||||||
|
if let crate::All::HuaKill(_) = x {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Option::Some(action) = huakill_action {
|
||||||
|
return vec![action.clone()];
|
||||||
|
}
|
||||||
|
let mut goal_actions = action_list.iter().filter_map(|x| {
|
||||||
|
if let crate::All::Goal(x) = x {
|
||||||
|
return Option::Some(x);
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let minimum_goal = solboard
|
||||||
|
.goal
|
||||||
|
.iter()
|
||||||
|
.map(|x| match x {
|
||||||
|
Option::None => return 0,
|
||||||
|
Option::Some(card) => return card.value,
|
||||||
|
})
|
||||||
|
.min()
|
||||||
|
.unwrap();
|
||||||
|
if let Option::Some(minimum_goal_action) = goal_actions
|
||||||
|
.by_ref()
|
||||||
|
.min_by(|x, y| return x.card.value.cmp(&y.card.value))
|
||||||
|
{
|
||||||
|
if minimum_goal_action.card.value <= minimum_goal + 1 {
|
||||||
|
return vec![crate::All::Goal(minimum_goal_action.clone())];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return action_list.to_vec();
|
||||||
|
}
|
||||||
39
solver-rs/lib/actions/src/tests.rs
Normal file
39
solver-rs/lib/actions/src/tests.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use crate::possibilities::{all_actions, bunkerize_actions, dragonkill_actions};
|
||||||
|
use board::{BunkerSlot, CardTypeNoHua, SpecialCardType};
|
||||||
|
#[test]
|
||||||
|
pub fn dragonkill_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut x = board::load_test_board!("specific/dragonkill.json")?;
|
||||||
|
assert_eq!(dragonkill_actions(&x).len(), 1);
|
||||||
|
x.field[3].pop();
|
||||||
|
x.bunker[2] = BunkerSlot::Stash(CardTypeNoHua::Special(SpecialCardType::Zhong));
|
||||||
|
assert_eq!(dragonkill_actions(&x).len(), 1);
|
||||||
|
return Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn bunkerize_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let x = board::load_test_board!("specific/dragonkill.json")?;
|
||||||
|
assert_eq!(bunkerize_actions(&x).len(), 5);
|
||||||
|
return Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn all_actions_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let x = board::load_test_board!("specific/dragonkill.json")?;
|
||||||
|
let possible_actions = all_actions(&x);
|
||||||
|
assert_eq!(possible_actions.len(), 12);
|
||||||
|
assert_eq!(
|
||||||
|
possible_actions.iter().fold([0, 0, 0, 0, 0], |mut sum, x| {
|
||||||
|
match x {
|
||||||
|
crate::All::Bunkerize(_) => sum[0] += 1,
|
||||||
|
crate::All::Move(_) => sum[1] += 1,
|
||||||
|
crate::All::Goal(_) => sum[2] += 1,
|
||||||
|
crate::All::DragonKill(_) => sum[3] += 1,
|
||||||
|
_ => sum[4] += 1,
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}),
|
||||||
|
[5, 5, 1, 1, 0]
|
||||||
|
);
|
||||||
|
return Result::Ok(());
|
||||||
|
}
|
||||||
12
solver-rs/lib/board/Cargo.toml
Normal file
12
solver-rs/lib/board/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "board"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Lukas Wölfer <lukas.woelfer@rwth-aachen.de>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = {version="1.0.105",features=["derive"]}
|
||||||
|
serde_json = "1.0"
|
||||||
|
enum-iterator = "0.6.0"
|
||||||
4
solver-rs/lib/board/fuzz/.gitignore
vendored
Normal file
4
solver-rs/lib/board/fuzz/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
target
|
||||||
|
corpus
|
||||||
|
artifacts
|
||||||
216
solver-rs/lib/board/fuzz/Cargo.lock
generated
Normal file
216
solver-rs/lib/board/fuzz/Cargo.lock
generated
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75153c95fdedd7db9732dfbfc3702324a1627eec91ba56e37cd0ac78314ab2ed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "board"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"enum-iterator",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "board-fuzz"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"board",
|
||||||
|
"enum-iterator",
|
||||||
|
"libfuzzer-sys",
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.0.50"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum-iterator"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c79a6321a1197d7730510c7e3f6cb80432dfefecb32426de8cea0aa19b4bb8d7"
|
||||||
|
dependencies = [
|
||||||
|
"enum-iterator-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enum-iterator-derive"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e94aa31f7c0dc764f57896dc615ddd76fc13b0d5dca7eb6cc5e018a5a09ec06"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.68"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libfuzzer-sys"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d718794b8e23533b9069bd2c4597d69e41cc7ab1c02700a502971aca0cdcf24"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
"rand_hc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_hc"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.50"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78a7a12c167809363ec3bd7329fc0a3369056996de43c4b37ef3cd54a6ce4867"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.9.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||||
26
solver-rs/lib/board/fuzz/Cargo.toml
Normal file
26
solver-rs/lib/board/fuzz/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
[package]
|
||||||
|
name = "board-fuzz"
|
||||||
|
version = "0.0.0"
|
||||||
|
authors = ["Automatically generated"]
|
||||||
|
publish = false
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
cargo-fuzz = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libfuzzer-sys = "0.3"
|
||||||
|
rand = "*"
|
||||||
|
enum-iterator = "*"
|
||||||
|
|
||||||
|
[dependencies.board]
|
||||||
|
path = ".."
|
||||||
|
|
||||||
|
# Prevent this from interfering with workspaces
|
||||||
|
[workspace]
|
||||||
|
members = ["."]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fuzz_target_1"
|
||||||
|
path = "fuzz_targets/fuzz_target_1.rs"
|
||||||
127
solver-rs/lib/board/fuzz/fuzz_targets/fuzz_target_1.rs
Normal file
127
solver-rs/lib/board/fuzz/fuzz_targets/fuzz_target_1.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#![no_main]
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
|
||||||
|
use board::{Board, CardType};
|
||||||
|
use enum_iterator::IntoEnumIterator;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
|
||||||
|
struct RandomBytes<'a> {
|
||||||
|
data: &'a [u8],
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> rand::RngCore for RandomBytes<'a> {
|
||||||
|
fn next_u32(&mut self) -> u32 {
|
||||||
|
if let Option::Some(x) = self.data.get(self.index..self.index + 4) {
|
||||||
|
self.index += 4;
|
||||||
|
return (u32::from(x[3]) << 24)
|
||||||
|
| (u32::from(x[2]) << 16)
|
||||||
|
| (u32::from(x[1]) << 8)
|
||||||
|
| u32::from(x[0]);
|
||||||
|
} else {
|
||||||
|
self.index = self.data.len();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn next_u64(&mut self) -> u64 {
|
||||||
|
return u64::from(self.next_u32()) << 32 | u64::from(self.next_u32());
|
||||||
|
}
|
||||||
|
fn fill_bytes(&mut self, dest: &mut [u8]) {
|
||||||
|
if (self.index >= self.data.len()) || (dest.len() > self.data.len() - self.index) {
|
||||||
|
for cell in dest.iter_mut() {
|
||||||
|
*cell = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dest.len() < self.data.len() - self.index {
|
||||||
|
dest.clone_from_slice(&self.data[self.index..self.index + dest.len()]);
|
||||||
|
self.index += dest.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> {
|
||||||
|
self.fill_bytes(dest);
|
||||||
|
return Result::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn correct_board_permutation(data: &[u8]) -> Board {
|
||||||
|
if let Option::Some(remove_info) = data.get(0..2) {
|
||||||
|
let remove_info: u16 = u16::from(remove_info[1]) << 8 | u16::from(remove_info[0]);
|
||||||
|
let mut result = Board::default();
|
||||||
|
let mut whole_vec = Vec::<CardType>::new();
|
||||||
|
if remove_info & 1 == 1 {
|
||||||
|
result.hua_set = true;
|
||||||
|
} else {
|
||||||
|
whole_vec.push(CardType::Hua);
|
||||||
|
result.hua_set = false;
|
||||||
|
}
|
||||||
|
for (index, card) in (1_u8..).zip(board::SpecialCardType::into_enum_iter()) {
|
||||||
|
if remove_info & (1 << index) == 0 {
|
||||||
|
result.bunker[usize::from(index - 1)] =
|
||||||
|
board::BunkerSlot::Blocked(Option::Some(card.clone()));
|
||||||
|
} else {
|
||||||
|
whole_vec.push(CardType::Special(card.clone()));
|
||||||
|
whole_vec.push(CardType::Special(card.clone()));
|
||||||
|
whole_vec.push(CardType::Special(card.clone()));
|
||||||
|
whole_vec.push(CardType::Special(card.clone()));
|
||||||
|
result.bunker[usize::from(index - 1)] = board::BunkerSlot::Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (index, suit) in (4_u8..)
|
||||||
|
.step_by(4)
|
||||||
|
.zip(board::NumberCardColor::into_enum_iter())
|
||||||
|
{
|
||||||
|
let value = (((remove_info >> index) & 0b1111) % 10) as u8;
|
||||||
|
let slot_index = usize::from((index - 4) / 4);
|
||||||
|
if value == 0 {
|
||||||
|
result.goal[slot_index] = Option::None;
|
||||||
|
} else {
|
||||||
|
result.goal[slot_index] = Option::Some(board::NumberCard {
|
||||||
|
value,
|
||||||
|
suit: suit.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for value in (value + 1)..10 {
|
||||||
|
whole_vec.push(board::CardType::Number(board::NumberCard {
|
||||||
|
value,
|
||||||
|
suit: suit.clone(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
whole_vec.shuffle(&mut RandomBytes { data, index: 2 });
|
||||||
|
for ((index_start, index_end), slot) in (0..)
|
||||||
|
.step_by(8)
|
||||||
|
.zip((8..).step_by(8))
|
||||||
|
.zip(result.field.iter_mut())
|
||||||
|
{
|
||||||
|
if let Option::Some(tasty_slice) = whole_vec.get(index_start..index_end) {
|
||||||
|
slot.extend_from_slice(tasty_slice);
|
||||||
|
} else if let Option::Some(tasty_slice) = whole_vec.get(index_start..) {
|
||||||
|
slot.extend_from_slice(tasty_slice);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return Board::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
if data.len() == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let x = correct_board_permutation(&data[1..]);
|
||||||
|
assert_eq!(x.check(), Result::Ok(()));
|
||||||
|
if let Option::Some(action) = board::possibilities::all_actions(&x).choose(&mut RandomBytes {
|
||||||
|
data: &data[0..1],
|
||||||
|
index: 0,
|
||||||
|
}) {
|
||||||
|
let mut action_board = x.clone();
|
||||||
|
action.apply(&mut action_board);
|
||||||
|
assert_ne!(action_board, x);
|
||||||
|
action.undo(&mut action_board);
|
||||||
|
assert_eq!(action_board, x);
|
||||||
|
}
|
||||||
|
});
|
||||||
403
solver-rs/lib/board/src/board_class.rs
Normal file
403
solver-rs/lib/board/src/board_class.rs
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
use crate::{
|
||||||
|
BunkerSlot, CardType, CardTypeNoHua, FieldPosition, NumberCard, NumberCardColor,
|
||||||
|
PositionNoGoal, SpecialCardType,
|
||||||
|
};
|
||||||
|
use enum_iterator::IntoEnumIterator;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub enum Error {
|
||||||
|
CardMissing(CardType),
|
||||||
|
CardDouble(CardType),
|
||||||
|
GoalTooHigh(NumberCard),
|
||||||
|
ErraneousCard(NumberCard),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Board {
|
||||||
|
pub field: [Vec<CardType>; 8],
|
||||||
|
pub goal: [Option<NumberCard>; 3],
|
||||||
|
pub hua_set: bool,
|
||||||
|
pub bunker: [BunkerSlot; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Board {
|
||||||
|
fn default() -> Self {
|
||||||
|
return Self {
|
||||||
|
field: [
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
],
|
||||||
|
goal: [
|
||||||
|
Option::Some(NumberCard {
|
||||||
|
value: 9,
|
||||||
|
suit: NumberCardColor::Black,
|
||||||
|
}),
|
||||||
|
Option::Some(NumberCard {
|
||||||
|
value: 9,
|
||||||
|
suit: NumberCardColor::Red,
|
||||||
|
}),
|
||||||
|
Option::Some(NumberCard {
|
||||||
|
value: 9,
|
||||||
|
suit: NumberCardColor::Green,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
hua_set: false,
|
||||||
|
bunker: [
|
||||||
|
BunkerSlot::Blocked(Option::Some(SpecialCardType::Bai)),
|
||||||
|
BunkerSlot::Blocked(Option::Some(SpecialCardType::Zhong)),
|
||||||
|
BunkerSlot::Blocked(Option::Some(SpecialCardType::Fa)),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct BoardEqHash([u8; 32]);
|
||||||
|
impl PartialEq for BoardEqHash {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
return self
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.zip(other.0.iter())
|
||||||
|
.all(|(this_cell, other_cell)| return this_cell == other_cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Eq for BoardEqHash {}
|
||||||
|
|
||||||
|
impl std::hash::Hash for BoardEqHash {
|
||||||
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
state.write(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::str::FromStr for Board {
|
||||||
|
type Err = serde_json::error::Error;
|
||||||
|
fn from_str(json_string: &str) -> Result<Self, Self::Err> {
|
||||||
|
//! # Errors
|
||||||
|
//! Will return `io::Result::Err` when the path cannot be found,
|
||||||
|
//! and `Result::Err` when the json in the file is incorrect
|
||||||
|
return serde_json::from_str::<Self>(json_string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BitSquasher<'a> {
|
||||||
|
sink: &'a mut [u8],
|
||||||
|
byte: usize,
|
||||||
|
bit: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BitSquasher<'a> {
|
||||||
|
pub fn new(sink: &'a mut [u8]) -> Self {
|
||||||
|
return BitSquasher {
|
||||||
|
sink,
|
||||||
|
byte: 0,
|
||||||
|
bit: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pub fn squash(&mut self, input: u8, count: u8) {
|
||||||
|
debug_assert!(count <= 8);
|
||||||
|
debug_assert!(count > 0);
|
||||||
|
self.sink[self.byte] |= input << self.bit;
|
||||||
|
if (8 - self.bit) < count {
|
||||||
|
self.sink[self.byte + 1] |= input >> (8 - self.bit);
|
||||||
|
}
|
||||||
|
self.bit += count;
|
||||||
|
self.byte += usize::from(self.bit / 8);
|
||||||
|
self.bit %= 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bit_squasher_test() {
|
||||||
|
let mut buffer: [u8; 4] = Default::default();
|
||||||
|
|
||||||
|
let mut squasher = BitSquasher::new(&mut buffer);
|
||||||
|
squasher.squash(0b101, 3);
|
||||||
|
squasher.squash(0b1111000, 7);
|
||||||
|
squasher.squash(0b11001100, 8);
|
||||||
|
squasher.squash(0b101010, 6);
|
||||||
|
|
||||||
|
assert_eq!(buffer, [0b11000101, 0b00110011, 0b10101011, 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Board {
|
||||||
|
pub fn from_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
//! # Errors
|
||||||
|
//! Will return `io::Result::Err` when the path cannot be found,
|
||||||
|
//! and `Result::Err` when the json in the file is incorrect
|
||||||
|
let f = File::open(path)?;
|
||||||
|
let reader = BufReader::new(f);
|
||||||
|
let x: Self = serde_json::from_reader(reader)?;
|
||||||
|
return Result::Ok(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn goal_value(&self, suit: &NumberCardColor) -> u8 {
|
||||||
|
return self
|
||||||
|
.goal
|
||||||
|
.iter()
|
||||||
|
.filter_map(|card| return card.clone())
|
||||||
|
.find_map(|card| {
|
||||||
|
if &card.suit == suit {
|
||||||
|
return Option::Some(card.value);
|
||||||
|
} else {
|
||||||
|
return Option::None;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
}
|
||||||
|
#[must_use]
|
||||||
|
pub fn equivalence_hash(&self) -> BoardEqHash {
|
||||||
|
// up to 40 cards on the field
|
||||||
|
// 8 empty card represents end of slot
|
||||||
|
// 3 bunker
|
||||||
|
// If hua in field -> hua not set, does not need representation
|
||||||
|
// We can skip goal, as the value of the card in the goal
|
||||||
|
// is the highest value missing from the board;
|
||||||
|
let mut result = [0_u8; 32];
|
||||||
|
let mut squasher = BitSquasher::new(&mut result);
|
||||||
|
|
||||||
|
let mut field_lengths: [usize; 8] = Default::default();
|
||||||
|
for (index, cell) in field_lengths.iter_mut().enumerate() {
|
||||||
|
*cell = index;
|
||||||
|
}
|
||||||
|
field_lengths.sort_unstable_by(|left_index, right_index| {
|
||||||
|
return self.field[*left_index].cmp(&self.field[*right_index]);
|
||||||
|
});
|
||||||
|
let sorted_iter = field_lengths.iter().map(|index| return &self.field[*index]);
|
||||||
|
|
||||||
|
for slot in sorted_iter {
|
||||||
|
let slot_size = slot.len();
|
||||||
|
debug_assert!(slot.len() < 16);
|
||||||
|
squasher.squash(slot_size as u8, 4);
|
||||||
|
for cell in slot {
|
||||||
|
let cell_byte = cell.to_byte();
|
||||||
|
debug_assert!(cell_byte < 32);
|
||||||
|
squasher.squash(cell_byte, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut sorted_bunker = self.bunker.clone();
|
||||||
|
sorted_bunker.sort_unstable();
|
||||||
|
for slot in sorted_bunker.iter() {
|
||||||
|
let bunker_byte = match slot {
|
||||||
|
BunkerSlot::Empty => 0,
|
||||||
|
BunkerSlot::Stash(card) => card.add_hua().to_byte(),
|
||||||
|
BunkerSlot::Blocked(Option::Some(card)) => {
|
||||||
|
CardType::Special(card.clone()).to_byte() | (1 << 5)
|
||||||
|
}
|
||||||
|
BunkerSlot::Blocked(Option::None) => (1 << 5),
|
||||||
|
};
|
||||||
|
debug_assert!(bunker_byte < 64);
|
||||||
|
squasher.squash(bunker_byte, 6);
|
||||||
|
}
|
||||||
|
return BoardEqHash(result);
|
||||||
|
}
|
||||||
|
pub fn movable_cards<'a>(&'a self) -> impl Iterator<Item = (PositionNoGoal, CardType)> + 'a {
|
||||||
|
let bunker_iterator = (0_u8..)
|
||||||
|
.zip(self.bunker.iter())
|
||||||
|
.filter_map(|(index, card)| {
|
||||||
|
let pos = PositionNoGoal::Bunker { slot_index: index };
|
||||||
|
let ret_card = match card {
|
||||||
|
BunkerSlot::Stash(CardTypeNoHua::Special(card)) => {
|
||||||
|
Option::Some(CardType::Special(card.clone()))
|
||||||
|
}
|
||||||
|
BunkerSlot::Stash(CardTypeNoHua::Number(card)) => {
|
||||||
|
Option::Some(CardType::Number(card.clone()))
|
||||||
|
}
|
||||||
|
_ => Option::None,
|
||||||
|
};
|
||||||
|
return ret_card.map(|card| return (pos, card));
|
||||||
|
});
|
||||||
|
let field_iterator = (0_u8..)
|
||||||
|
.zip(self.field.iter())
|
||||||
|
.filter_map(|(column_index, row)| {
|
||||||
|
return row.last().map(|ret_card| {
|
||||||
|
let pos = PositionNoGoal::Field(FieldPosition::new(
|
||||||
|
column_index,
|
||||||
|
(row.len() - 1) as u8,
|
||||||
|
));
|
||||||
|
return (pos, ret_card.clone());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
let result = bunker_iterator.chain(field_iterator);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_number_card(
|
||||||
|
card: &NumberCard,
|
||||||
|
number_card_map: &mut HashMap<NumberCardColor, [bool; 9]>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if card.value > 9 || card.value < 1 {
|
||||||
|
return Result::Err(Error::ErraneousCard(card.clone()));
|
||||||
|
}
|
||||||
|
if *number_card_map
|
||||||
|
.get_mut(&card.suit)
|
||||||
|
.unwrap()
|
||||||
|
.get(usize::from(card.value - 1))
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
return Result::Err(Error::CardDouble(CardType::Number(card.clone())));
|
||||||
|
}
|
||||||
|
*number_card_map
|
||||||
|
.get_mut(&card.suit)
|
||||||
|
.unwrap()
|
||||||
|
.get_mut(usize::from(card.value - 1))
|
||||||
|
.unwrap() = true;
|
||||||
|
return Result::Ok(());
|
||||||
|
}
|
||||||
|
fn handle_special_card(
|
||||||
|
card: &SpecialCardType,
|
||||||
|
special_card_map: &mut HashMap<SpecialCardType, i8>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let card_slot = special_card_map.entry(card.clone()).or_insert(0);
|
||||||
|
if *card_slot > 4 {
|
||||||
|
return Result::Err(Error::CardDouble(CardType::Special(card.clone())));
|
||||||
|
}
|
||||||
|
*card_slot += 1;
|
||||||
|
return Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check(&self) -> Result<(), Error> {
|
||||||
|
//! # Errors
|
||||||
|
//!
|
||||||
|
//! Returns the error in the board
|
||||||
|
let mut special_card_map: HashMap<SpecialCardType, i8> = HashMap::new();
|
||||||
|
let mut number_card_map: HashMap<NumberCardColor, [bool; 9]> = HashMap::new();
|
||||||
|
let mut unknown_blocked_count: u8 = 0;
|
||||||
|
for color in NumberCardColor::into_enum_iter() {
|
||||||
|
number_card_map.insert(color.clone(), [false; 9]);
|
||||||
|
}
|
||||||
|
for special_card_type in SpecialCardType::into_enum_iter() {
|
||||||
|
special_card_map.insert(special_card_type.clone(), 0);
|
||||||
|
}
|
||||||
|
let mut hua_exists: bool = self.hua_set;
|
||||||
|
|
||||||
|
for field_row in &self.field {
|
||||||
|
for cell in field_row.iter() {
|
||||||
|
match cell {
|
||||||
|
CardType::Number(number_card) => {
|
||||||
|
Self::handle_number_card(number_card, &mut number_card_map)?;
|
||||||
|
}
|
||||||
|
CardType::Special(card_type) => {
|
||||||
|
Self::handle_special_card(card_type, &mut special_card_map)?;
|
||||||
|
}
|
||||||
|
CardType::Hua => {
|
||||||
|
if hua_exists {
|
||||||
|
return Result::Err(Error::CardDouble(CardType::Hua));
|
||||||
|
} else {
|
||||||
|
hua_exists = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for bunker_cell in &self.bunker {
|
||||||
|
match bunker_cell {
|
||||||
|
BunkerSlot::Blocked(Option::None) => unknown_blocked_count += 1,
|
||||||
|
BunkerSlot::Blocked(Option::Some(special_card_type)) => {
|
||||||
|
for _ in 0..4 {
|
||||||
|
Self::handle_special_card(special_card_type, &mut special_card_map)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BunkerSlot::Stash(CardTypeNoHua::Special(special_card_type)) => {
|
||||||
|
Self::handle_special_card(special_card_type, &mut special_card_map)?;
|
||||||
|
}
|
||||||
|
BunkerSlot::Stash(CardTypeNoHua::Number(number_card)) => {
|
||||||
|
Self::handle_number_card(number_card, &mut number_card_map)?;
|
||||||
|
}
|
||||||
|
BunkerSlot::Empty => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for goal_cell in &self.goal {
|
||||||
|
if let Some(NumberCard { suit, value }) = goal_cell {
|
||||||
|
let color_slice = number_card_map.get_mut(suit).unwrap();
|
||||||
|
for i in 0..*value {
|
||||||
|
if *color_slice.get(usize::from(i)).unwrap() {
|
||||||
|
return Result::Err(Error::GoalTooHigh(NumberCard {
|
||||||
|
suit: suit.clone(),
|
||||||
|
value: *value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
*color_slice.get_mut(usize::from(i)).unwrap() = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (card_type, count) in &special_card_map {
|
||||||
|
if *count != 4 {
|
||||||
|
if unknown_blocked_count == 0 {
|
||||||
|
return Result::Err(Error::CardMissing(CardType::Special(card_type.clone())));
|
||||||
|
}
|
||||||
|
unknown_blocked_count -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (card_type, value_array) in &number_card_map {
|
||||||
|
for (index, value_hit) in (0_u8..).zip(value_array.iter()) {
|
||||||
|
if !*value_hit {
|
||||||
|
return Result::Err(Error::CardMissing(CardType::Number(NumberCard {
|
||||||
|
suit: card_type.clone(),
|
||||||
|
value: (index + 1),
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn solved(&self) -> bool {
|
||||||
|
for row in &self.field {
|
||||||
|
if !row.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for slot in &self.bunker {
|
||||||
|
if let BunkerSlot::Blocked(_) = slot {
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.hua_set {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for goal_slot in &self.goal {
|
||||||
|
if goal_slot.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for color in NumberCardColor::into_enum_iter() {
|
||||||
|
let color_position = self.goal.iter().position(|goal_card| {
|
||||||
|
return goal_card
|
||||||
|
.as_ref()
|
||||||
|
.expect("We already checked that every goal slot is not None")
|
||||||
|
.suit
|
||||||
|
== color;
|
||||||
|
});
|
||||||
|
if color_position.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for card in &self.goal {
|
||||||
|
if card
|
||||||
|
.as_ref()
|
||||||
|
.expect("We already checked that every goal slot is not None")
|
||||||
|
.value
|
||||||
|
!= 9
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
118
solver-rs/lib/board/src/cards.rs
Normal file
118
solver-rs/lib/board/src/cards.rs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
use enum_iterator::IntoEnumIterator;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::Display;
|
||||||
|
#[derive(
|
||||||
|
Serialize, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, IntoEnumIterator,
|
||||||
|
)]
|
||||||
|
pub enum SpecialCardType {
|
||||||
|
Zhong,
|
||||||
|
Bai,
|
||||||
|
Fa,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SpecialCardType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
return write!(f, "{:#?}", self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, IntoEnumIterator,
|
||||||
|
)]
|
||||||
|
pub enum NumberCardColor {
|
||||||
|
Red,
|
||||||
|
Green,
|
||||||
|
Black,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for NumberCardColor {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
return write!(f, "{:#?}", self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct NumberCard {
|
||||||
|
pub value: u8,
|
||||||
|
pub suit: NumberCardColor,
|
||||||
|
}
|
||||||
|
impl Display for NumberCard {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
return write!(f, "{} {}", self.suit, self.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
|
||||||
|
pub enum CardType {
|
||||||
|
Hua,
|
||||||
|
Number(NumberCard),
|
||||||
|
Special(SpecialCardType),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CardType {
|
||||||
|
#[must_use]
|
||||||
|
pub fn remove_hua(&self) -> CardTypeNoHua {
|
||||||
|
match self {
|
||||||
|
Self::Number(x) => return CardTypeNoHua::Number(x.clone()),
|
||||||
|
Self::Special(x) => return CardTypeNoHua::Special(x.clone()),
|
||||||
|
Self::Hua => panic!("Remove hua on hua"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a number from (1..=31)
|
||||||
|
#[must_use]
|
||||||
|
pub fn to_byte(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Number(numbercard) => {
|
||||||
|
let result = numbercard.value
|
||||||
|
+ 9 * (NumberCardColor::into_enum_iter()
|
||||||
|
.position(|suit| return numbercard.suit == suit)
|
||||||
|
.unwrap() as u8);
|
||||||
|
debug_assert!(result >= 1 && result <= 27);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Self::Special(specialcard) => {
|
||||||
|
let result = 28
|
||||||
|
+ (SpecialCardType::into_enum_iter()
|
||||||
|
.position(|x| return x == *specialcard)
|
||||||
|
.unwrap() as u8);
|
||||||
|
debug_assert!(result >= 28 && result <= 30);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Self::Hua => return 31,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CardType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Hua => return write!(f, "Hua"),
|
||||||
|
Self::Number(x) => return write!(f, "{}", x),
|
||||||
|
Self::Special(x) => return write!(f, "{}", x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)]
|
||||||
|
pub enum CardTypeNoHua {
|
||||||
|
Number(NumberCard),
|
||||||
|
Special(SpecialCardType),
|
||||||
|
}
|
||||||
|
impl Display for CardTypeNoHua {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Number(x) => return write!(f, "{}", x),
|
||||||
|
Self::Special(x) => return write!(f, "{}", x),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CardTypeNoHua {
|
||||||
|
#[must_use] pub fn add_hua(&self) -> CardType {
|
||||||
|
match self {
|
||||||
|
Self::Number(x) => return CardType::Number(x.clone()),
|
||||||
|
Self::Special(x) => return CardType::Special(x.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
112
solver-rs/lib/board/src/fieldposition.rs
Normal file
112
solver-rs/lib/board/src/fieldposition.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor};
|
||||||
|
use std::fmt;
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||||
|
pub struct FieldPosition {
|
||||||
|
buffer: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FieldPosition {
|
||||||
|
#[must_use]
|
||||||
|
pub fn column(&self) -> u8 {
|
||||||
|
return self.buffer & 0b1111;
|
||||||
|
}
|
||||||
|
#[must_use]
|
||||||
|
pub fn row(&self) -> u8 {
|
||||||
|
return (self.buffer & (0b1111 << 4)) >> 4;
|
||||||
|
}
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(column: u8, row: u8) -> Self {
|
||||||
|
debug_assert!(column < 8);
|
||||||
|
// Should be 13, allowing some buffer for end-markers because we've got the space
|
||||||
|
debug_assert!(row < 16);
|
||||||
|
return Self {
|
||||||
|
buffer: (column & 0b1111) | ((row & 0b1111) << 4),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for FieldPosition {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
return write!(f, "slot #{} index #{}", self.column(), self.row());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl serde::Serialize for FieldPosition {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
use serde::ser::SerializeStruct;
|
||||||
|
let mut state = serializer.serialize_struct("FieldPosition", 2)?;
|
||||||
|
state.serialize_field("column", &self.column())?;
|
||||||
|
state.serialize_field("row", &self.row())?;
|
||||||
|
return state.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::Deserialize<'de> for FieldPosition {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(field_identifier, rename_all = "lowercase")]
|
||||||
|
enum Field {
|
||||||
|
Column,
|
||||||
|
Row,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FieldPositionVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for FieldPositionVisitor {
|
||||||
|
type Value = FieldPosition;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
return formatter.write_str("struct FieldPosition")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<V>(self, mut seq: V) -> Result<FieldPosition, V::Error>
|
||||||
|
where
|
||||||
|
V: SeqAccess<'de>,
|
||||||
|
{
|
||||||
|
let column = seq
|
||||||
|
.next_element()?
|
||||||
|
.ok_or_else(|| return de::Error::invalid_length(0, &self))?;
|
||||||
|
let row = seq
|
||||||
|
.next_element()?
|
||||||
|
.ok_or_else(|| return de::Error::invalid_length(1, &self))?;
|
||||||
|
return Ok(FieldPosition::new(column, row));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<V>(self, mut map: V) -> Result<FieldPosition, V::Error>
|
||||||
|
where
|
||||||
|
V: MapAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut column = None;
|
||||||
|
let mut row = None;
|
||||||
|
while let Some(key) = map.next_key()? {
|
||||||
|
match key {
|
||||||
|
Field::Column => {
|
||||||
|
if column.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("column"));
|
||||||
|
}
|
||||||
|
column = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::Row => {
|
||||||
|
if row.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("row"));
|
||||||
|
}
|
||||||
|
row = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let column = column.ok_or_else(|| return de::Error::missing_field("column"))?;
|
||||||
|
let row = row.ok_or_else(|| return de::Error::missing_field("row"))?;
|
||||||
|
return Ok(FieldPosition::new(column, row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIELDS: &[&str] = &["column", "row"];
|
||||||
|
return deserializer.deserialize_struct("Duration", FIELDS, FieldPositionVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
54
solver-rs/lib/board/src/lib.rs
Normal file
54
solver-rs/lib/board/src/lib.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#![warn(
|
||||||
|
clippy::all,
|
||||||
|
clippy::restriction,
|
||||||
|
clippy::pedantic,
|
||||||
|
clippy::nursery,
|
||||||
|
clippy::cargo
|
||||||
|
)]
|
||||||
|
#![allow(clippy::cargo)]
|
||||||
|
// Style choices
|
||||||
|
#![allow(
|
||||||
|
clippy::missing_docs_in_private_items,
|
||||||
|
clippy::needless_return,
|
||||||
|
clippy::get_unwrap,
|
||||||
|
clippy::indexing_slicing,
|
||||||
|
clippy::explicit_iter_loop
|
||||||
|
)]
|
||||||
|
// Way too pedantic
|
||||||
|
#![allow(clippy::integer_arithmetic)]
|
||||||
|
#![allow(clippy::integer_division)]
|
||||||
|
// Useless
|
||||||
|
#![allow(clippy::missing_inline_in_public_items, clippy::missing_const_for_fn)]
|
||||||
|
// Useful for production
|
||||||
|
#![allow(
|
||||||
|
clippy::use_debug,
|
||||||
|
clippy::print_stdout,
|
||||||
|
clippy::dbg_macro,
|
||||||
|
clippy::panic
|
||||||
|
)]
|
||||||
|
// Useful for improving code robustness
|
||||||
|
#![allow(
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_possible_wrap,
|
||||||
|
clippy::option_unwrap_used,
|
||||||
|
clippy::option_expect_used,
|
||||||
|
clippy::as_conversions,
|
||||||
|
clippy::result_unwrap_used,
|
||||||
|
clippy::result_expect_used,
|
||||||
|
// clippy::wildcard_enum_match_arm
|
||||||
|
)]
|
||||||
|
#![allow(clippy::trivially_copy_pass_by_ref)]
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
mod cards;
|
||||||
|
pub use cards::*;
|
||||||
|
mod fieldposition;
|
||||||
|
pub use fieldposition::*;
|
||||||
|
mod positions;
|
||||||
|
pub use positions::*;
|
||||||
|
mod board_class;
|
||||||
|
pub use crate::board_class::*;
|
||||||
|
pub mod test_boards;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
85
solver-rs/lib/board/src/positions.rs
Normal file
85
solver-rs/lib/board/src/positions.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
use crate::cards::{CardTypeNoHua, SpecialCardType};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, Clone)]
|
||||||
|
pub enum BunkerSlot {
|
||||||
|
Empty,
|
||||||
|
Blocked(Option<SpecialCardType>),
|
||||||
|
Stash(CardTypeNoHua),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub enum Position {
|
||||||
|
Field(crate::FieldPosition),
|
||||||
|
Bunker { slot_index: u8 },
|
||||||
|
Goal { slot_index: u8 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Position {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Field(x) => return write!(f, "Field ({})", x),
|
||||||
|
Self::Bunker { slot_index } => return write!(f, "Bunker #{}", slot_index),
|
||||||
|
Self::Goal { slot_index } => return write!(f, "Goal #{}", slot_index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PositionNoGoal> for Position {
|
||||||
|
fn from(input: PositionNoGoal) -> Self {
|
||||||
|
match input {
|
||||||
|
PositionNoGoal::Field(x) => return Self::Field(x),
|
||||||
|
PositionNoGoal::Bunker { slot_index } => return Self::Bunker { slot_index },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<PositionNoGoal> for Position {
|
||||||
|
fn eq(&self, other: &PositionNoGoal) -> bool {
|
||||||
|
return other.eq(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Hash)]
|
||||||
|
pub enum PositionNoGoal {
|
||||||
|
Field(crate::FieldPosition),
|
||||||
|
Bunker { slot_index: u8 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for PositionNoGoal {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
return write!(f, "{}", Position::from(*self));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GoalTransformError;
|
||||||
|
|
||||||
|
impl std::convert::TryFrom<Position> for PositionNoGoal {
|
||||||
|
type Error = GoalTransformError;
|
||||||
|
fn try_from(input: Position) -> Result<Self, Self::Error> {
|
||||||
|
match input {
|
||||||
|
Position::Field(field_position) => return Result::Ok(Self::Field(field_position)),
|
||||||
|
Position::Bunker { slot_index } => {
|
||||||
|
return Result::Ok(Self::Bunker { slot_index });
|
||||||
|
}
|
||||||
|
Position::Goal { .. } => {
|
||||||
|
return Result::Err(GoalTransformError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<Position> for PositionNoGoal {
|
||||||
|
fn eq(&self, other: &Position) -> bool {
|
||||||
|
let other = <Self as std::convert::TryFrom<Position>>::try_from(other.clone());
|
||||||
|
match other {
|
||||||
|
Ok(other) => {
|
||||||
|
return Self::eq(self, &other);
|
||||||
|
}
|
||||||
|
Err(GoalTransformError) => {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
solver-rs/lib/board/src/test_boards.rs
Normal file
21
solver-rs/lib/board/src/test_boards.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// This is incredibly shit, as other crates call this macro with _their_ CARGO_MANIFEST_DIR. Ideally we would move
|
||||||
|
// the boards into the board crate, and use the path of the board crate. But it seems to be really hard to get this done with
|
||||||
|
// macros, and const variables can't be used by macros, so we're using this hack for now.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! TEST_BOARD_ROOT {
|
||||||
|
() => {
|
||||||
|
concat!(env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/../../aux/boards/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! load_test_board {
|
||||||
|
( $relpath:expr ) => {
|
||||||
|
{
|
||||||
|
<$crate::Board as std::str::FromStr>::from_str(
|
||||||
|
include_str!(concat!($crate::TEST_BOARD_ROOT!(),
|
||||||
|
$relpath)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
30
solver-rs/lib/board/src/tests.rs
Normal file
30
solver-rs/lib/board/src/tests.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use crate::{CardType, Error, NumberCard, NumberCardColor};
|
||||||
|
#[test]
|
||||||
|
pub fn check_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// let mut x = Board::from_file(std::path::Path::new(crate::test_boards::SPECIFIC)?;
|
||||||
|
let mut x = crate::load_test_board!("specific/solved.json")?;
|
||||||
|
assert_eq!(x.check(), Result::Ok(()));
|
||||||
|
assert_eq!(x.solved(), true);
|
||||||
|
x.field[2].push(CardType::Hua);
|
||||||
|
assert_eq!(x.check(), Result::Err(Error::CardDouble(CardType::Hua)));
|
||||||
|
x.hua_set = false;
|
||||||
|
assert_eq!(x.check(), Result::Ok(()));
|
||||||
|
x.field[2].push(CardType::Number(NumberCard {
|
||||||
|
suit: NumberCardColor::Black,
|
||||||
|
value: 9,
|
||||||
|
}));
|
||||||
|
assert_eq!(
|
||||||
|
x.check(),
|
||||||
|
Result::Err(Error::GoalTooHigh(NumberCard {
|
||||||
|
value: 9,
|
||||||
|
suit: NumberCardColor::Black
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
x.goal[0] = Some(NumberCard {
|
||||||
|
suit: NumberCardColor::Black,
|
||||||
|
value: 8,
|
||||||
|
});
|
||||||
|
assert_eq!(x.check(), Result::Ok(()));
|
||||||
|
assert_eq!(x.solved(), false);
|
||||||
|
return Result::Ok(());
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user