Worked on assistant
This commit is contained in:
@@ -105,7 +105,6 @@ def _save_adjustments(zip_file: zipfile.ZipFile, conf: Configuration) -> None:
|
||||
adjustments[SPECIAL_BUTTON_ADJUSTMENT_KEY] = dataclasses.asdict(
|
||||
conf.special_button_adjustment
|
||||
)
|
||||
print(adjustments)
|
||||
zip_file.writestr(
|
||||
ADJUSTMENT_FILE_NAME, json.dumps(adjustments),
|
||||
)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import time
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Dict, Any
|
||||
|
||||
import pyautogui
|
||||
import shenzhen_solitaire.board as board
|
||||
import shenzhen_solitaire.card_detection.adjustment as adjustment
|
||||
import shenzhen_solitaire.card_detection.configuration as configuration
|
||||
import shenzhen_solitaire.solver.board_actions as board_actions
|
||||
import warnings
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
def drag(
|
||||
src: Tuple[int, int], dst: Tuple[int, int], offset: Tuple[int, int] = (0, 0)
|
||||
@@ -50,80 +51,73 @@ def clickSquare(
|
||||
)
|
||||
|
||||
|
||||
def handle_action(
|
||||
action: board_actions.Action,
|
||||
offset: Tuple[int, int],
|
||||
conf: configuration.Configuration,
|
||||
) -> None:
|
||||
if isinstance(action, board_actions.MoveAction):
|
||||
src = adjustment.get_square(
|
||||
conf.field_adjustment,
|
||||
index_x=action.source_id,
|
||||
index_y=action.source_row_index,
|
||||
@dataclass
|
||||
class DragAction:
|
||||
source: Tuple[int, int]
|
||||
destination: Tuple[int, int]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClickAction:
|
||||
destination: Tuple[int, int]
|
||||
|
||||
|
||||
class WaitAction:
|
||||
pass
|
||||
|
||||
|
||||
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,
|
||||
int(field["row"]) * conf.field_adjustment.dy + conf.field_adjustment.y,
|
||||
)
|
||||
|
||||
|
||||
def parse_action(action: Dict[str, Any], conf: configuration.Configuration):
|
||||
assert len(action) == 1
|
||||
action_name, info = next(iter(action.items()))
|
||||
action_name = action_name.lower()
|
||||
if action_name == "bunkerize":
|
||||
field = _parse_field(info["field_position"], conf)
|
||||
bunker = (
|
||||
int(info["bunker_slot_index"]) * conf.bunker_adjustment.dx
|
||||
+ conf.bunker_adjustment.x,
|
||||
conf.bunker_adjustment.y,
|
||||
)
|
||||
dst = adjustment.get_square(
|
||||
conf.field_adjustment,
|
||||
index_x=action.destination_id,
|
||||
index_y=action.destination_row_index,
|
||||
)
|
||||
dragSquare(src, dst, offset)
|
||||
return
|
||||
if isinstance(action, board_actions.HuaKillAction):
|
||||
warnings.warn("Hua kill should be handled before handle_action")
|
||||
return
|
||||
if isinstance(action, board_actions.BunkerizeAction):
|
||||
field = adjustment.get_square(
|
||||
conf.field_adjustment,
|
||||
index_x=action.field_id,
|
||||
index_y=action.field_row_index,
|
||||
)
|
||||
bunker = adjustment.get_square(
|
||||
conf.bunker_adjustment, index_x=action.bunker_id, index_y=0,
|
||||
)
|
||||
if action.to_bunker:
|
||||
dragSquare(field, bunker, offset)
|
||||
if str(info["to_bunker"]).lower() == "true":
|
||||
return DragAction(source=field, destination=bunker)
|
||||
else:
|
||||
dragSquare(bunker, field, offset)
|
||||
return
|
||||
if isinstance(action, board_actions.DragonKillAction):
|
||||
dragon_sequence = [
|
||||
board.SpecialCard.Zhong,
|
||||
board.SpecialCard.Fa,
|
||||
board.SpecialCard.Bai,
|
||||
]
|
||||
field = adjustment.get_square(
|
||||
conf.special_button_adjustment,
|
||||
index_x=0,
|
||||
index_y=dragon_sequence.index(action.dragon),
|
||||
return DragAction(source=bunker, destination=field)
|
||||
elif action_name == "move":
|
||||
return DragAction(
|
||||
source=_parse_field(info["source"], conf),
|
||||
destination=_parse_field(info["source"], conf),
|
||||
)
|
||||
clickSquare(
|
||||
field, offset,
|
||||
elif action_name == "dragonkill":
|
||||
return ClickAction()
|
||||
elif action_name == "goal":
|
||||
goal = (
|
||||
int(info["goal_slot_index"]) * conf.goal_adjustment.dx
|
||||
+ conf.goal_adjustment.x,
|
||||
conf.goal_adjustment.y,
|
||||
)
|
||||
time.sleep(1)
|
||||
return
|
||||
if isinstance(action, board_actions.GoalAction):
|
||||
dst = adjustment.get_square(
|
||||
conf.goal_adjustment, index_x=action.goal_id, index_y=0,
|
||||
)
|
||||
if action.source_position == board.Position.Field:
|
||||
assert action.source_row_index is not None
|
||||
src = adjustment.get_square(
|
||||
conf.field_adjustment,
|
||||
index_x=action.source_id,
|
||||
index_y=action.source_row_index,
|
||||
)
|
||||
if "Field" in info["source"]:
|
||||
source = _parse_field(info["source"]["Field"], conf)
|
||||
else:
|
||||
assert action.source_position == board.Position.Bunker
|
||||
src = adjustment.get_square(
|
||||
conf.bunker_adjustment, index_x=action.source_id, index_y=0,
|
||||
source = (
|
||||
int(info["source"]["Bunker"]["slot_index"]) * conf.bunker_adjustment.dx
|
||||
+ conf.bunker_adjustment.x,
|
||||
conf.bunker_adjustment.y,
|
||||
)
|
||||
dragSquare(src, dst, offset)
|
||||
return
|
||||
raise AssertionError("You forgot an Action type")
|
||||
return DragAction(source=source, destination=goal)
|
||||
elif action_name == "huakill":
|
||||
return WaitAction()
|
||||
|
||||
|
||||
def handle_actions(
|
||||
actions: List[board_actions.Action],
|
||||
actions: List[Dict[str, Dict[str, Any]]],
|
||||
offset: Tuple[int, int],
|
||||
conf: configuration.Configuration,
|
||||
) -> None:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,19 +1,19 @@
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from typing import List, Dict, Any
|
||||
import json
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pyautogui
|
||||
|
||||
import shenzhen_solitaire.card_detection.configuration as configuration
|
||||
import shenzhen_solitaire.clicker as clicker
|
||||
import shenzhen_solitaire.solver.solver as solver
|
||||
from shenzhen_solitaire.board import Board
|
||||
from shenzhen_solitaire.card_detection.board_parser import parse_start_board
|
||||
from shenzhen_solitaire.solver.board_actions import Action
|
||||
|
||||
OFFSET = (0, 0)
|
||||
# SIZE = (2560, 1440)
|
||||
@@ -22,39 +22,29 @@ NEW_BUTTON = (1900, 1100)
|
||||
|
||||
SAVE_UNSOLVED = False
|
||||
UNSOLVED_DIR = "E:/shenzhen-solitaire/unsolved"
|
||||
SOLVER_PATH = '/home/lukas/documents/coding/rust/shenzhen-solitaire/target/release/solver'
|
||||
|
||||
def extern_solve(board: Board) -> List[Dict[str, Any]]:
|
||||
result = subprocess.run([SOLVER_PATH], input=board.to_json(), capture_output=True, text=True)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
def extern_solve(board: Board) -> List[Action]:
|
||||
pass
|
||||
|
||||
|
||||
def solve(conf: configuration.Configuration) -> None:
|
||||
def take_screenshot() :
|
||||
with tempfile.TemporaryDirectory(prefix="shenzhen_solitaire") as screenshot_dir:
|
||||
print("Taking screenshot")
|
||||
screenshot_file = Path(screenshot_dir) / "screenshot.png"
|
||||
screenshot = pyautogui.screenshot(region=(*OFFSET, *SIZE))
|
||||
screenshot.save(screenshot_file)
|
||||
image = cv2.imread(str(screenshot_file))
|
||||
input()
|
||||
return image
|
||||
|
||||
print("Solving")
|
||||
def solve(conf: configuration.Configuration) -> None:
|
||||
image = take_screenshot()
|
||||
board = parse_start_board(image, conf)
|
||||
print(board.to_json())
|
||||
assert board.check_correct()
|
||||
input()
|
||||
solution_iterator = next(solver.solve(board, timeout=10, verbose=True), None)
|
||||
if solution_iterator is None:
|
||||
clicker.click(NEW_BUTTON, OFFSET)
|
||||
time.sleep(10)
|
||||
if SAVE_UNSOLVED:
|
||||
fd, outfile = tempfile.mkstemp(dir=UNSOLVED_DIR, suffix=".png")
|
||||
sock = os.fdopen(fd, "w")
|
||||
sock.close()
|
||||
cv2.imwrite(outfile, image)
|
||||
return
|
||||
solution = list(solution_iterator)
|
||||
print(f"Solved in {len(solution)} steps")
|
||||
clicker.handle_actions(solution, OFFSET, conf)
|
||||
actions = extern_solve(board)
|
||||
assert 0
|
||||
print(f"Solved in {len(actions)} steps")
|
||||
clicker.handle_actions(actions, OFFSET, conf)
|
||||
print("Solved")
|
||||
time.sleep(2)
|
||||
clicker.click(NEW_BUTTON, OFFSET)
|
||||
@@ -62,8 +52,18 @@ def solve(conf: configuration.Configuration) -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Solve board"
|
||||
)
|
||||
parser.add_argument(
|
||||
"config_path",
|
||||
type=str,
|
||||
help="Config path",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
time.sleep(3)
|
||||
conf = configuration.load("test_config.zip")
|
||||
conf = configuration.load(args.config_path)
|
||||
while True:
|
||||
solve(conf)
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import copy
|
||||
import dataclasses
|
||||
import json
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
import shenzhen_solitaire.card_detection.adjustment as adjustment
|
||||
import shenzhen_solitaire.card_detection.card_finder as card_finder
|
||||
from shenzhen_solitaire.card_detection.configuration import Configuration
|
||||
import argparse
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Generate a configuration"""
|
||||
|
||||
parser = argparse.ArgumentParser(description='Process some integers.')
|
||||
parser.add_argument('screenshot_path', metavar='screenshot_path', type=str,
|
||||
help='Path to the screenshot')
|
||||
|
||||
args = parser.parse_args()
|
||||
print(args.screenshot_path)
|
||||
image = cv2.imread(args.screenshot_path)
|
||||
|
||||
border_adjustment = adjustment.adjust_squares(image, count_x=8, count_y=13)
|
||||
border_square_pos = adjustment.adjust_squares(
|
||||
image, count_x=1, count_y=1, adjustment=copy.deepcopy(border_adjustment)
|
||||
)
|
||||
border_square = card_finder.get_field_squares(image, border_square_pos, 1, 1)
|
||||
empty_square_pos = adjustment.adjust_squares(
|
||||
image, count_x=1, count_y=1, adjustment=copy.deepcopy(border_adjustment)
|
||||
)
|
||||
empty_square = card_finder.get_field_squares(image, empty_square_pos, 1, 1)
|
||||
|
||||
cv2.imwrite("/tmp/border_square.png", border_square[0])
|
||||
cv2.imwrite("/tmp/empty_square.png", empty_square[0])
|
||||
print(json.dumps(dataclasses.asdict(border_adjustment)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,43 +0,0 @@
|
||||
import copy
|
||||
import dataclasses
|
||||
import json
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
import shenzhen_solitaire.card_detection.adjustment as adjustment
|
||||
import shenzhen_solitaire.card_detection.card_finder as card_finder
|
||||
from shenzhen_solitaire.card_detection.configuration import Configuration
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Generate a configuration"""
|
||||
image = cv2.imread("pictures/specific/BunkerCards.jpg")
|
||||
|
||||
bunker_adjustment = adjustment.adjust_squares(
|
||||
image,
|
||||
count_x=3,
|
||||
count_y=1,
|
||||
adjustment=adjustment.Adjustment(
|
||||
**{"x": 730, "y": 310, "w": 19, "h": 21, "dx": 152, "dy": 0}
|
||||
),
|
||||
)
|
||||
print(json.dumps(dataclasses.asdict(bunker_adjustment)))
|
||||
|
||||
back_image = cv2.imread("pictures/specific/BaiShiny.jpg")
|
||||
back_squares = card_finder.get_field_squares(
|
||||
back_image, count_x=1, count_y=3, adjustment=copy.deepcopy(bunker_adjustment)
|
||||
)
|
||||
|
||||
green_image = cv2.imread("pictures/20190809172213_1.jpg")
|
||||
green_squares = card_finder.get_field_squares(
|
||||
green_image, count_x=1, count_y=3, adjustment=copy.deepcopy(bunker_adjustment)
|
||||
)
|
||||
|
||||
cv2.imwrite("/tmp/bunker_green_1.png", green_squares[0])
|
||||
cv2.imwrite("/tmp/bunker_green_2.png", green_squares[1])
|
||||
cv2.imwrite("/tmp/bunker_green_3.png", green_squares[2])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,7 +2,7 @@ import argparse
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
import copy
|
||||
from shenzhen_solitaire.card_detection import configuration, adjustment, card_finder
|
||||
from shenzhen_solitaire.card_detection.configuration import Configuration
|
||||
|
||||
@@ -31,11 +31,19 @@ def main() -> None:
|
||||
args = parser.parse_args()
|
||||
image = cv2.imread(args.screenshot_path)
|
||||
conf = configuration.load(args.config_path)
|
||||
|
||||
squares = card_finder.get_field_squares(image, conf.field_adjustment, 5, 8)
|
||||
catalogue = card_finder.catalogue_cards(squares)
|
||||
conf.catalogue.extend(catalogue)
|
||||
|
||||
conf.card_border.extend(
|
||||
card_finder.get_field_squares(image, conf.border_adjustment, 1, 1)
|
||||
)
|
||||
|
||||
empty_adjust = copy.deepcopy(conf.border_adjustment)
|
||||
empty_adjust.y = empty_adjust.y + 4 * empty_adjust.dy
|
||||
conf.empty_card.extend(card_finder.get_field_squares(image, empty_adjust, 1, 1))
|
||||
|
||||
conf.green_card.extend(
|
||||
card_finder.get_field_squares(image, conf.bunker_adjustment, 1, 3)
|
||||
)
|
||||
@@ -45,7 +53,7 @@ def main() -> None:
|
||||
conf.green_card.extend(
|
||||
card_finder.get_field_squares(image, conf.hua_adjustment, 1, 1)
|
||||
)
|
||||
conf.catalogue.extend(catalogue)
|
||||
|
||||
configuration.save(conf, args.config_path)
|
||||
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import argparse
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from shenzhen_solitaire.card_detection import configuration, adjustment, card_finder
|
||||
from shenzhen_solitaire.card_detection.configuration import Configuration
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Generate a configuration"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate pictures for symbols, "
|
||||
"requires screenshot of field with no moved cards, "
|
||||
"so 8 columns of 5 cards each"
|
||||
)
|
||||
parser.add_argument(
|
||||
"screenshot_path",
|
||||
metavar="screenshot_path",
|
||||
type=str,
|
||||
help="Path to the screenshot",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--conf",
|
||||
dest="config_path",
|
||||
type=str,
|
||||
default="config.zip",
|
||||
help="Path to existing config to be merged, or new config",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
print(args.screenshot_path)
|
||||
image = cv2.imread(args.screenshot_path)
|
||||
|
||||
adj = adjustment.adjust_field(image)
|
||||
squares = card_finder.get_field_squares(image, adj, 5, 8)
|
||||
catalogue = card_finder.catalogue_cards(squares)
|
||||
generated_config = Configuration(field_adjustment=adj, catalogue=catalogue, meta={})
|
||||
configuration.save(generated_config, args.config_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,38 +0,0 @@
|
||||
import copy
|
||||
import dataclasses
|
||||
import json
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
import shenzhen_solitaire.card_detection.adjustment as adjustment
|
||||
import shenzhen_solitaire.card_detection.card_finder as card_finder
|
||||
from shenzhen_solitaire.card_detection.configuration import Configuration
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Generate a configuration"""
|
||||
image = cv2.imread("pictures/specific/BaiShiny.jpg")
|
||||
|
||||
goal_adjustment = adjustment.adjust_squares(
|
||||
image,
|
||||
count_x=3,
|
||||
count_y=1,
|
||||
adjustment=adjustment.Adjustment(
|
||||
**{"x": 1490, "y": 310, "w": 19, "h": 21, "dx": 152, "dy": 0}
|
||||
),
|
||||
)
|
||||
print(json.dumps(dataclasses.asdict(goal_adjustment)))
|
||||
|
||||
green_image = cv2.imread("pictures/20190809172213_1.jpg")
|
||||
green_squares = card_finder.get_field_squares(
|
||||
green_image, count_x=1, count_y=3, adjustment=copy.deepcopy(goal_adjustment)
|
||||
)
|
||||
|
||||
cv2.imwrite("/tmp/goal_green_1.png", green_squares[0])
|
||||
cv2.imwrite("/tmp/goal_green_2.png", green_squares[1])
|
||||
cv2.imwrite("/tmp/goal_green_3.png", green_squares[2])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,34 +0,0 @@
|
||||
import copy
|
||||
import dataclasses
|
||||
import json
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
import shenzhen_solitaire.card_detection.adjustment as adjustment
|
||||
import shenzhen_solitaire.card_detection.card_finder as card_finder
|
||||
from shenzhen_solitaire.card_detection.configuration import Configuration
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Generate a configuration"""
|
||||
image = cv2.imread("pictures/specific/BunkerCards.jpg")
|
||||
|
||||
hua_adjustment = adjustment.adjust_squares(
|
||||
image,
|
||||
count_x=1,
|
||||
count_y=1,
|
||||
adjustment=adjustment.Adjustment(
|
||||
**{"x": 1299, "y": 314, "w": 19, "h": 21, "dx": 0, "dy": 0}
|
||||
),
|
||||
)
|
||||
print(json.dumps(dataclasses.asdict(hua_adjustment)))
|
||||
green_image = cv2.imread("pictures/specific/ZhongShiny.jpg")
|
||||
hua_green = card_finder.get_field_squares(
|
||||
green_image, hua_adjustment, count_x=1, count_y=1
|
||||
)
|
||||
cv2.imwrite("/tmp/hua_green.png", hua_green[0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,14 +1,21 @@
|
||||
from shenzhen_solitaire.card_detection.board_parser import parse_to_json
|
||||
import shenzhen_solitaire.card_detection.configuration as configuration
|
||||
import cv2
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
import cv2
|
||||
|
||||
import shenzhen_solitaire.card_detection.configuration as configuration
|
||||
from shenzhen_solitaire.card_detection.board_parser import parse_to_json
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
print("Give filename pls")
|
||||
return
|
||||
image = cv2.imread(str(sys.argv[1]))
|
||||
parser = argparse.ArgumentParser(description="Parse board to json")
|
||||
parser.add_argument("board_path", type=str, help="Path to image of board")
|
||||
parser.add_argument(
|
||||
"--config", dest="config_path", type=str, help="Config path",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
image = cv2.imread(args.board_path)
|
||||
|
||||
conf = configuration.load("test_config.zip")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user