diff --git a/benchmark/__init__.py b/benchmark/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/benchmark/__main__.py b/benchmark/__main__.py new file mode 100644 index 0000000..bfe1021 --- /dev/null +++ b/benchmark/__main__.py @@ -0,0 +1,51 @@ +import tempfile +import time +from pathlib import Path + +import cv2 +import numpy as np + +import shenzhen_solitaire.card_detection.configuration as configuration +import shenzhen_solitaire.solver.solver as solver +from shenzhen_solitaire.card_detection.board_parser import parse_board + +benchmark_files = [ + "pictures/20190809172206_1.jpg", + "pictures/20190809172213_1.jpg", + "pictures/20190809172219_1.jpg", + "pictures/20190809172225_1.jpg", + "pictures/20190809172232_1.jpg", + "pictures/20190809172238_1.jpg", +] + + +def main() -> None: + for benchmark in benchmark_files: + print(f"{benchmark}:") + read_file_time = time.time() + + image = cv2.imread(benchmark) + load_config_time = time.time() + print(f"Load image: {load_config_time - read_file_time:5.2f}") + + conf = configuration.load("test_config.zip") + parse_board_time = time.time() + print(f"Load config: {parse_board_time - load_config_time:5.2f}") + + board = parse_board(image, conf) + solve_time = time.time() + print(f"Parse image: {solve_time - parse_board_time:5.2f}") + + solution_iterator = next(solver.solve(board, timeout=10), None) + finished_time = time.time() + print(f"Solve board: {finished_time - solve_time:5.2f}") + + assert board.check_correct() + if solution_iterator is None: + print("Solution timed out") + else: + print(f"Solved in {len(list(solution_iterator))} steps") + + +if __name__ == "__main__": + main() diff --git a/shenzhen_solitaire/clicker/__init__.py b/shenzhen_solitaire/clicker/__init__.py index e69de29..9089c27 100644 --- a/shenzhen_solitaire/clicker/__init__.py +++ b/shenzhen_solitaire/clicker/__init__.py @@ -0,0 +1,123 @@ +import time +from typing import List, Tuple + +import pyautogui +import shenzhen_solitaire.board as board +import shenzhen_solitaire.card_detection.adjustment as adjustment +import shenzhen_solitaire.card_detection.configuration as configuration +import shenzhen_solitaire.solver.board_actions as board_actions +import warnings + +def drag( + src: Tuple[int, int], dst: Tuple[int, int], offset: Tuple[int, int] = (0, 0) +) -> None: + + pyautogui.moveTo(x=src[0] + offset[0], y=src[1] + offset[1]) + pyautogui.dragTo( + x=dst[0] + offset[0], + y=dst[1] + offset[1], + duration=0.4, + tween=lambda x: 0 if x < 0.5 else 1, + ) + + +def click(point: Tuple[int, int], offset: Tuple[int, int] = (0, 0)) -> None: + pyautogui.moveTo(x=point[0] + offset[0], y=point[1] + offset[1]) + pyautogui.mouseDown() + time.sleep(0.2) + pyautogui.mouseUp() + + +def handle_action( + action: board_actions.Action, + offset: Tuple[int, int], + conf: configuration.Configuration, +) -> None: + if isinstance(action, board_actions.MoveAction): + src_x, src_y, _, _ = adjustment.get_square( + conf.field_adjustment, + index_x=action.source_id, + index_y=action.source_row_index, + ) + dst_x, dst_y, _, _ = adjustment.get_square( + conf.field_adjustment, + index_x=action.destination_id, + index_y=action.destination_row_index, + ) + drag((src_x, src_y), (dst_x, dst_y), 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_x, field_y, _, _ = adjustment.get_square( + conf.field_adjustment, + index_x=action.field_id, + index_y=action.field_row_index, + ) + bunker_x, bunker_y, _, _ = adjustment.get_square( + conf.bunker_adjustment, index_x=action.bunker_id, index_y=0, + ) + if action.to_bunker: + drag((field_x, field_y), (bunker_x, bunker_y), offset) + else: + drag((bunker_x, bunker_y), (field_x, field_y), offset) + return + if isinstance(action, board_actions.DragonKillAction): + dragon_sequence = [ + board.SpecialCard.Zhong, + board.SpecialCard.Fa, + board.SpecialCard.Bai, + ] + field_x, field_y, size_x, size_y = adjustment.get_square( + conf.special_button_adjustment, + index_x=0, + index_y=dragon_sequence.index(action.dragon), + ) + click( + (field_x + (size_x - field_x) // 2, field_y + (size_y - field_y) // 2), + offset, + ) + time.sleep(0.5) + return + if isinstance(action, board_actions.GoalAction): + dst_x, dst_y, _, _ = adjustment.get_square( + conf.goal_adjustment, index_x=action.goal_id, index_y=0, + ) + if action.source_position == board.Position.Field: + assert action.source_row_index is not None + src_x, src_y, _, _ = adjustment.get_square( + conf.field_adjustment, + index_x=action.source_id, + index_y=action.source_row_index, + ) + else: + assert action.source_position == board.Position.Bunker + src_x, src_y, _, _ = adjustment.get_square( + conf.bunker_adjustment, index_x=action.source_id, index_y=0, + ) + drag((src_x, src_y), (dst_x, dst_y), offset) + return + raise AssertionError("You forgot an Action type") + +def automatic(action: board_actions.Action) -> bool: + if isinstance(action, board_actions.HuaKillAction): + return True + if isinstance(action, board_actions.GoalAction) and action.obvious: + return True + return False + +def handle_actions( + actions: List[board_actions.Action], + offset: Tuple[int, int], + conf: configuration.Configuration, +) -> None: + automatic_count = 0 + for action in actions: + print(action) + if automatic(action): + automatic_count += 1 + else: + time.sleep(0.5 * automatic_count) + automatic_count = 0 + handle_action(action, offset, conf) diff --git a/shenzhen_solitaire/clicker/main.py b/shenzhen_solitaire/clicker/main.py deleted file mode 100644 index 9089c27..0000000 --- a/shenzhen_solitaire/clicker/main.py +++ /dev/null @@ -1,123 +0,0 @@ -import time -from typing import List, Tuple - -import pyautogui -import shenzhen_solitaire.board as board -import shenzhen_solitaire.card_detection.adjustment as adjustment -import shenzhen_solitaire.card_detection.configuration as configuration -import shenzhen_solitaire.solver.board_actions as board_actions -import warnings - -def drag( - src: Tuple[int, int], dst: Tuple[int, int], offset: Tuple[int, int] = (0, 0) -) -> None: - - pyautogui.moveTo(x=src[0] + offset[0], y=src[1] + offset[1]) - pyautogui.dragTo( - x=dst[0] + offset[0], - y=dst[1] + offset[1], - duration=0.4, - tween=lambda x: 0 if x < 0.5 else 1, - ) - - -def click(point: Tuple[int, int], offset: Tuple[int, int] = (0, 0)) -> None: - pyautogui.moveTo(x=point[0] + offset[0], y=point[1] + offset[1]) - pyautogui.mouseDown() - time.sleep(0.2) - pyautogui.mouseUp() - - -def handle_action( - action: board_actions.Action, - offset: Tuple[int, int], - conf: configuration.Configuration, -) -> None: - if isinstance(action, board_actions.MoveAction): - src_x, src_y, _, _ = adjustment.get_square( - conf.field_adjustment, - index_x=action.source_id, - index_y=action.source_row_index, - ) - dst_x, dst_y, _, _ = adjustment.get_square( - conf.field_adjustment, - index_x=action.destination_id, - index_y=action.destination_row_index, - ) - drag((src_x, src_y), (dst_x, dst_y), 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_x, field_y, _, _ = adjustment.get_square( - conf.field_adjustment, - index_x=action.field_id, - index_y=action.field_row_index, - ) - bunker_x, bunker_y, _, _ = adjustment.get_square( - conf.bunker_adjustment, index_x=action.bunker_id, index_y=0, - ) - if action.to_bunker: - drag((field_x, field_y), (bunker_x, bunker_y), offset) - else: - drag((bunker_x, bunker_y), (field_x, field_y), offset) - return - if isinstance(action, board_actions.DragonKillAction): - dragon_sequence = [ - board.SpecialCard.Zhong, - board.SpecialCard.Fa, - board.SpecialCard.Bai, - ] - field_x, field_y, size_x, size_y = adjustment.get_square( - conf.special_button_adjustment, - index_x=0, - index_y=dragon_sequence.index(action.dragon), - ) - click( - (field_x + (size_x - field_x) // 2, field_y + (size_y - field_y) // 2), - offset, - ) - time.sleep(0.5) - return - if isinstance(action, board_actions.GoalAction): - dst_x, dst_y, _, _ = adjustment.get_square( - conf.goal_adjustment, index_x=action.goal_id, index_y=0, - ) - if action.source_position == board.Position.Field: - assert action.source_row_index is not None - src_x, src_y, _, _ = adjustment.get_square( - conf.field_adjustment, - index_x=action.source_id, - index_y=action.source_row_index, - ) - else: - assert action.source_position == board.Position.Bunker - src_x, src_y, _, _ = adjustment.get_square( - conf.bunker_adjustment, index_x=action.source_id, index_y=0, - ) - drag((src_x, src_y), (dst_x, dst_y), offset) - return - raise AssertionError("You forgot an Action type") - -def automatic(action: board_actions.Action) -> bool: - if isinstance(action, board_actions.HuaKillAction): - return True - if isinstance(action, board_actions.GoalAction) and action.obvious: - return True - return False - -def handle_actions( - actions: List[board_actions.Action], - offset: Tuple[int, int], - conf: configuration.Configuration, -) -> None: - automatic_count = 0 - for action in actions: - print(action) - if automatic(action): - automatic_count += 1 - else: - time.sleep(0.5 * automatic_count) - automatic_count = 0 - handle_action(action, offset, conf) diff --git a/shenzhen_solitaire/solver/solver.py b/shenzhen_solitaire/solver/solver.py index 83caabe..2b33830 100644 --- a/shenzhen_solitaire/solver/solver.py +++ b/shenzhen_solitaire/solver/solver.py @@ -51,7 +51,7 @@ class ActionStack: def solve( - board: Board, *, timeout: Optional[float] = None + board: Board, *, timeout: Optional[float] = None, verbose: bool = False ) -> Iterator[List[board_actions.Action]]: """Solve a solitaire puzzle""" state_set = {board.state_identifier} @@ -85,7 +85,8 @@ def solve( count += 1 if count > 5000: count = 0 - print(f"{time.time() - iter_start} {len(stack)} {board.goal}") + if verbose: + print(f"{time.time() - iter_start} {len(stack)} {board.goal}") if timeout is not None and time.time() - iter_start > timeout: return diff --git a/tools/assistant.py b/tools/assistant.py index 1b310a4..4b3c739 100644 --- a/tools/assistant.py +++ b/tools/assistant.py @@ -7,7 +7,7 @@ import numpy as np import pyautogui import shenzhen_solitaire.card_detection.configuration as configuration -import shenzhen_solitaire.clicker.main as clicker +import shenzhen_solitaire.clicker as clicker import shenzhen_solitaire.solver.solver as solver from shenzhen_solitaire.card_detection.board_parser import parse_board