Worked on assistant

This commit is contained in:
Lukas Wölfer
2020-06-12 22:40:58 +02:00
parent f4ac445f61
commit b5d74d1ac0
14 changed files with 112 additions and 883 deletions

View File

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

View File

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

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)