diff --git a/shenzhen_solitaire/board_actions.py b/shenzhen_solitaire/board_actions.py index b76d93b..1df92be 100644 --- a/shenzhen_solitaire/board_actions.py +++ b/shenzhen_solitaire/board_actions.py @@ -6,21 +6,39 @@ from . import board class Action: + _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: - pass + #print(f"Applying {str(self)}") + assert self._before_state == 0 + assert self._after_state == 0 + self._before_state = action_board.state_identifier + self._apply(action_board) + self._after_state = action_board.state_identifier + def undo(self, action_board: board.Board) -> None: - pass + #print(f"Undoing {str(self)}") + assert action_board.state_identifier == self._after_state + self._undo(action_board) + assert action_board.state_identifier == self._before_state @dataclass -class GoalAction: +class GoalAction(Action): """Move card from field to goal""" card: board.NumberCard source_id: int source_position: board.Position - def apply(self, action_board: board.Board) -> None: + def _apply(self, action_board: board.Board) -> None: """Do action""" if self.source_position == board.Position.Field: assert action_board.field[self.source_id][-1] == self.card @@ -35,7 +53,7 @@ class GoalAction: else: raise RuntimeError("Unknown position") - def undo(self, action_board: board.Board) -> None: + def _undo(self, action_board: board.Board) -> None: """Undo action""" assert action_board.goal[self.card.suit] == self.card.number if self.source_position == board.Position.Field: @@ -49,15 +67,13 @@ class GoalAction: @dataclass -class BunkerizeAction: +class BunkerizeAction(Action): """Move card from bunker to field""" card: board.Card bunker_id: int field_id: int to_bunker: bool - _before_state: int = 0 - _after_state: int = 0 def _move_from_bunker(self, action_board: board.Board) -> None: assert action_board.bunker[self.bunker_id] == self.card @@ -70,14 +86,14 @@ class BunkerizeAction: action_board.bunker[self.bunker_id] = self.card action_board.field[self.field_id].pop() - def apply(self, action_board: board.Board) -> None: + 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: + def _undo(self, action_board: board.Board) -> None: """Undo action""" if self.to_bunker: self._move_from_bunker(action_board) @@ -86,14 +102,12 @@ class BunkerizeAction: @dataclass -class MoveAction: +class MoveAction(Action): """Moving a card from one field stack to another""" cards: List[board.Card] source_id: int destination_id: int - _before_state: int = 0 - _after_state: int = 0 def _shift( self, @@ -102,21 +116,18 @@ class MoveAction: dest: int) -> None: """Shift a card from the field id 'source' to field id 'dest'""" - print(f"Incoming shift from {source} to {dest}") - print( - * - list(itertools.zip_longest( - action_board.field[source][::-1], - self.cards[::-1]))[::-1], - sep="\n") - print("--- GO ---") - for stack_offset, card in enumerate( self.cards, start=-len(self.cards)): assert action_board.field[source][stack_offset] == card - if action_board.field[dest]: - dest_card = action_board.field[dest][-1] + 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): @@ -127,49 +138,22 @@ class MoveAction: raise AssertionError() if dest_card.number != self.cards[0].number + 1: raise AssertionError() - - print( - * - itertools.zip_longest( - action_board.field[source], - action_board.field[dest]), - sep="\n") - action_board.field[source] = action_board.field[ - source][:-len(self.cards)] - action_board.field[dest].extend(self.cards) - print() - print( - * - itertools.zip_longest( - action_board.field[source], - action_board.field[dest]), - sep="\n") - print() - print() - - def apply(self, action_board: board.Board) -> None: - """Do action""" - self._before_state = action_board.state_identifier self._shift(action_board, self.source_id, self.destination_id) - self._after_state = action_board.state_identifier - def undo(self, action_board: board.Board) -> None: + def _undo(self, action_board: board.Board) -> None: """Undo action""" - print("Undo shift") - assert action_board.state_identifier == self._after_state self._shift(action_board, self.destination_id, self.source_id) - assert action_board.state_identifier == self._before_state @dataclass -class DragonKillAction: +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: + def _apply(self, action_board: board.Board) -> None: """Do action""" assert ( action_board.bunker[self.destination_bunker_id] is None @@ -188,7 +172,7 @@ class DragonKillAction: 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: + def _undo(self, action_board: board.Board) -> None: """Undo action""" assert action_board.bunker[self.destination_bunker_id] == ( self.dragon, 4) @@ -204,12 +188,12 @@ class DragonKillAction: @dataclass -class HuaKillAction: +class HuaKillAction(Action): """Remove the flower card""" source_field_id: int - def apply(self, action_board: board.Board) -> None: + 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] @@ -217,12 +201,8 @@ class HuaKillAction: action_board.field[self.source_field_id].pop() action_board.flower_gone = True - def undo(self, action_board: board.Board) -> None: + 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 - - -Action = Union[MoveAction, DragonKillAction, - HuaKillAction, BunkerizeAction, GoalAction] diff --git a/shenzhen_solitaire/board_possibilities.py b/shenzhen_solitaire/board_possibilities.py index f9c791b..79c523d 100644 --- a/shenzhen_solitaire/board_possibilities.py +++ b/shenzhen_solitaire/board_possibilities.py @@ -142,7 +142,6 @@ def _can_stack(bottom: board.Card, top: board.Card) -> bool: return False if bottom.number != top.number + 1: return False - print(f"{bottom.number} and {top.number} can stack") return True @@ -160,8 +159,6 @@ def _get_cardstacks(search_board: board.Board) -> List[List[board.Card]]: if not isinstance(card, board.NumberCard): break result[-1].insert(0, card) - print(f"Cardstack {search_board.state_identifier}:") - print(*result, sep='\n') return result @@ -170,20 +167,25 @@ def possible_field_move_actions( ) -> Iterator[board_actions.MoveAction]: """Enumerate all possible move actions from one field stack to another field stack""" + first_empty_field_id = -1 my_id = search_board.state_identifier for index, stack in enumerate(_get_cardstacks(search_board)): if not stack: continue - print(f"{index}: {stack}") # TODO: sort all substacks by length for substack in (stack[i:] for i in range(len(stack))): for other_index, other_stack in enumerate(search_board.field): - print(f"{index} - {other_index}: \n\t{substack} \n\t{other_stack}") if index == other_index: continue if other_stack: if not _can_stack(other_stack[-1], substack[0]): continue + elif first_empty_field_id == -1: + first_empty_field_id = other_index + elif other_index != first_empty_field_id: + continue + elif len(substack) == len(search_board.field[index]): + continue assert search_board.state_identifier == my_id yield board_actions.MoveAction( cards=substack, source_id=index, destination_id=other_index diff --git a/shenzhen_solitaire/solver.py b/shenzhen_solitaire/solver.py index 8b0ef65..3f78ffa 100644 --- a/shenzhen_solitaire/solver.py +++ b/shenzhen_solitaire/solver.py @@ -9,16 +9,19 @@ class ActionStack: iterator_stack: List[Iterator[board_actions.Action]] action_stack: List[Optional[board_actions.Action]] index_stack: List[int] + state_stack: List[int] def __init__(self) -> None: self.iterator_stack = [] self.index_stack = [] self.action_stack = [] + self.state_stack = [] def push(self, board: Board) -> None: self.iterator_stack.append(possible_actions(board)) self.action_stack.append(None) self.index_stack.append(0) + self.state_stack.append(board.state_identifier) def get(self) -> Optional[board_actions.Action]: try: @@ -32,6 +35,7 @@ class ActionStack: self.action_stack.pop() self.iterator_stack.pop() self.index_stack.pop() + self.state_stack.pop() def __len__(self) -> int: return len(self.index_stack) @@ -51,27 +55,29 @@ class SolitaireSolver: self.stack.push(self.search_board) def solve(self) -> List[board_actions.Action]: - applied = True + max_goal = 0 action = None while self.stack: - last_action = action + assert (self.search_board.state_identifier == + self.stack.state_stack[-1]) action = self.stack.get() if not action: - if last_action and applied: - last_action.undo(self.search_board) - assert (self.search_board.state_identifier - in self.state_set) self.stack.pop() + self.stack.action_stack[-1].undo(self.search_board) + assert (self.search_board.state_identifier + in self.state_set) continue action.apply(self.search_board) - print(f"Applying {str(action)}") - applied = True + if self.search_board.solved(): + return self.stack.action_stack if self.search_board.state_identifier in self.state_set: action.undo(self.search_board) - applied = False assert self.search_board.state_identifier in self.state_set continue self.state_set.add(self.search_board.state_identifier) self.stack.push(self.search_board) + if sum(self.search_board.goal.values()) > max_goal: + max_goal = sum(self.search_board.goal.values()) + print(self.search_board.goal) return self.stack.action_stack diff --git a/test/boards.py b/test/boards.py new file mode 100644 index 0000000..53d6f85 --- /dev/null +++ b/test/boards.py @@ -0,0 +1,67 @@ +from .context import shenzhen_solitaire +from shenzhen_solitaire.board import NumberCard, SpecialCard, Board + +my_board: Board = Board() +my_board.field[0] = [ + SpecialCard.Fa, + NumberCard(NumberCard.Suit.Black, 8), + SpecialCard.Bai, + NumberCard(NumberCard.Suit.Black, 7), + SpecialCard.Zhong, +] + +my_board.field[1] = [ + NumberCard(NumberCard.Suit.Red, 9), + SpecialCard.Zhong, + SpecialCard.Zhong, + NumberCard(NumberCard.Suit.Black, 4), + NumberCard(NumberCard.Suit.Black, 3), +] + +my_board.field[2] = [ + SpecialCard.Hua, + NumberCard(NumberCard.Suit.Red, 1), + NumberCard(NumberCard.Suit.Red, 4), + NumberCard(NumberCard.Suit.Green, 1), + NumberCard(NumberCard.Suit.Red, 6), +] + +my_board.field[3] = [ + SpecialCard.Bai, + SpecialCard.Zhong, + NumberCard(NumberCard.Suit.Red, 3), + NumberCard(NumberCard.Suit.Red, 7), + NumberCard(NumberCard.Suit.Green, 6), +] + +my_board.field[4] = [ + NumberCard(NumberCard.Suit.Green, 7), + NumberCard(NumberCard.Suit.Green, 4), + NumberCard(NumberCard.Suit.Red, 5), + NumberCard(NumberCard.Suit.Green, 5), + NumberCard(NumberCard.Suit.Black, 6), +] + +my_board.field[5] = [ + NumberCard(NumberCard.Suit.Green, 3), + SpecialCard.Bai, + SpecialCard.Fa, + NumberCard(NumberCard.Suit.Black, 2), + NumberCard(NumberCard.Suit.Black, 5), +] + +my_board.field[6] = [ + SpecialCard.Fa, + NumberCard(NumberCard.Suit.Green, 9), + NumberCard(NumberCard.Suit.Green, 2), + NumberCard(NumberCard.Suit.Black, 9), + NumberCard(NumberCard.Suit.Red, 8), +] + +my_board.field[7] = [ + SpecialCard.Bai, + NumberCard(NumberCard.Suit.Red, 2), + SpecialCard.Fa, + NumberCard(NumberCard.Suit.Black, 1), + NumberCard(NumberCard.Suit.Green, 8), +] diff --git a/test/test_solver.py b/test/test_solver.py new file mode 100644 index 0000000..420097f --- /dev/null +++ b/test/test_solver.py @@ -0,0 +1,9 @@ +from .context import shenzhen_solitaire +from shenzhen_solitaire import solver + +from .boards import my_board + +A = solver.SolitaireSolver(my_board) +B = A.solve() +print(*B, sep='\n') +print(len(B))