Compare commits

...

12 Commits

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

View File

@@ -9,5 +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"
} }

BIN
16_10_conf.zip Normal file

Binary file not shown.

BIN
laptop_conf.zip Normal file

Binary file not shown.

View File

@@ -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"""
@@ -187,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)

View File

@@ -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)

View File

@@ -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
@@ -230,3 +228,11 @@ def parse_board(image: np.ndarray, conf: Configuration) -> Board:
result.bunker = parse_bunker(image, conf) result.bunker = parse_bunker(image, conf)
result.goal = parse_goal(image, conf) result.goal = parse_goal(image, conf)
return result return result
def parse_start_board(image: np.ndarray, conf: Configuration) -> Board:
result = Board()
result.field = parse_field(image, conf)
result.flower_gone = parse_hua(image, conf)
result.bunker = [None] * 3
result.goal = parse_goal(image, conf)
return result

View File

@@ -32,7 +32,6 @@ def get_field_squares(
def catalogue_cards(squares: List[np.ndarray]) -> List[Tuple[np.ndarray, Card]]: 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:

View File

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

View File

@@ -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 = {
time.sleep(1) "zhong": SpecialCard.Zhong,
return "fa": SpecialCard.Fa,
if isinstance(action, board_actions.GoalAction): "bai": SpecialCard.Bai,
dst = adjustment.get_square( }
conf.goal_adjustment, index_x=action.goal_id, index_y=0, card_type = dragon_name_map[info["card"].lower()]
) dragon_id = dragon_sequence.index(card_type)
if action.source_position == board.Position.Field: return ClickAction(
assert action.source_row_index is not None destination=(
src = adjustment.get_square( conf.special_button_adjustment.x
conf.field_adjustment, + conf.special_button_adjustment.w // 2,
index_x=action.source_id, conf.special_button_adjustment.y
index_y=action.source_row_index, + dragon_id * conf.special_button_adjustment.dy
+ conf.special_button_adjustment.h // 2,
) )
)
elif action_name == "goal":
current_value = goal_values[info["card"]["suit"].lower()]
proposed_value = info["card"]["value"]
assert (current_value == 0) or (current_value + 1 == proposed_value)
if proposed_value == min(goal_values.values()) + 1:
obvious = True
elif proposed_value == 2:
obvious = True
else: 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,
)
if "Field" in info["source"]:
source = _parse_field(info["source"]["Field"], conf)
else:
source = (
int(info["source"]["Bunker"]["slot_index"]) * conf.bunker_adjustment.dx
+ conf.bunker_adjustment.x
+ conf.bunker_adjustment.w // 2,
conf.bunker_adjustment.y + conf.bunker_adjustment.h // 2,
) )
dragSquare(src, dst, offset) return DragAction(source=source, destination=goal)
return elif action_name == "huakill":
raise AssertionError("You forgot an Action type") 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)

View File

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

View File

@@ -1,234 +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.field):
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.bunker):
new_possible_dragons = []
for dragon in possible_dragons:
if any(x == dragon for x in search_board.bunker):
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.bunker) if d == dragon]
field_dragons = [
i for i, f in enumerate(search_board.field) 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.bunker) 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.bunker) if x is None]
if not open_bunker_list:
return
open_bunker = open_bunker_list[0]
for index, stack in enumerate(search_board.field):
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.bunker)
if isinstance(x, board.NumberCard)
]
for index, card in bunker_number_cards:
for other_index, other_stack in enumerate(search_board.field):
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.field)
if stack
]
bunker_cards = [
(board.Position.Bunker, index, card)
for index, card in enumerate(search_board.bunker)
]
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:
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.field[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.field:
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.field):
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.field[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.field[source_index])
- len(source_substack),
destination_id=destination_index,
destination_row_index=len(destination_stack),
)
def possible_actions(search_board: board.Board) -> List[board_actions.Action]:
"""Enumerate all possible actions on the current search_board"""
result: List[board_actions.Action] = [
*list(possible_huakill_action(search_board)),
*list(possible_dragonkill_actions(search_board)),
*list(possible_goal_move_actions(search_board)),
*list(possible_debunkerize_actions(search_board)),
*list(possible_field_move_actions(search_board)),
*list(possible_bunkerize_actions(search_board)),
]
for action in result:
if action.automatic():
return [action]
return result

View File

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,17 @@
[package]
name = "shenzhen"
version = "0.1.0"
authors = ["Lukas Wölfer <lukas.woelfer@rwth-aachen.de>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
board = {path = "lib/board"}
actions = {path = "lib/actions"}
action_optimization = {path = "lib/action_optimization"}
solving = {path = "lib/solving"}
serde = {version="1.0.105",features=["derive"]}
serde_json = "1.0"
structopt = "0.3.14"

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,37 @@
#![warn(
clippy::all,
clippy::restriction,
clippy::pedantic,
clippy::nursery,
clippy::cargo
)]
#![allow(clippy::cargo)]
// Style choices
#![allow(
clippy::missing_docs_in_private_items,
clippy::needless_return,
clippy::get_unwrap,
clippy::indexing_slicing,
clippy::explicit_iter_loop
)]
// Way too pedantic
#![allow(clippy::integer_arithmetic)]
// Useless
#![allow(clippy::missing_inline_in_public_items, clippy::missing_const_for_fn)]
// Useful for production
#![allow(
clippy::use_debug,
clippy::print_stdout,
clippy::dbg_macro,
clippy::panic
)]
// Useful for improving code robustness
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::option_unwrap_used,
// clippy::result_unwrap_used,
// clippy::wildcard_enum_match_arm
)]
#![allow(clippy::trivially_copy_pass_by_ref)]
#![allow(dead_code)]

View File

@@ -0,0 +1,15 @@
[package]
name = "action_optimization"
version = "0.1.0"
authors = ["Lukas Wölfer <lukas.woelfer@rwth-aachen.de>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = {version="1.0.105",features=["derive"]}
serde_json = "1.0"
petgraph = "0.5.1"
board = {path = "../board"}
actions = {path = "../actions"}

View File

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

View File

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

View File

@@ -0,0 +1,52 @@
#![warn(
clippy::all,
clippy::restriction,
clippy::pedantic,
clippy::nursery,
clippy::cargo
)]
#![allow(clippy::cargo)]
// Style choices
#![allow(
clippy::missing_docs_in_private_items,
clippy::needless_return,
clippy::get_unwrap,
clippy::indexing_slicing,
clippy::explicit_iter_loop
)]
// Way too pedantic
#![allow(clippy::integer_arithmetic)]
// Useless
#![allow(clippy::missing_inline_in_public_items, clippy::missing_const_for_fn)]
// Useful for production
#![allow(
clippy::use_debug,
clippy::print_stdout,
clippy::dbg_macro,
clippy::panic
)]
// Useful for improving code robustness
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::option_unwrap_used,
clippy::option_expect_used,
clippy::as_conversions,
clippy::result_unwrap_used,
// clippy::wildcard_enum_match_arm
)]
#![allow(clippy::trivially_copy_pass_by_ref)]
#![allow(dead_code)]
mod drawing;
mod graph_entity;
mod optimize;
pub use optimize::optimize;
mod relation;
mod util;
// mod graph_check;
pub use drawing::*;
pub mod test_actions;
#[cfg(test)]
mod tests;

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
// This is incredibly shit, as other crates call this macro with _their_ CARGO_MANIFEST_DIR. Ideally we would move
// the boards into the board crate, and use the path of the board crate. But it seems to be really hard to get this done with
// macros, and const variables can't be used by macros, so we're using this hack for now.
#[macro_export]
macro_rules! TEST_ACTION_ROOT {
() => {
concat!(env!("CARGO_MANIFEST_DIR"),
"/../../aux/actions/")
}
}
#[macro_export]
macro_rules! load_test_action {
( $relpath:expr ) => {
{
return serde_json::from_str::<Vec<actions::All>>(include_str!(concat!($crate::TEST_ACTION_ROOT!(),
$relpath)));
}
};
}

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
[package]
name = "actions"
version = "0.1.0"
authors = ["Lukas Wölfer <lukas.woelfer@rwth-aachen.de>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = {version="1.0.105",features=["derive"]}
serde_json = "1.0"
enum-iterator = "0.6.0"
board = {path = "../board"}

View File

@@ -0,0 +1,405 @@
use board::{
Board, BunkerSlot, CardType, CardTypeNoHua, FieldPosition, NumberCard, PositionNoGoal,
SpecialCardType,
};
use serde::{Deserialize, Serialize};
pub(super) trait BoardApplication {
fn apply(&self, solboard: &mut Board);
fn undo(&self, solboard: &mut Board);
fn can_apply(&self, solboard: &Board) -> bool;
fn can_undo(&self, solboard: &Board) -> bool;
fn checked_apply(&self, solboard: &mut Board) -> bool {
if self.can_apply(solboard) {
self.apply(solboard);
return true;
}
return false;
}
fn checked_undo(&self, solboard: &mut Board) -> bool {
if self.can_undo(solboard) {
self.undo(solboard);
return true;
}
return false;
}
}
fn can_pop_top(solboard: &Board, position: &PositionNoGoal, card: &CardType) -> bool {
match position {
PositionNoGoal::Field(fieldpos) => {
if solboard.field[usize::from(fieldpos.column())]
.last()
.expect("Trying to pop top of empty field stack")
!= card
{
return false;
}
}
PositionNoGoal::Bunker { slot_index } => {
if solboard.bunker[usize::from(*slot_index)] != BunkerSlot::Stash(card.remove_hua()) {
return false;
}
}
};
return true;
}
fn pop_top(solboard: &mut Board, position: &PositionNoGoal, card: &CardType) {
debug_assert!(can_pop_top(solboard, position, card));
match position {
PositionNoGoal::Field(fieldpos) => {
solboard
.field
.get_mut(usize::from(fieldpos.column()))
.expect("Column index fucked")
.pop();
}
PositionNoGoal::Bunker { slot_index } => {
solboard.bunker[usize::from(*slot_index)] = BunkerSlot::Empty;
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct Goal {
pub card: NumberCard,
pub source: PositionNoGoal,
pub goal_slot_index: u8,
}
impl BoardApplication for Goal {
fn can_apply(&self, solboard: &Board) -> bool {
match &solboard.goal[usize::from(self.goal_slot_index)] {
Option::Some(NumberCard { value, suit }) => {
if self.card.value != *value + 1 {
return false;
}
if self.card.suit != *suit {
return false;
}
}
Option::None => {
if self.card.value != 1 {
return false;
}
}
}
if !can_pop_top(solboard, &self.source, &CardType::Number(self.card.clone())) {
return false;
}
return true;
}
fn can_undo(&self, _solboard: &Board) -> bool {
return true;
}
fn apply(&self, solboard: &mut Board) {
pop_top(solboard, &self.source, &CardType::Number(self.card.clone()));
*solboard
.goal
.get_mut(usize::from(self.goal_slot_index))
.expect("Slot index fucked") = Option::Some(self.card.clone());
}
fn undo(&self, solboard: &mut Board) {
match &self.source {
PositionNoGoal::Field(position) => {
solboard
.field
.get_mut(usize::from(position.column()))
.expect("Column index fucked")
.push(CardType::Number(self.card.clone()));
}
PositionNoGoal::Bunker { slot_index } => {
solboard.bunker[usize::from(*slot_index)] =
BunkerSlot::Stash(CardTypeNoHua::Number(self.card.clone()));
}
}
if self.card.value == 1 {
solboard.goal[usize::from(self.goal_slot_index)] = Option::None;
} else {
solboard.goal[usize::from(self.goal_slot_index)] = Option::Some(NumberCard {
suit: self.card.suit.clone(),
value: self.card.value - 1,
});
}
}
}
impl std::fmt::Display for Goal {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(
f,
"Goal {} from {} to slot #{}",
self.card, self.source, self.goal_slot_index
);
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct DragonKill {
pub card: SpecialCardType,
pub source: [PositionNoGoal; 4],
pub destination_slot_index: u8,
}
impl BoardApplication for DragonKill {
fn apply(&self, solboard: &mut Board) {
for position in &self.source {
pop_top(solboard, position, &CardType::Special(self.card.clone()));
}
solboard.bunker[usize::from(self.destination_slot_index)] =
BunkerSlot::Blocked(Option::Some(self.card.clone()));
}
fn undo(&self, solboard: &mut Board) {
solboard.bunker[usize::from(self.destination_slot_index)] = BunkerSlot::Empty;
for position in &self.source {
match &position {
PositionNoGoal::Field(field_position) => {
solboard.field[usize::from(field_position.column())]
.push(CardType::Special(self.card.clone()));
}
PositionNoGoal::Bunker { slot_index } => {
solboard.bunker[usize::from(*slot_index)] =
BunkerSlot::Stash(CardTypeNoHua::Special(self.card.clone()));
}
}
}
}
fn can_apply(&self, solboard: &Board) -> bool {
if self.destination_slot_index >= 3 {
return false;
}
let previous_slot_empty = solboard
.bunker
.iter()
.take(self.destination_slot_index.saturating_sub(1).into())
.all(|x| {
if let BunkerSlot::Empty = x {
return true;
} else {
return false;
}
});
if previous_slot_empty {
return false;
}
return true;
}
fn can_undo(&self, _solboard: &Board) -> bool {
return true;
}
}
impl std::fmt::Display for DragonKill {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(
f,
"Kill {} to bunker #{} from {}, {}, {}, {}",
self.card,
self.destination_slot_index,
self.source[0],
self.source[1],
self.source[2],
self.source[3],
);
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct Bunkerize {
pub card: CardTypeNoHua,
pub bunker_slot_index: u8,
pub field_position: FieldPosition,
pub to_bunker: bool,
}
impl Bunkerize {
fn can_move_to_bunker(&self, solboard: &Board) -> bool {
if self.field_position.row() + 1
!= solboard.field[usize::from(self.field_position.column())].len() as u8
{
return false;
}
if self.card.add_hua()
!= *solboard.field[usize::from(self.field_position.column())]
.last()
.unwrap()
{
return false;
}
if solboard.bunker[usize::from(self.bunker_slot_index)] != BunkerSlot::Empty {
return false;
}
return true;
}
fn move_to_bunker(&self, solboard: &mut Board) {
debug_assert!(self.can_move_to_bunker(solboard));
solboard.field[usize::from(self.field_position.column())].pop();
solboard.bunker[usize::from(self.bunker_slot_index)] = BunkerSlot::Stash(self.card.clone());
}
fn can_move_from_bunker(&self, solboard: &Board) -> bool {
if solboard.bunker[usize::from(self.bunker_slot_index)]
!= BunkerSlot::Stash(self.card.clone())
{
return false;
}
if self.field_position.row()
!= solboard.field[usize::from(self.field_position.column())].len() as u8
{
return false;
}
return true;
}
fn move_from_bunker(&self, solboard: &mut Board) {
debug_assert!(self.can_move_from_bunker(solboard));
solboard.field[usize::from(self.field_position.column())].push(self.card.add_hua());
solboard.bunker[usize::from(self.bunker_slot_index)] = BunkerSlot::Empty;
}
}
impl BoardApplication for Bunkerize {
fn apply(&self, solboard: &mut Board) {
if self.to_bunker {
self.move_to_bunker(solboard);
} else {
self.move_from_bunker(solboard);
}
}
fn undo(&self, solboard: &mut Board) {
if self.to_bunker {
self.move_from_bunker(solboard);
} else {
self.move_to_bunker(solboard);
}
}
fn can_apply(&self, solboard: &Board) -> bool {
if self.to_bunker {
return self.can_move_to_bunker(solboard);
} else {
return self.can_move_from_bunker(solboard);
}
}
fn can_undo(&self, solboard: &Board) -> bool {
if self.to_bunker {
return self.can_move_from_bunker(solboard);
} else {
return self.can_move_to_bunker(solboard);
}
}
}
impl std::fmt::Display for Bunkerize {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if self.to_bunker {
return write!(
f,
"Move {} from {} to bunker #{}",
self.card, self.field_position, self.bunker_slot_index,
);
} else {
return write!(
f,
"Move {} from bunker #{} to {}",
self.card, self.bunker_slot_index, self.field_position,
);
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct HuaKill {
pub field_position: FieldPosition,
}
impl BoardApplication for HuaKill {
fn can_apply(&self, solboard: &Board) -> bool {
if solboard.field[usize::from(self.field_position.column())].last()
!= Option::Some(&CardType::Hua)
{
return false;
}
if solboard.field[usize::from(self.field_position.column())].len()
!= (self.field_position.row() + 1) as usize
{
return false;
}
return true;
}
fn apply(&self, solboard: &mut Board) {
debug_assert!(self.can_apply(solboard));
solboard.field[usize::from(self.field_position.column())].pop();
solboard.hua_set = true;
}
fn can_undo(&self, solboard: &Board) -> bool {
if solboard.field[usize::from(self.field_position.column())].len()
!= self.field_position.row() as usize
{
return false;
}
return true;
}
fn undo(&self, solboard: &mut Board) {
debug_assert!(self.can_undo(solboard));
solboard.field[usize::from(self.field_position.column())].push(CardType::Hua);
solboard.hua_set = false;
}
}
impl std::fmt::Display for HuaKill {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(f, "Kill hua from {}", self.field_position);
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum All {
Bunkerize(Bunkerize),
DragonKill(DragonKill),
Goal(Goal),
HuaKill(HuaKill),
Move(super::Move),
}
impl std::fmt::Display for All {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Bunkerize(x) => return write!(f, "{}", x),
Self::DragonKill(x) => return write!(f, "{}", x),
Self::Goal(x) => return write!(f, "{}", x),
Self::HuaKill(x) => return write!(f, "{}", x),
Self::Move(x) => return write!(f, "{}", x),
}
}
}
impl All {
pub fn apply(&self, solboard: &mut Board) {
match self {
Self::HuaKill(obj) => {
obj.apply(solboard);
}
Self::DragonKill(obj) => {
obj.apply(solboard);
}
Self::Goal(obj) => {
obj.apply(solboard);
}
Self::Bunkerize(obj) => {
obj.apply(solboard);
}
Self::Move(obj) => obj.apply(solboard),
}
}
pub fn undo(&self, solboard: &mut Board) {
match self {
Self::HuaKill(obj) => {
obj.undo(solboard);
}
Self::DragonKill(obj) => {
obj.undo(solboard);
}
Self::Goal(obj) => {
obj.undo(solboard);
}
Self::Bunkerize(obj) => {
obj.undo(solboard);
}
Self::Move(obj) => obj.undo(solboard),
}
}
}

View File

@@ -0,0 +1,49 @@
#![warn(
clippy::all,
clippy::restriction,
clippy::pedantic,
clippy::nursery,
clippy::cargo
)]
#![allow(clippy::cargo)]
// Style choices
#![allow(
clippy::missing_docs_in_private_items,
clippy::needless_return,
clippy::get_unwrap,
clippy::indexing_slicing,
clippy::explicit_iter_loop
)]
// Way too pedantic
#![allow(clippy::integer_arithmetic)]
// Useless
#![allow(clippy::missing_inline_in_public_items, clippy::missing_const_for_fn)]
// Useful for production
#![allow(
clippy::use_debug,
clippy::print_stdout,
clippy::dbg_macro,
clippy::panic
)]
// Useful for improving code robustness
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::option_unwrap_used,
clippy::option_expect_used,
clippy::as_conversions,
// clippy::result_unwrap_used,
// clippy::wildcard_enum_match_arm
)]
#![allow(clippy::trivially_copy_pass_by_ref)]
#![allow(dead_code)]
mod base;
pub use base::*;
mod move_action;
pub use move_action::*;
#[cfg(test)]
mod tests;
pub mod possibilities;

View File

@@ -0,0 +1,178 @@
use board::{Board, CardType, FieldPosition, NumberCard, NumberCardColor};
use serde::{Deserialize, Serialize};
const COLOR_SEQUENCE: [NumberCardColor; 3] = [
NumberCardColor::Red,
NumberCardColor::Green,
NumberCardColor::Black,
];
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct Move {
start_card: CardType,
stack_len: u8,
pattern: u8,
pub source: FieldPosition,
pub destination: FieldPosition,
}
impl Move {
#[must_use]
fn alternate_card(bottom_suit: &NumberCardColor, bit: u8) -> NumberCardColor {
let pos = COLOR_SEQUENCE
.iter()
.position(|x| return x == bottom_suit)
.unwrap();
let shift_value = if bit == 0 { 0 } else { 1 };
return COLOR_SEQUENCE[(pos + shift_value + 1) % 3].clone();
}
#[must_use]
fn bit_card(last_card: &board::NumberCardColor, current_card: &board::NumberCardColor) -> u8 {
let last_pos = COLOR_SEQUENCE
.iter()
.position(|x| return x == last_card)
.unwrap();
let current_pos = COLOR_SEQUENCE
.iter()
.position(|x| return x == current_card)
.unwrap();
if (last_pos + 1) % 3 == current_pos {
return 0;
} else {
return 1;
}
}
#[must_use]
pub fn cards(&self) -> Vec<CardType> {
if let CardType::Number(NumberCard { value, .. }) = self.start_card {
let mut result = Vec::with_capacity(usize::from(self.stack_len));
result.push(self.start_card.clone());
for index in 1..self.stack_len {
let new_color = if let board::CardType::Number(board::NumberCard {
suit: last_suit,
..
}) = result.last().unwrap()
{
Self::alternate_card(last_suit, self.pattern & (1 << (index - 1)))
} else {
panic!("");
};
result.push(board::CardType::Number(board::NumberCard {
suit: new_color,
value: value - index,
}));
}
return result;
} else {
return vec![self.start_card.clone()];
}
}
#[must_use]
pub fn stack_len(&self) -> u8 {
return self.stack_len;
}
#[must_use]
pub fn new<'a>(
source: FieldPosition,
destination: FieldPosition,
cards: &'a [board::CardType],
) -> Self {
let mut pattern: u8 = 0;
let numbercard_filter = |card: &'a CardType| -> Option<&'a NumberCard> {
if let board::CardType::Number(numbercard) = card {
return Option::Some(numbercard);
} else {
return Option::None;
}
};
for (index, (last_card, card)) in (0_u8..).zip(
cards
.iter()
.filter_map(numbercard_filter)
.zip(cards.iter().skip(1).filter_map(numbercard_filter)),
) {
pattern |= Self::bit_card(&last_card.suit, &card.suit) << index;
debug_assert_eq!(card.value + 1, last_card.value);
}
return Self {
source,
destination,
start_card: cards[0].clone(),
stack_len: cards.len() as u8,
pattern,
};
}
}
impl super::BoardApplication for Move {
fn apply(&self, solboard: &mut Board) {
solboard.field[usize::from(self.source.column())].truncate(
solboard.field[usize::from(self.source.column())].len() - usize::from(self.stack_len()),
);
solboard.field[usize::from(self.destination.column())].append(&mut self.cards());
}
fn undo(&self, solboard: &mut Board) {
solboard.field[usize::from(self.destination.column())].truncate(
solboard.field[usize::from(self.destination.column())].len()
- usize::from(self.stack_len()),
);
solboard.field[usize::from(self.source.column())].append(&mut self.cards());
}
#[must_use]
fn can_apply(&self, _solboard: &Board) -> bool {
return true;
}
#[must_use]
fn can_undo(&self, _solboard: &Board) -> bool {
return true;
}
}
impl std::fmt::Display for Move {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let card_name = if self.stack_len() == 1 {
format!("{}", self.cards()[0])
} else {
format!("{} cards", self.stack_len())
};
return write!(
f,
"Move {} from {} to {}",
card_name, self.source, self.destination
);
}
}
#[test]
fn move_storage() {
let card_stack = vec![
board::CardType::Number(NumberCard {
value: 5,
suit: board::NumberCardColor::Red,
}),
board::CardType::Number(NumberCard {
value: 4,
suit: board::NumberCardColor::Black,
}),
board::CardType::Number(NumberCard {
value: 3,
suit: board::NumberCardColor::Green,
}),
];
let source = FieldPosition::new(0, 0);
let destination = FieldPosition::new(0, 1);
let my_move = Move::new(source.clone(), destination.clone(), &card_stack);
assert_eq!(my_move.cards(), card_stack);
let my_move = Move::new(source, destination, &card_stack[0..1]);
assert_eq!(
my_move.cards().iter().collect::<Vec<_>>(),
card_stack.iter().take(1).collect::<Vec<_>>()
)
}

View File

@@ -0,0 +1,373 @@
use board::{
Board, BunkerSlot, CardType, CardTypeNoHua, FieldPosition, NumberCard, NumberCardColor,
PositionNoGoal, SpecialCardType,
};
#[must_use]
pub fn bunkerize_actions(solboard: &Board) -> Vec<crate::All> {
let first_empty_bunker_index = solboard.bunker.iter().position(|x| match x {
BunkerSlot::Empty => return true,
_ => return false,
});
if let Option::Some(first_empty_bunker_index) = first_empty_bunker_index {
return solboard
.field
.iter()
.enumerate()
.filter_map(|(index, row)| {
return row
.last()
.filter(|card| {
if let CardType::Hua = card {
return false;
} else {
return true;
}
})
.map(|card| {
let field_position = FieldPosition::new(index as u8, (row.len() - 1) as u8);
return crate::All::Bunkerize(crate::Bunkerize {
field_position,
card: card.remove_hua(),
bunker_slot_index: first_empty_bunker_index as u8,
to_bunker: true,
});
});
})
.collect();
}
return Vec::new();
}
fn card_fits(source: &NumberCard, dest: &NumberCard) -> bool {
return (source.suit != dest.suit) && (source.value + 1 == dest.value);
}
fn fitting_field_number_position(card: &NumberCard, board: &Board) -> Option<FieldPosition> {
return board.field.iter().enumerate().find_map(|(index, row)| {
if let Option::Some(CardType::Number(top_card)) = row.last() {
if card_fits(card, top_card) {
return Option::Some(FieldPosition::new(index as u8, (row.len()) as u8));
}
}
return Option::None;
});
}
fn fitting_field_positions(card: &CardType, board: &Board) -> Vec<FieldPosition> {
let mut result = Vec::new();
if let CardType::Number(card) = card {
if let Option::Some(position) = fitting_field_number_position(card, board) {
result.push(position);
}
}
if let Option::Some(position) =
(0_u8..)
.zip(board.field.iter())
.find_map(|(column_index, slot)| {
if slot.is_empty() {
return Option::Some(FieldPosition::new(column_index, 0));
} else {
return Option::None;
}
})
{
result.push(position)
}
return result;
}
#[must_use]
pub fn debunkerize_actions(solboard: &Board) -> Vec<crate::All> {
let number_matching_cards =
(0_u8..)
.zip(solboard.bunker.iter())
.filter_map(|(bunker_slot_index, slot)| {
if let BunkerSlot::Stash(CardTypeNoHua::Number(card)) = slot {
return fitting_field_number_position(card, solboard).map(|field_position| {
return crate::All::Bunkerize(crate::Bunkerize {
card: CardTypeNoHua::Number(card.clone()),
field_position,
bunker_slot_index,
to_bunker: false,
});
});
} else {
return Option::None;
}
});
let empty_slot = solboard
.field
.iter()
.position(|row| return row.is_empty())
.map(|column_index| {
return FieldPosition::new(column_index as u8, 0);
});
if let Option::Some(field_position) = empty_slot {
let empty_slot_cards =
(0_u8..)
.zip(solboard.bunker.iter())
.filter_map(|(bunker_slot_index, slot)| {
if let BunkerSlot::Stash(card) = slot {
let result = crate::Bunkerize {
card: card.clone(),
bunker_slot_index,
field_position,
to_bunker: false,
};
return Option::Some(crate::All::Bunkerize(result));
} else {
return Option::None;
}
});
return number_matching_cards.chain(empty_slot_cards).collect();
} else {
return number_matching_cards.collect();
}
}
struct DragonTracker {
dragons: [(u8, [PositionNoGoal; 4]); 3],
}
impl DragonTracker {
fn new() -> Self {
return Self {
dragons: [(0, [PositionNoGoal::Bunker { slot_index: 0 }; 4]); 3],
};
}
fn dragon_to_id(dragon: &SpecialCardType) -> u8 {
return match dragon {
SpecialCardType::Zhong => 0,
SpecialCardType::Bai => 1,
SpecialCardType::Fa => 2,
};
}
fn id_to_dragon(id: u8) -> SpecialCardType {
return match id {
0 => SpecialCardType::Zhong,
1 => SpecialCardType::Bai,
2 => SpecialCardType::Fa,
_ => panic!("Dragon id too high"),
};
}
fn push(&mut self, dragon: &SpecialCardType, position: PositionNoGoal) {
let (ref mut count, ref mut cell) = self.dragons[usize::from(Self::dragon_to_id(dragon))];
cell[usize::from(*count)] = position;
*count += 1;
}
fn found_dragons(&self) -> impl Iterator<Item = (SpecialCardType, &[PositionNoGoal; 4])> {
return (0_u8..)
.zip(self.dragons.iter())
.filter_map(|(index, (count, positions))| {
if *count == 4 {
return Option::Some((Self::id_to_dragon(index), positions));
} else {
return Option::None;
}
});
}
}
#[must_use]
pub fn dragonkill_actions(solboard: &Board) -> Vec<crate::All> {
let mut dragon_position = DragonTracker::new();
for (position, card) in solboard.movable_cards() {
if let CardType::Special(card) = card {
dragon_position.push(&card, position);
}
}
let mut result: Vec<crate::All> = Vec::new();
for (card_type, positions) in dragon_position.found_dragons() {
let dragon_destination = solboard.bunker.iter().position(|x| {
return match x {
BunkerSlot::Empty => true,
BunkerSlot::Stash(CardTypeNoHua::Special(special_card_type)) => {
special_card_type == &card_type
}
_ => false,
};
});
if let Option::Some(dragon_destination) = dragon_destination {
let mut my_positions: [PositionNoGoal; 4] =
[PositionNoGoal::Bunker { slot_index: 0 }; 4];
my_positions.clone_from_slice(positions);
result.push(crate::All::DragonKill(crate::DragonKill {
card: card_type.clone(),
source: my_positions,
destination_slot_index: dragon_destination as u8,
}));
}
}
return result;
}
fn get_max_stack_count(board: &Board) -> [u8; 8] {
let mut result = [0; 8];
for (index, row) in result.iter_mut().zip(&board.field) {
let row_iterator = row.iter().rev();
let mut next_row_iterator = row.iter().rev();
if next_row_iterator.next().is_none() {
*index = 0;
continue;
}
*index = (row_iterator
.zip(next_row_iterator)
.take_while(|(card, bottom_card)| {
if let (CardType::Number(card), CardType::Number(bottom_card)) = (card, bottom_card)
{
return card_fits(card, bottom_card);
} else {
return false;
}
})
.count()
+ 1) as u8;
}
return result;
}
#[must_use]
pub fn field_move_actions(solboard: &Board) -> Vec<crate::All> {
let max_stack_counts: [u8; 8] = get_max_stack_count(solboard);
let required_size: u8 = max_stack_counts.iter().cloned().sum();
let mut result = Vec::with_capacity(usize::from(required_size));
for ((column_index, row), stack_size) in (0_u8..)
.zip(solboard.field.iter())
.zip(max_stack_counts.iter())
.filter(|(_, size)| return **size > 0)
{
for row_index in (row.len() - usize::from(*stack_size)) as u8..(row.len()) as u8 {
let my_stack = &row
.get(usize::from(row_index)..row.len())
.expect("Slicing failed");
for position in fitting_field_positions(
my_stack
.first()
.expect("Stack should at least have one entry"),
solboard,
) {
result.push(crate::All::Move(crate::Move::new(
FieldPosition::new(column_index, row_index),
position,
my_stack,
)));
}
}
}
return result;
}
#[must_use]
pub fn goal_move_actions(solboard: &Board) -> Vec<crate::All> {
let suit_to_id = |suit: &NumberCardColor| -> u8 {
return match suit {
NumberCardColor::Red => 0,
NumberCardColor::Green => 1,
NumberCardColor::Black => 2,
};
};
let first_empty_goal_slot_index = (0_u8..)
.zip(solboard.goal.iter())
.find_map(|(index, card)| {
if card.is_none() {
return Option::Some(index);
} else {
return Option::None;
}
})
.unwrap_or(3);
let mut goal_desired_pos = [(1_u8, first_empty_goal_slot_index); 3];
for (slot_id, card) in (0_u8..).zip(solboard.goal.iter()) {
match card {
Option::Some(NumberCard { value, suit }) => {
goal_desired_pos[usize::from(suit_to_id(suit))] = (*value + 1, slot_id);
}
Option::None => {}
};
}
let mut result = Vec::<crate::All>::new();
for (position, card) in solboard.movable_cards() {
if let CardType::Number(card) = card {
if goal_desired_pos[usize::from(suit_to_id(&card.suit))].0 == card.value {
result.push(crate::All::Goal(crate::Goal {
card: card.clone(),
source: position,
goal_slot_index: goal_desired_pos[usize::from(suit_to_id(&card.suit))].1,
}));
}
}
}
return result;
}
#[must_use]
pub fn huakill_actions(solboard: &Board) -> Vec<crate::All> {
for (slot_id, field_column) in (0_u8..).zip(solboard.field.iter()) {
if let Option::Some(CardType::Hua) = field_column.last() {
return vec![crate::All::HuaKill(crate::HuaKill {
field_position: FieldPosition::new(slot_id, (field_column.len() - 1) as u8),
})];
}
}
return Vec::new();
}
#[must_use]
pub fn all_actions(solboard: &Board) -> Vec<crate::All> {
return [
&huakill_actions(solboard)[..],
&dragonkill_actions(solboard)[..],
&goal_move_actions(solboard)[..],
&debunkerize_actions(solboard)[..],
&field_move_actions(solboard)[..],
&bunkerize_actions(solboard)[..],
]
.concat();
}
#[must_use]
pub fn filter_actions(solboard: &Board) -> Vec<crate::All> {
let action_list = all_actions(solboard);
let huakill_action = action_list.iter().find(|x| {
if let crate::All::HuaKill(_) = x {
return true;
} else {
return false;
}
});
if let Option::Some(action) = huakill_action {
return vec![action.clone()];
}
let mut goal_actions = action_list.iter().filter_map(|x| {
if let crate::All::Goal(x) = x {
return Option::Some(x);
} else {
return Option::None;
}
});
let minimum_goal = solboard
.goal
.iter()
.map(|x| match x {
Option::None => return 0,
Option::Some(card) => return card.value,
})
.min()
.unwrap();
if let Option::Some(minimum_goal_action) = goal_actions
.by_ref()
.min_by(|x, y| return x.card.value.cmp(&y.card.value))
{
if minimum_goal_action.card.value <= minimum_goal + 1 {
return vec![crate::All::Goal(minimum_goal_action.clone())];
}
}
return action_list.to_vec();
}

View File

@@ -0,0 +1,39 @@
use crate::possibilities::{all_actions, bunkerize_actions, dragonkill_actions};
use board::{BunkerSlot, CardTypeNoHua, SpecialCardType};
#[test]
pub fn dragonkill_test() -> Result<(), Box<dyn std::error::Error>> {
let mut x = board::load_test_board!("specific/dragonkill.json")?;
assert_eq!(dragonkill_actions(&x).len(), 1);
x.field[3].pop();
x.bunker[2] = BunkerSlot::Stash(CardTypeNoHua::Special(SpecialCardType::Zhong));
assert_eq!(dragonkill_actions(&x).len(), 1);
return Result::Ok(());
}
#[test]
pub fn bunkerize_test() -> Result<(), Box<dyn std::error::Error>> {
let x = board::load_test_board!("specific/dragonkill.json")?;
assert_eq!(bunkerize_actions(&x).len(), 5);
return Result::Ok(());
}
#[test]
pub fn all_actions_test() -> Result<(), Box<dyn std::error::Error>> {
let x = board::load_test_board!("specific/dragonkill.json")?;
let possible_actions = all_actions(&x);
assert_eq!(possible_actions.len(), 12);
assert_eq!(
possible_actions.iter().fold([0, 0, 0, 0, 0], |mut sum, x| {
match x {
crate::All::Bunkerize(_) => sum[0] += 1,
crate::All::Move(_) => sum[1] += 1,
crate::All::Goal(_) => sum[2] += 1,
crate::All::DragonKill(_) => sum[3] += 1,
_ => sum[4] += 1,
}
return sum;
}),
[5, 5, 1, 1, 0]
);
return Result::Ok(());
}

View File

@@ -0,0 +1,12 @@
[package]
name = "board"
version = "0.1.0"
authors = ["Lukas Wölfer <lukas.woelfer@rwth-aachen.de>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = {version="1.0.105",features=["derive"]}
serde_json = "1.0"
enum-iterator = "0.6.0"

4
solver-rs/lib/board/fuzz/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
target
corpus
artifacts

216
solver-rs/lib/board/fuzz/Cargo.lock generated Normal file
View File

@@ -0,0 +1,216 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "arbitrary"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75153c95fdedd7db9732dfbfc3702324a1627eec91ba56e37cd0ac78314ab2ed"
[[package]]
name = "board"
version = "0.1.0"
dependencies = [
"enum-iterator",
"serde",
"serde_json",
]
[[package]]
name = "board-fuzz"
version = "0.0.0"
dependencies = [
"board",
"enum-iterator",
"libfuzzer-sys",
"rand",
]
[[package]]
name = "cc"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "enum-iterator"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c79a6321a1197d7730510c7e3f6cb80432dfefecb32426de8cea0aa19b4bb8d7"
dependencies = [
"enum-iterator-derive",
]
[[package]]
name = "enum-iterator-derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e94aa31f7c0dc764f57896dc615ddd76fc13b0d5dca7eb6cc5e018a5a09ec06"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "getrandom"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "itoa"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
[[package]]
name = "libc"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0"
[[package]]
name = "libfuzzer-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d718794b8e23533b9069bd2c4597d69e41cc7ab1c02700a502971aca0cdcf24"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "ppv-lite86"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
[[package]]
name = "proc-macro2"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom",
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core",
]
[[package]]
name = "ryu"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76"
[[package]]
name = "serde"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78a7a12c167809363ec3bd7329fc0a3369056996de43c4b37ef3cd54a6ce4867"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"

View File

@@ -0,0 +1,26 @@
[package]
name = "board-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.3"
rand = "*"
enum-iterator = "*"
[dependencies.board]
path = ".."
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[[bin]]
name = "fuzz_target_1"
path = "fuzz_targets/fuzz_target_1.rs"

View File

@@ -0,0 +1,127 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use board::{Board, CardType};
use enum_iterator::IntoEnumIterator;
use rand::seq::SliceRandom;
struct RandomBytes<'a> {
data: &'a [u8],
index: usize,
}
impl<'a> rand::RngCore for RandomBytes<'a> {
fn next_u32(&mut self) -> u32 {
if let Option::Some(x) = self.data.get(self.index..self.index + 4) {
self.index += 4;
return (u32::from(x[3]) << 24)
| (u32::from(x[2]) << 16)
| (u32::from(x[1]) << 8)
| u32::from(x[0]);
} else {
self.index = self.data.len();
return 0;
}
}
fn next_u64(&mut self) -> u64 {
return u64::from(self.next_u32()) << 32 | u64::from(self.next_u32());
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
if (self.index >= self.data.len()) || (dest.len() > self.data.len() - self.index) {
for cell in dest.iter_mut() {
*cell = 0;
}
}
if dest.len() < self.data.len() - self.index {
dest.clone_from_slice(&self.data[self.index..self.index + dest.len()]);
self.index += dest.len()
}
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> {
self.fill_bytes(dest);
return Result::Ok(());
}
}
fn correct_board_permutation(data: &[u8]) -> Board {
if let Option::Some(remove_info) = data.get(0..2) {
let remove_info: u16 = u16::from(remove_info[1]) << 8 | u16::from(remove_info[0]);
let mut result = Board::default();
let mut whole_vec = Vec::<CardType>::new();
if remove_info & 1 == 1 {
result.hua_set = true;
} else {
whole_vec.push(CardType::Hua);
result.hua_set = false;
}
for (index, card) in (1_u8..).zip(board::SpecialCardType::into_enum_iter()) {
if remove_info & (1 << index) == 0 {
result.bunker[usize::from(index - 1)] =
board::BunkerSlot::Blocked(Option::Some(card.clone()));
} else {
whole_vec.push(CardType::Special(card.clone()));
whole_vec.push(CardType::Special(card.clone()));
whole_vec.push(CardType::Special(card.clone()));
whole_vec.push(CardType::Special(card.clone()));
result.bunker[usize::from(index - 1)] = board::BunkerSlot::Empty;
}
}
for (index, suit) in (4_u8..)
.step_by(4)
.zip(board::NumberCardColor::into_enum_iter())
{
let value = (((remove_info >> index) & 0b1111) % 10) as u8;
let slot_index = usize::from((index - 4) / 4);
if value == 0 {
result.goal[slot_index] = Option::None;
} else {
result.goal[slot_index] = Option::Some(board::NumberCard {
value,
suit: suit.clone(),
});
}
for value in (value + 1)..10 {
whole_vec.push(board::CardType::Number(board::NumberCard {
value,
suit: suit.clone(),
}));
}
}
whole_vec.shuffle(&mut RandomBytes { data, index: 2 });
for ((index_start, index_end), slot) in (0..)
.step_by(8)
.zip((8..).step_by(8))
.zip(result.field.iter_mut())
{
if let Option::Some(tasty_slice) = whole_vec.get(index_start..index_end) {
slot.extend_from_slice(tasty_slice);
} else if let Option::Some(tasty_slice) = whole_vec.get(index_start..) {
slot.extend_from_slice(tasty_slice);
break;
} else {
break;
}
}
return result;
} else {
return Board::default();
}
}
fuzz_target!(|data: &[u8]| {
if data.len() == 0 {
return;
}
let x = correct_board_permutation(&data[1..]);
assert_eq!(x.check(), Result::Ok(()));
if let Option::Some(action) = board::possibilities::all_actions(&x).choose(&mut RandomBytes {
data: &data[0..1],
index: 0,
}) {
let mut action_board = x.clone();
action.apply(&mut action_board);
assert_ne!(action_board, x);
action.undo(&mut action_board);
assert_eq!(action_board, x);
}
});

View File

@@ -0,0 +1,403 @@
use crate::{
BunkerSlot, CardType, CardTypeNoHua, FieldPosition, NumberCard, NumberCardColor,
PositionNoGoal, SpecialCardType,
};
use enum_iterator::IntoEnumIterator;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Error {
CardMissing(CardType),
CardDouble(CardType),
GoalTooHigh(NumberCard),
ErraneousCard(NumberCard),
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
pub struct Board {
pub field: [Vec<CardType>; 8],
pub goal: [Option<NumberCard>; 3],
pub hua_set: bool,
pub bunker: [BunkerSlot; 3],
}
impl Default for Board {
fn default() -> Self {
return Self {
field: [
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
],
goal: [
Option::Some(NumberCard {
value: 9,
suit: NumberCardColor::Black,
}),
Option::Some(NumberCard {
value: 9,
suit: NumberCardColor::Red,
}),
Option::Some(NumberCard {
value: 9,
suit: NumberCardColor::Green,
}),
],
hua_set: false,
bunker: [
BunkerSlot::Blocked(Option::Some(SpecialCardType::Bai)),
BunkerSlot::Blocked(Option::Some(SpecialCardType::Zhong)),
BunkerSlot::Blocked(Option::Some(SpecialCardType::Fa)),
],
};
}
}
pub struct BoardEqHash([u8; 32]);
impl PartialEq for BoardEqHash {
fn eq(&self, other: &Self) -> bool {
return self
.0
.iter()
.zip(other.0.iter())
.all(|(this_cell, other_cell)| return this_cell == other_cell);
}
}
impl Eq for BoardEqHash {}
impl std::hash::Hash for BoardEqHash {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write(&self.0);
}
}
impl std::str::FromStr for Board {
type Err = serde_json::error::Error;
fn from_str(json_string: &str) -> Result<Self, Self::Err> {
//! # Errors
//! Will return `io::Result::Err` when the path cannot be found,
//! and `Result::Err` when the json in the file is incorrect
return serde_json::from_str::<Self>(json_string);
}
}
struct BitSquasher<'a> {
sink: &'a mut [u8],
byte: usize,
bit: u8,
}
impl<'a> BitSquasher<'a> {
pub fn new(sink: &'a mut [u8]) -> Self {
return BitSquasher {
sink,
byte: 0,
bit: 0,
};
}
pub fn squash(&mut self, input: u8, count: u8) {
debug_assert!(count <= 8);
debug_assert!(count > 0);
self.sink[self.byte] |= input << self.bit;
if (8 - self.bit) < count {
self.sink[self.byte + 1] |= input >> (8 - self.bit);
}
self.bit += count;
self.byte += usize::from(self.bit / 8);
self.bit %= 8;
}
}
#[test]
fn bit_squasher_test() {
let mut buffer: [u8; 4] = Default::default();
let mut squasher = BitSquasher::new(&mut buffer);
squasher.squash(0b101, 3);
squasher.squash(0b1111000, 7);
squasher.squash(0b11001100, 8);
squasher.squash(0b101010, 6);
assert_eq!(buffer, [0b11000101, 0b00110011, 0b10101011, 0]);
}
impl Board {
pub fn from_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
//! # Errors
//! Will return `io::Result::Err` when the path cannot be found,
//! and `Result::Err` when the json in the file is incorrect
let f = File::open(path)?;
let reader = BufReader::new(f);
let x: Self = serde_json::from_reader(reader)?;
return Result::Ok(x);
}
#[must_use]
pub fn goal_value(&self, suit: &NumberCardColor) -> u8 {
return self
.goal
.iter()
.filter_map(|card| return card.clone())
.find_map(|card| {
if &card.suit == suit {
return Option::Some(card.value);
} else {
return Option::None;
}
})
.unwrap_or(0);
}
#[must_use]
pub fn equivalence_hash(&self) -> BoardEqHash {
// up to 40 cards on the field
// 8 empty card represents end of slot
// 3 bunker
// If hua in field -> hua not set, does not need representation
// We can skip goal, as the value of the card in the goal
// is the highest value missing from the board;
let mut result = [0_u8; 32];
let mut squasher = BitSquasher::new(&mut result);
let mut field_lengths: [usize; 8] = Default::default();
for (index, cell) in field_lengths.iter_mut().enumerate() {
*cell = index;
}
field_lengths.sort_unstable_by(|left_index, right_index| {
return self.field[*left_index].cmp(&self.field[*right_index]);
});
let sorted_iter = field_lengths.iter().map(|index| return &self.field[*index]);
for slot in sorted_iter {
let slot_size = slot.len();
debug_assert!(slot.len() < 16);
squasher.squash(slot_size as u8, 4);
for cell in slot {
let cell_byte = cell.to_byte();
debug_assert!(cell_byte < 32);
squasher.squash(cell_byte, 5);
}
}
let mut sorted_bunker = self.bunker.clone();
sorted_bunker.sort_unstable();
for slot in sorted_bunker.iter() {
let bunker_byte = match slot {
BunkerSlot::Empty => 0,
BunkerSlot::Stash(card) => card.add_hua().to_byte(),
BunkerSlot::Blocked(Option::Some(card)) => {
CardType::Special(card.clone()).to_byte() | (1 << 5)
}
BunkerSlot::Blocked(Option::None) => (1 << 5),
};
debug_assert!(bunker_byte < 64);
squasher.squash(bunker_byte, 6);
}
return BoardEqHash(result);
}
pub fn movable_cards<'a>(&'a self) -> impl Iterator<Item = (PositionNoGoal, CardType)> + 'a {
let bunker_iterator = (0_u8..)
.zip(self.bunker.iter())
.filter_map(|(index, card)| {
let pos = PositionNoGoal::Bunker { slot_index: index };
let ret_card = match card {
BunkerSlot::Stash(CardTypeNoHua::Special(card)) => {
Option::Some(CardType::Special(card.clone()))
}
BunkerSlot::Stash(CardTypeNoHua::Number(card)) => {
Option::Some(CardType::Number(card.clone()))
}
_ => Option::None,
};
return ret_card.map(|card| return (pos, card));
});
let field_iterator = (0_u8..)
.zip(self.field.iter())
.filter_map(|(column_index, row)| {
return row.last().map(|ret_card| {
let pos = PositionNoGoal::Field(FieldPosition::new(
column_index,
(row.len() - 1) as u8,
));
return (pos, ret_card.clone());
});
});
let result = bunker_iterator.chain(field_iterator);
return result;
}
fn handle_number_card(
card: &NumberCard,
number_card_map: &mut HashMap<NumberCardColor, [bool; 9]>,
) -> Result<(), Error> {
if card.value > 9 || card.value < 1 {
return Result::Err(Error::ErraneousCard(card.clone()));
}
if *number_card_map
.get_mut(&card.suit)
.unwrap()
.get(usize::from(card.value - 1))
.unwrap()
{
return Result::Err(Error::CardDouble(CardType::Number(card.clone())));
}
*number_card_map
.get_mut(&card.suit)
.unwrap()
.get_mut(usize::from(card.value - 1))
.unwrap() = true;
return Result::Ok(());
}
fn handle_special_card(
card: &SpecialCardType,
special_card_map: &mut HashMap<SpecialCardType, i8>,
) -> Result<(), Error> {
let card_slot = special_card_map.entry(card.clone()).or_insert(0);
if *card_slot > 4 {
return Result::Err(Error::CardDouble(CardType::Special(card.clone())));
}
*card_slot += 1;
return Result::Ok(());
}
pub fn check(&self) -> Result<(), Error> {
//! # Errors
//!
//! Returns the error in the board
let mut special_card_map: HashMap<SpecialCardType, i8> = HashMap::new();
let mut number_card_map: HashMap<NumberCardColor, [bool; 9]> = HashMap::new();
let mut unknown_blocked_count: u8 = 0;
for color in NumberCardColor::into_enum_iter() {
number_card_map.insert(color.clone(), [false; 9]);
}
for special_card_type in SpecialCardType::into_enum_iter() {
special_card_map.insert(special_card_type.clone(), 0);
}
let mut hua_exists: bool = self.hua_set;
for field_row in &self.field {
for cell in field_row.iter() {
match cell {
CardType::Number(number_card) => {
Self::handle_number_card(number_card, &mut number_card_map)?;
}
CardType::Special(card_type) => {
Self::handle_special_card(card_type, &mut special_card_map)?;
}
CardType::Hua => {
if hua_exists {
return Result::Err(Error::CardDouble(CardType::Hua));
} else {
hua_exists = true
}
}
}
}
}
for bunker_cell in &self.bunker {
match bunker_cell {
BunkerSlot::Blocked(Option::None) => unknown_blocked_count += 1,
BunkerSlot::Blocked(Option::Some(special_card_type)) => {
for _ in 0..4 {
Self::handle_special_card(special_card_type, &mut special_card_map)?;
}
}
BunkerSlot::Stash(CardTypeNoHua::Special(special_card_type)) => {
Self::handle_special_card(special_card_type, &mut special_card_map)?;
}
BunkerSlot::Stash(CardTypeNoHua::Number(number_card)) => {
Self::handle_number_card(number_card, &mut number_card_map)?;
}
BunkerSlot::Empty => {}
}
}
for goal_cell in &self.goal {
if let Some(NumberCard { suit, value }) = goal_cell {
let color_slice = number_card_map.get_mut(suit).unwrap();
for i in 0..*value {
if *color_slice.get(usize::from(i)).unwrap() {
return Result::Err(Error::GoalTooHigh(NumberCard {
suit: suit.clone(),
value: *value,
}));
}
*color_slice.get_mut(usize::from(i)).unwrap() = true;
}
}
}
for (card_type, count) in &special_card_map {
if *count != 4 {
if unknown_blocked_count == 0 {
return Result::Err(Error::CardMissing(CardType::Special(card_type.clone())));
}
unknown_blocked_count -= 1;
}
}
for (card_type, value_array) in &number_card_map {
for (index, value_hit) in (0_u8..).zip(value_array.iter()) {
if !*value_hit {
return Result::Err(Error::CardMissing(CardType::Number(NumberCard {
suit: card_type.clone(),
value: (index + 1),
})));
}
}
}
return Result::Ok(());
}
#[must_use]
pub fn solved(&self) -> bool {
for row in &self.field {
if !row.is_empty() {
return false;
}
}
for slot in &self.bunker {
if let BunkerSlot::Blocked(_) = slot {
} else {
return false;
}
}
if !self.hua_set {
return false;
}
for goal_slot in &self.goal {
if goal_slot.is_none() {
return false;
}
}
for color in NumberCardColor::into_enum_iter() {
let color_position = self.goal.iter().position(|goal_card| {
return goal_card
.as_ref()
.expect("We already checked that every goal slot is not None")
.suit
== color;
});
if color_position.is_none() {
return false;
}
}
for card in &self.goal {
if card
.as_ref()
.expect("We already checked that every goal slot is not None")
.value
!= 9
{
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,118 @@
use enum_iterator::IntoEnumIterator;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[derive(
Serialize, Deserialize, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, IntoEnumIterator,
)]
pub enum SpecialCardType {
Zhong,
Bai,
Fa,
}
impl Display for SpecialCardType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(f, "{:#?}", self);
}
}
#[derive(
Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, IntoEnumIterator,
)]
pub enum NumberCardColor {
Red,
Green,
Black,
}
impl Display for NumberCardColor {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(f, "{:#?}", self);
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct NumberCard {
pub value: u8,
pub suit: NumberCardColor,
}
impl Display for NumberCard {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(f, "{} {}", self.suit, self.value);
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub enum CardType {
Hua,
Number(NumberCard),
Special(SpecialCardType),
}
impl CardType {
#[must_use]
pub fn remove_hua(&self) -> CardTypeNoHua {
match self {
Self::Number(x) => return CardTypeNoHua::Number(x.clone()),
Self::Special(x) => return CardTypeNoHua::Special(x.clone()),
Self::Hua => panic!("Remove hua on hua"),
}
}
/// Returns a number from (1..=31)
#[must_use]
pub fn to_byte(&self) -> u8 {
match self {
Self::Number(numbercard) => {
let result = numbercard.value
+ 9 * (NumberCardColor::into_enum_iter()
.position(|suit| return numbercard.suit == suit)
.unwrap() as u8);
debug_assert!(result >= 1 && result <= 27);
return result;
}
Self::Special(specialcard) => {
let result = 28
+ (SpecialCardType::into_enum_iter()
.position(|x| return x == *specialcard)
.unwrap() as u8);
debug_assert!(result >= 28 && result <= 30);
return result;
}
Self::Hua => return 31,
}
}
}
impl Display for CardType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Hua => return write!(f, "Hua"),
Self::Number(x) => return write!(f, "{}", x),
Self::Special(x) => return write!(f, "{}", x),
}
}
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)]
pub enum CardTypeNoHua {
Number(NumberCard),
Special(SpecialCardType),
}
impl Display for CardTypeNoHua {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Number(x) => return write!(f, "{}", x),
Self::Special(x) => return write!(f, "{}", x),
}
}
}
impl CardTypeNoHua {
#[must_use] pub fn add_hua(&self) -> CardType {
match self {
Self::Number(x) => return CardType::Number(x.clone()),
Self::Special(x) => return CardType::Special(x.clone()),
}
}
}

View File

@@ -0,0 +1,112 @@
use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor};
use std::fmt;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct FieldPosition {
buffer: u8,
}
impl FieldPosition {
#[must_use]
pub fn column(&self) -> u8 {
return self.buffer & 0b1111;
}
#[must_use]
pub fn row(&self) -> u8 {
return (self.buffer & (0b1111 << 4)) >> 4;
}
#[must_use]
pub fn new(column: u8, row: u8) -> Self {
debug_assert!(column < 8);
// Should be 13, allowing some buffer for end-markers because we've got the space
debug_assert!(row < 16);
return Self {
buffer: (column & 0b1111) | ((row & 0b1111) << 4),
};
}
}
impl std::fmt::Display for FieldPosition {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(f, "slot #{} index #{}", self.column(), self.row());
}
}
impl serde::Serialize for FieldPosition {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("FieldPosition", 2)?;
state.serialize_field("column", &self.column())?;
state.serialize_field("row", &self.row())?;
return state.end();
}
}
impl<'de> serde::Deserialize<'de> for FieldPosition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Column,
Row,
};
struct FieldPositionVisitor;
impl<'de> Visitor<'de> for FieldPositionVisitor {
type Value = FieldPosition;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
return formatter.write_str("struct FieldPosition")
}
fn visit_seq<V>(self, mut seq: V) -> Result<FieldPosition, V::Error>
where
V: SeqAccess<'de>,
{
let column = seq
.next_element()?
.ok_or_else(|| return de::Error::invalid_length(0, &self))?;
let row = seq
.next_element()?
.ok_or_else(|| return de::Error::invalid_length(1, &self))?;
return Ok(FieldPosition::new(column, row));
}
fn visit_map<V>(self, mut map: V) -> Result<FieldPosition, V::Error>
where
V: MapAccess<'de>,
{
let mut column = None;
let mut row = None;
while let Some(key) = map.next_key()? {
match key {
Field::Column => {
if column.is_some() {
return Err(de::Error::duplicate_field("column"));
}
column = Some(map.next_value()?);
}
Field::Row => {
if row.is_some() {
return Err(de::Error::duplicate_field("row"));
}
row = Some(map.next_value()?);
}
}
}
let column = column.ok_or_else(|| return de::Error::missing_field("column"))?;
let row = row.ok_or_else(|| return de::Error::missing_field("row"))?;
return Ok(FieldPosition::new(column, row));
}
}
const FIELDS: &[&str] = &["column", "row"];
return deserializer.deserialize_struct("Duration", FIELDS, FieldPositionVisitor)
}
}

View File

@@ -0,0 +1,54 @@
#![warn(
clippy::all,
clippy::restriction,
clippy::pedantic,
clippy::nursery,
clippy::cargo
)]
#![allow(clippy::cargo)]
// Style choices
#![allow(
clippy::missing_docs_in_private_items,
clippy::needless_return,
clippy::get_unwrap,
clippy::indexing_slicing,
clippy::explicit_iter_loop
)]
// Way too pedantic
#![allow(clippy::integer_arithmetic)]
#![allow(clippy::integer_division)]
// Useless
#![allow(clippy::missing_inline_in_public_items, clippy::missing_const_for_fn)]
// Useful for production
#![allow(
clippy::use_debug,
clippy::print_stdout,
clippy::dbg_macro,
clippy::panic
)]
// Useful for improving code robustness
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::option_unwrap_used,
clippy::option_expect_used,
clippy::as_conversions,
clippy::result_unwrap_used,
clippy::result_expect_used,
// clippy::wildcard_enum_match_arm
)]
#![allow(clippy::trivially_copy_pass_by_ref)]
#![allow(dead_code)]
mod cards;
pub use cards::*;
mod fieldposition;
pub use fieldposition::*;
mod positions;
pub use positions::*;
mod board_class;
pub use crate::board_class::*;
pub mod test_boards;
#[cfg(test)]
mod tests;

View File

@@ -0,0 +1,85 @@
use crate::cards::{CardTypeNoHua, SpecialCardType};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, Clone)]
pub enum BunkerSlot {
Empty,
Blocked(Option<SpecialCardType>),
Stash(CardTypeNoHua),
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)]
pub enum Position {
Field(crate::FieldPosition),
Bunker { slot_index: u8 },
Goal { slot_index: u8 },
}
impl std::fmt::Display for Position {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Field(x) => return write!(f, "Field ({})", x),
Self::Bunker { slot_index } => return write!(f, "Bunker #{}", slot_index),
Self::Goal { slot_index } => return write!(f, "Goal #{}", slot_index),
}
}
}
impl From<PositionNoGoal> for Position {
fn from(input: PositionNoGoal) -> Self {
match input {
PositionNoGoal::Field(x) => return Self::Field(x),
PositionNoGoal::Bunker { slot_index } => return Self::Bunker { slot_index },
};
}
}
impl PartialEq<PositionNoGoal> for Position {
fn eq(&self, other: &PositionNoGoal) -> bool {
return other.eq(self);
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Hash)]
pub enum PositionNoGoal {
Field(crate::FieldPosition),
Bunker { slot_index: u8 },
}
impl std::fmt::Display for PositionNoGoal {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
return write!(f, "{}", Position::from(*self));
}
}
#[derive(Debug, Clone)]
pub struct GoalTransformError;
impl std::convert::TryFrom<Position> for PositionNoGoal {
type Error = GoalTransformError;
fn try_from(input: Position) -> Result<Self, Self::Error> {
match input {
Position::Field(field_position) => return Result::Ok(Self::Field(field_position)),
Position::Bunker { slot_index } => {
return Result::Ok(Self::Bunker { slot_index });
}
Position::Goal { .. } => {
return Result::Err(GoalTransformError);
}
}
}
}
impl PartialEq<Position> for PositionNoGoal {
fn eq(&self, other: &Position) -> bool {
let other = <Self as std::convert::TryFrom<Position>>::try_from(other.clone());
match other {
Ok(other) => {
return Self::eq(self, &other);
}
Err(GoalTransformError) => {
return false;
}
}
}
}

View File

@@ -0,0 +1,21 @@
// This is incredibly shit, as other crates call this macro with _their_ CARGO_MANIFEST_DIR. Ideally we would move
// the boards into the board crate, and use the path of the board crate. But it seems to be really hard to get this done with
// macros, and const variables can't be used by macros, so we're using this hack for now.
#[macro_export]
macro_rules! TEST_BOARD_ROOT {
() => {
concat!(env!("CARGO_MANIFEST_DIR"),
"/../../aux/boards/")
}
}
#[macro_export]
macro_rules! load_test_board {
( $relpath:expr ) => {
{
<$crate::Board as std::str::FromStr>::from_str(
include_str!(concat!($crate::TEST_BOARD_ROOT!(),
$relpath)))
}
};
}

View File

@@ -0,0 +1,30 @@
use crate::{CardType, Error, NumberCard, NumberCardColor};
#[test]
pub fn check_test() -> Result<(), Box<dyn std::error::Error>> {
// let mut x = Board::from_file(std::path::Path::new(crate::test_boards::SPECIFIC)?;
let mut x = crate::load_test_board!("specific/solved.json")?;
assert_eq!(x.check(), Result::Ok(()));
assert_eq!(x.solved(), true);
x.field[2].push(CardType::Hua);
assert_eq!(x.check(), Result::Err(Error::CardDouble(CardType::Hua)));
x.hua_set = false;
assert_eq!(x.check(), Result::Ok(()));
x.field[2].push(CardType::Number(NumberCard {
suit: NumberCardColor::Black,
value: 9,
}));
assert_eq!(
x.check(),
Result::Err(Error::GoalTooHigh(NumberCard {
value: 9,
suit: NumberCardColor::Black
}))
);
x.goal[0] = Some(NumberCard {
suit: NumberCardColor::Black,
value: 8,
});
assert_eq!(x.check(), Result::Ok(()));
assert_eq!(x.solved(), false);
return Result::Ok(());
}

View File

@@ -0,0 +1,12 @@
[package]
name = "solving"
version = "0.1.0"
authors = ["Lukas Wölfer <lukas.woelfer@rwth-aachen.de>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
board = {path = "../board" }
actions = {path = "../actions"}
graphing = {package = "action_optimization", path = "../action_optimization"}

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 128 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
#![warn(
clippy::all,
clippy::restriction,
clippy::pedantic,
clippy::nursery,
clippy::cargo
)]
#![allow(clippy::cargo)]
// Style choices
#![allow(
clippy::missing_docs_in_private_items,
clippy::needless_return,
clippy::get_unwrap,
clippy::indexing_slicing,
clippy::explicit_iter_loop,
clippy::redundant_pub_crate, // Just dont understand it, maybe fix instead?
)]
// Way too pedantic
#![allow(clippy::integer_arithmetic)]
// Useless
#![allow(clippy::missing_inline_in_public_items, clippy::missing_const_for_fn)]
// Useful for production
#![allow(
clippy::use_debug,
clippy::print_stdout,
clippy::dbg_macro,
clippy::panic
)]
// Useful for improving code robustness
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::option_unwrap_used,
clippy::result_unwrap_used,
clippy::result_expect_used,
// clippy::wildcard_enum_match_arm
)]
#![allow(clippy::trivially_copy_pass_by_ref)]
#![allow(dead_code)]
mod board_state_iterator;
mod solve;
pub use solve::*;
pub mod benchmark;
#[cfg(test)]
pub mod tests;

View File

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

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