diff --git a/benchmark/__main__.py b/benchmark/__main__.py deleted file mode 100644 index bfe1021..0000000 --- a/benchmark/__main__.py +++ /dev/null @@ -1,51 +0,0 @@ -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/benchmark/timing.py b/benchmark/timing.py new file mode 100644 index 0000000..9abe032 --- /dev/null +++ b/benchmark/timing.py @@ -0,0 +1,35 @@ +import multiprocessing +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 + +from .util import run_benchmark + +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: + with multiprocessing.Pool() as pool: + result = pool.imap_unordered( + run_benchmark, [Path(benchmark) for benchmark in benchmark_files] + ) + for current_result in result: + print(current_result) + + +if __name__ == "__main__": + main() diff --git a/benchmark/unsolved.py b/benchmark/unsolved.py new file mode 100644 index 0000000..06b3d9c --- /dev/null +++ b/benchmark/unsolved.py @@ -0,0 +1,41 @@ +import multiprocessing +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 + +from .util import run_benchmark + +benchmark_files = [ + "pictures/unsolved/tmp1ern14si.png", + "pictures/unsolved/tmp2_0vn4tl.png", + "pictures/unsolved/tmp32jmcnfp.png", + "pictures/unsolved/tmpcml2ldfl.png", + "pictures/unsolved/tmpd7rbwwdb.png", + "pictures/unsolved/tmpdudxuw0s.png", + "pictures/unsolved/tmpeplvz9bk.png", + "pictures/unsolved/tmph_esy__3.png", + "pictures/unsolved/tmpn95ueb7_.png", + "pictures/unsolved/tmpqzay4q08.png", + "pictures/unsolved/tmputbych59.png", + "pictures/unsolved/tmpx4uo6pg3.png", +] + + +def main() -> None: + with multiprocessing.Pool() as pool: + result = pool.imap_unordered( + run_benchmark, [Path(benchmark) for benchmark in benchmark_files] + ) + for current_result in result: + print(current_result) + + +if __name__ == "__main__": + main() diff --git a/benchmark/util.py b/benchmark/util.py new file mode 100644 index 0000000..db27517 --- /dev/null +++ b/benchmark/util.py @@ -0,0 +1,36 @@ +import time +from pathlib import Path + +import cv2 + +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 + + +def run_benchmark(benchmark: Path) -> str: + result = "" + result += f"{benchmark}:\n" + read_file_time = time.time() + image = cv2.imread(str(benchmark)) + load_config_time = time.time() + result += f"\tLoad image: {load_config_time - read_file_time:5.2f}\n" + + conf = configuration.load("test_config.zip") + parse_board_time = time.time() + result += f"\tLoad config: {parse_board_time - load_config_time:5.2f}\n" + + board = parse_board(image, conf) + solve_time = time.time() + result += f"\tParse image: {solve_time - parse_board_time:5.2f}\n" + + solution_iterator = next(solver.solve(board, timeout=10), None) + finished_time = time.time() + result += f"\tSolve board: {finished_time - solve_time:5.2f}\n" + + assert board.check_correct() + if solution_iterator is None: + result += "\tSolution timed out\n" + else: + result += f"\tSolved in {len(list(solution_iterator))} steps\n" + return result diff --git a/shenzhen_solitaire/clicker/__init__.py b/shenzhen_solitaire/clicker/__init__.py index 61c176c..687b838 100644 --- a/shenzhen_solitaire/clicker/__init__.py +++ b/shenzhen_solitaire/clicker/__init__.py @@ -8,6 +8,7 @@ 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: @@ -21,6 +22,18 @@ def drag( ) +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, + ) + + 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() @@ -28,40 +41,49 @@ def click(point: Tuple[int, int], offset: Tuple[int, int] = (0, 0)) -> None: pyautogui.mouseUp() +def clickSquare( + field: Tuple[int, int, int, int], offset: Tuple[int, int] = (0, 0) +) -> None: + click( + (field[0] + (field[2] - field[0]) // 2, field[1] + (field[3] - field[1]) // 2), + offset, + ) + + def 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( + src = adjustment.get_square( conf.field_adjustment, index_x=action.source_id, index_y=action.source_row_index, ) - dst_x, dst_y, _, _ = adjustment.get_square( + dst = 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) + 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_x, field_y, _, _ = adjustment.get_square( + field = adjustment.get_square( conf.field_adjustment, index_x=action.field_id, index_y=action.field_row_index, ) - bunker_x, bunker_y, _, _ = adjustment.get_square( + bunker = 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) + dragSquare(field, bunker, offset) else: - drag((bunker_x, bunker_y), (field_x, field_y), offset) + dragSquare(bunker, field, offset) return if isinstance(action, board_actions.DragonKillAction): dragon_sequence = [ @@ -69,34 +91,33 @@ def handle_action( board.SpecialCard.Fa, board.SpecialCard.Bai, ] - field_x, field_y, size_x, size_y = adjustment.get_square( + field = 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, + clickSquare( + field, offset, ) time.sleep(1) return if isinstance(action, board_actions.GoalAction): - dst_x, dst_y, _, _ = adjustment.get_square( + dst = adjustment.get_square( conf.goal_adjustment, index_x=action.goal_id, index_y=0, ) if action.source_position == board.Position.Field: assert action.source_row_index is not None - src_x, src_y, _, _ = adjustment.get_square( + src = adjustment.get_square( conf.field_adjustment, index_x=action.source_id, index_y=action.source_row_index, ) else: assert action.source_position == board.Position.Bunker - src_x, src_y, _, _ = adjustment.get_square( + src = adjustment.get_square( conf.bunker_adjustment, index_x=action.source_id, index_y=0, ) - drag((src_x, src_y), (dst_x, dst_y), offset) + dragSquare(src, dst, offset) return raise AssertionError("You forgot an Action type") @@ -109,7 +130,7 @@ def handle_actions( automatic_count = 0 for action in actions: print(action) - if action.automatic(): + if isinstance(action, board_actions.HuaKillAction): automatic_count += 1 else: time.sleep(0.5 * automatic_count) diff --git a/shenzhen_solitaire/solver/board_possibilities.py b/shenzhen_solitaire/solver/board_possibilities.py index 537b512..49ce510 100644 --- a/shenzhen_solitaire/solver/board_possibilities.py +++ b/shenzhen_solitaire/solver/board_possibilities.py @@ -129,7 +129,7 @@ def possible_goal_move_actions( if not (card.number == search_board.getGoal(card.suit) + 1): continue obvious = all( - search_board.getGoal(other_suit) >= card.number - 1 + search_board.getGoal(other_suit) >= card.number - 2 for other_suit in set(board.NumberCard.Suit) - {card.suit} ) yield board_actions.GoalAction( diff --git a/shenzhen_solitaire/solver/solver.py b/shenzhen_solitaire/solver/solver.py index a4e15f1..3f09f03 100644 --- a/shenzhen_solitaire/solver/solver.py +++ b/shenzhen_solitaire/solver/solver.py @@ -86,7 +86,7 @@ def solve( iter_start = time.time() count = 0 - while stack: + while len(stack) > 0: count += 1 if count > 5000: diff --git a/test/test_chain.py b/test/test_chain.py index e2c7a46..fa87771 100644 --- a/test/test_chain.py +++ b/test/test_chain.py @@ -3,9 +3,12 @@ import unittest from shenzhen_solitaire.board import NumberCard, Position from shenzhen_solitaire.solver import board_possibilities -from shenzhen_solitaire.solver.board_actions import (BunkerizeAction, - GoalAction, HuaKillAction, - MoveAction) +from shenzhen_solitaire.solver.board_actions import ( + BunkerizeAction, + GoalAction, + HuaKillAction, + MoveAction, +) from .boards import TEST_BOARD diff --git a/tools/assistant.py b/tools/assistant.py index 4b3c739..1d2de6a 100644 --- a/tools/assistant.py +++ b/tools/assistant.py @@ -4,6 +4,7 @@ from pathlib import Path import cv2 import numpy as np +import os import pyautogui import shenzhen_solitaire.card_detection.configuration as configuration @@ -28,14 +29,21 @@ def solve() -> None: conf = configuration.load("test_config.zip") board = parse_board(image, conf) assert board.check_correct() - solution_iterator = next(solver.solve(board, timeout=10), None) + solution_iterator = next(solver.solve(board, timeout=10, verbose=True), None) if solution_iterator is None: clicker.click(NEW_BUTTON, OFFSET) time.sleep(10) + fd, outfile = tempfile.mkstemp( + dir="E:/shenzhen-solitaire/unsolved", suffix=".png" + ) + sock = os.fdopen(fd, "w") + sock.close() + cv2.imwrite(outfile, image) return - solution=list(solution_iterator) + solution = list(solution_iterator) print(f"Solved in {len(solution)} steps") clicker.handle_actions(solution, OFFSET, conf) + print("Solved") time.sleep(2) clicker.click(NEW_BUTTON, OFFSET) time.sleep(7)