From af2565548e837f528e876c66e91839b949caf80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20W=C3=B6lfer?= Date: Fri, 19 Apr 2019 23:58:07 +0200 Subject: [PATCH] Worked a bit --- shenzhen_solitaire/board.py | 14 ++++- shenzhen_solitaire/board_actions.py | 59 ++++++++++++++++--- shenzhen_solitaire/board_possibilities.py | 18 ++++-- shenzhen_solitaire/solver.py | 70 +++++++++++++++++++++- test/context.py | 2 + test/test_chain.py | 71 ++--------------------- 6 files changed, 151 insertions(+), 83 deletions(-) diff --git a/shenzhen_solitaire/board.py b/shenzhen_solitaire/board.py index 8b0c8cc..1846d05 100644 --- a/shenzhen_solitaire/board.py +++ b/shenzhen_solitaire/board.py @@ -62,6 +62,18 @@ class Board: } self.flower_gone: bool = False + def solved(self) -> bool: + """Returns true if the board is solved""" + if any(x != 9 for x in self.goal.values()): + return False + if any(not isinstance(x, tuple) for x in self.bunker): + return False + if not self.flower_gone: + return False + assert all(not x for x in self.field) + return True + + @property def state_identifier(self) -> int: """Returns a unique identifier to represent the board state""" result: int = 0 @@ -98,7 +110,7 @@ class Board: result <<= 5 result |= field_card.identifier() - return 0 + return result def check_correct(self) -> bool: """Returns true, if the board is in a valid state""" diff --git a/shenzhen_solitaire/board_actions.py b/shenzhen_solitaire/board_actions.py index ed20c16..b76d93b 100644 --- a/shenzhen_solitaire/board_actions.py +++ b/shenzhen_solitaire/board_actions.py @@ -1,9 +1,17 @@ """Contains actions that can be used on the board""" from typing import List, Tuple, Union from dataclasses import dataclass +import itertools from . import board +class Action: + def apply(self, action_board: board.Board) -> None: + pass + def undo(self, action_board: board.Board) -> None: + pass + + @dataclass class GoalAction: """Move card from field to goal""" @@ -45,20 +53,22 @@ class BunkerizeAction: """Move card from bunker to field""" card: board.Card - source_id: int - destination_id: int + 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.source_id] == self.card - action_board.bunker[self.source_id] = None - action_board.field[self.destination_id].append(self.card) + 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.source_id][-1] == self.card - assert action_board.bunker[self.destination_id] is None - action_board.bunker[self.destination_id] = self.card - action_board.field[self.source_id].pop() + 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""" @@ -82,6 +92,8 @@ class MoveAction: cards: List[board.Card] source_id: int destination_id: int + _before_state: int = 0 + _after_state: int = 0 def _shift( self, @@ -90,6 +102,15 @@ 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 @@ -107,17 +128,37 @@ class MoveAction: 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: """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 diff --git a/shenzhen_solitaire/board_possibilities.py b/shenzhen_solitaire/board_possibilities.py index 31af1fe..f9c791b 100644 --- a/shenzhen_solitaire/board_possibilities.py +++ b/shenzhen_solitaire/board_possibilities.py @@ -73,8 +73,8 @@ def possible_bunkerize_actions( continue yield board_actions.BunkerizeAction( card=stack[-1], - source_id=index, - destination_id=open_bunker, + field_id=index, + bunker_id=open_bunker, to_bunker=True ) @@ -100,8 +100,8 @@ def possible_debunkerize_actions( continue yield board_actions.BunkerizeAction( card=card, - source_id=index, - destination_id=other_index, + bunker_id=index, + field_id=other_index, to_bunker=False ) @@ -142,6 +142,7 @@ 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 @@ -159,6 +160,8 @@ 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 @@ -167,18 +170,25 @@ def possible_field_move_actions( ) -> Iterator[board_actions.MoveAction]: """Enumerate all possible move actions from one field stack to another field stack""" + 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 + assert search_board.state_identifier == my_id yield board_actions.MoveAction( cards=substack, source_id=index, destination_id=other_index ) + assert search_board.state_identifier == my_id def possible_actions( diff --git a/shenzhen_solitaire/solver.py b/shenzhen_solitaire/solver.py index 10dcaee..8b0ef65 100644 --- a/shenzhen_solitaire/solver.py +++ b/shenzhen_solitaire/solver.py @@ -1,11 +1,77 @@ """Contains solver for solitaire""" -from typing import List, Tuple +from typing import List, Tuple, Iterator, Set, Optional from .board import Board from . import board_actions +from .board_possibilities import possible_actions + + +class ActionStack: + iterator_stack: List[Iterator[board_actions.Action]] + action_stack: List[Optional[board_actions.Action]] + index_stack: List[int] + + def __init__(self) -> None: + self.iterator_stack = [] + self.index_stack = [] + self.action_stack = [] + + def push(self, board: Board) -> None: + self.iterator_stack.append(possible_actions(board)) + self.action_stack.append(None) + self.index_stack.append(0) + + def get(self) -> Optional[board_actions.Action]: + try: + self.action_stack[-1] = next(self.iterator_stack[-1]) + except StopIteration: + return None + self.index_stack[-1] += 1 + return self.action_stack[-1] + + def pop(self) -> None: + self.action_stack.pop() + self.iterator_stack.pop() + self.index_stack.pop() + + def __len__(self) -> int: + return len(self.index_stack) class SolitaireSolver: """Solver for Shenzhen Solitaire""" search_board: Board - stack: List[Tuple[board_actions.Action, int]] + stack: ActionStack + state_set: Set[int] + + def __init__(self, board: Board) -> None: + self.search_board = board + self.state_set = {board.state_identifier} + self.stack = ActionStack() + self.stack.push(self.search_board) + + def solve(self) -> List[board_actions.Action]: + applied = True + action = None + while self.stack: + last_action = action + 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() + continue + action.apply(self.search_board) + print(f"Applying {str(action)}") + applied = True + 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) + + return self.stack.action_stack diff --git a/test/context.py b/test/context.py index 90f0c95..98fe044 100644 --- a/test/context.py +++ b/test/context.py @@ -3,3 +3,5 @@ import os import sys sys.path.insert(0, os.path.abspath( os.path.join(os.path.dirname(__file__), '..'))) + +import shenzhen_solitaire diff --git a/test/test_chain.py b/test/test_chain.py index f072e31..b012e8e 100644 --- a/test/test_chain.py +++ b/test/test_chain.py @@ -1,9 +1,10 @@ """Contains tests for chain module""" import unittest -from . import context # pylint: disable=unused-import -from shenzhen_solitaire.board import NumberCard, SpecialCard, Board # pylint: disable=wrong-import-order -from shenzhen_solitaire import board_possibilities # pylint: disable=wrong-import-order +from .context import shenzhen_solitaire +from shenzhen_solitaire.board import NumberCard, SpecialCard, Board +from shenzhen_solitaire import board_possibilities +from .boards import my_board class ChainTestClass(unittest.TestCase): @@ -11,70 +12,6 @@ class ChainTestClass(unittest.TestCase): def test_sequence(self) -> None: """Tests a given sequence. Might break if I change the iterators""" - 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), - ] self.assertTrue(my_board.check_correct()) sequence = [