diff --git a/board.py b/board.py index 1526ef8..461bd4d 100644 --- a/board.py +++ b/board.py @@ -1,11 +1,14 @@ """Contains board class""" import enum -from typing import Union, List, Dict, Optional, NewType +from typing import Union, List, Dict, Optional, Set, Tuple +import dataclasses from dataclasses import dataclass +import itertools class SpecialCard(enum.Enum): """Different types of special cards""" + Zhong = enum.auto() Bai = enum.auto() Fa = enum.auto() @@ -15,11 +18,14 @@ class SpecialCard(enum.Enum): @dataclass(frozen=True) class NumberCard: """Different number cards""" + class Suit(enum.Enum): """Different colors number cards can have""" + Red = enum.auto() Green = enum.auto() Black = enum.auto() + suit: Suit number: int @@ -29,19 +35,65 @@ Card = Union[NumberCard, SpecialCard] class Position(enum.Enum): """Possible Board positions""" + Field = enum.auto() Bunker = enum.auto() Goal = enum.auto() -KilledDragon = NewType('KilledDragon', SpecialCard) - -@dataclass class Board: """Solitaire board""" - field: List[List[Card]] = [[]] * 8 - bunker: List[Union[KilledDragon, Optional[Card]]] = [None] * 3 - goal: Dict[NumberCard.Suit, int] = {NumberCard.Suit.Red: 0, - NumberCard.Suit.Green: 0, - NumberCard.Suit.Black: 0} + + def __init__(self) -> None: + self.field: List[List[Card]] = [[]] * 8 + self.bunker: List[Union[Tuple[SpecialCard, int], Optional[Card]]] = [None] * 3 + self.goal: Dict[NumberCard.Suit, int] = { + NumberCard.Suit.Red: 0, + NumberCard.Suit.Green: 0, + NumberCard.Suit.Black: 0, + } + flowerGone: bool = False + + def check_correct(self) -> bool: + """Returns true, if the board is in a valid state""" + number_cards: Dict[NumberCard.Suit, Set[int]] = { + NumberCard.Suit.Red: set(), + NumberCard.Suit.Green: set(), + NumberCard.Suit.Black: set(), + } + special_cards: Dict[SpecialCard, int] = { + SpecialCard.Zhong: 0, + SpecialCard.Bai: 0, + SpecialCard.Fa: 0, + SpecialCard.Hua: 0, + } + + if self.flowerGone: + special_cards[SpecialCard.Hua] += 1 + + for card in itertools.chain( + self.bunker, + itertools.chain.from_iterable(stack for stack in self.field if stack), + ): + if isinstance(card, tuple): + special_cards[card[0]] += 4 # pylint: disable=E1136 + elif isinstance(card, SpecialCard): + special_cards[card] += 1 + elif isinstance(card, NumberCard): + if card.number in number_cards[card.suit]: + return False + number_cards[card.suit].add(card.number) + + for _, numbers in number_cards.items(): + if set(range(1, 10)) != numbers: + return False + + for cardtype, count in special_cards.items(): + if cardtype == SpecialCard.Hua: + if count != 1: + return False + else: + if count != 4: + return False + return True diff --git a/board_actions.py b/board_actions.py index 7005b29..e8d6eb7 100644 --- a/board_actions.py +++ b/board_actions.py @@ -7,6 +7,7 @@ import board @dataclass class GoalAction: """Move card from field to goal""" + card: board.NumberCard source_id: int source_position: board.Position @@ -40,49 +41,89 @@ class GoalAction: @dataclass -class RestoreAction: +class BunkerizeAction: """Move card from bunker to field""" + card: board.Card source_id: int destination_id: int + to_bunker: bool + + 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) + + 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() def apply(self, action_board: board.Board) -> None: """Do action""" - assert action_board.bunker[self.source_id] == self.card - action_board.bunker[self.source_id] = None + if self.to_bunker: + self._move_to_bunker(action_board) + else: + self._move_from_bunker(action_board) - - -@dataclass -class StoreAction: - """Move card from field to bunker""" - card: board.Card - source_id: int - destination_id: int + 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: """Moving a card from one field stack to another""" - card: board.Card + + cards: List[board.Card] source_id: int destination_id: 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 + + if action_board.field[dest]: + dest_card = action_board.field[dest] + if not isinstance(dest_card, board.NumberCard): + raise AssertionError() + if dest_card.suit != self.cards[0].suit: + raise AssertionError() + if dest_card.number + 1 == self.cards[0].number: + raise AssertionError() + + 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""" + 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: """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 ( + 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: @@ -94,13 +135,11 @@ class DragonKillAction: action_board.bunker[index] = None else: raise RuntimeError("Can only kill dragons in field and bunker") - action_board.bunker[self.destination_bunker_id] = board.KilledDragon( - self.dragon) + 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] == board.KilledDragon( - self.dragon) + assert action_board.bunker[self.destination_bunker_id] == (self.dragon, 4) assert len(self.source_stacks) == 4 for position, index in self.source_stacks: if position == board.Position.Field: @@ -115,6 +154,7 @@ class DragonKillAction: @dataclass class HuaKillAction: """Remove the flower card""" + source_field_id: int def apply(self, action_board: board.Board) -> None: @@ -131,5 +171,4 @@ class HuaKillAction: action_board.flowerGone = False -Action = Union[MoveAction, DragonKillAction, - HuaKillAction, StoreAction, RestoreAction, GoalAction] +Action = Union[MoveAction, DragonKillAction, HuaKillAction, BunkerizeAction, GoalAction] diff --git a/board_possibilities.py b/board_possibilities.py index ceb3bdb..e4f4a9a 100644 --- a/board_possibilities.py +++ b/board_possibilities.py @@ -4,7 +4,9 @@ import board import board_actions -def possible_huakill_action(search_board: board.Board) -> Iterator[board_actions.HuaKillAction]: +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[-1] == board.SpecialCard.Hua: @@ -12,10 +14,14 @@ def possible_huakill_action(search_board: board.Board) -> Iterator[board_actions def possible_dragonkill_actions( - search_board: board.Board) -> Iterator[board_actions.DragonKillAction]: + search_board: board.Board +) -> Iterator[board_actions.DragonKillAction]: """Enumerate all possible dragon kills""" - possible_dragons = [board.SpecialCard.Zhong, - board.SpecialCard.Fa, board.SpecialCard.Bai] + 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: @@ -24,10 +30,10 @@ def possible_dragonkill_actions( 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] + 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 @@ -35,20 +41,24 @@ def possible_dragonkill_actions( destination_bunker_id = bunker_dragons[0] else: destination_bunker_id = [ - i for i, x in enumerate(search_board.bunker) if x is None][0] + 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]) + 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) + 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.StoreAction]: +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] + open_bunker_list = [i for i, x in enumerate(search_board.bunker) if x is None] if not open_bunker_list: return @@ -57,16 +67,20 @@ def possible_bunkerize_actions(search_board: board.Board) -> Iterator[board_acti for index, stack in enumerate(search_board.field): if not stack: continue - yield board_actions.StoreAction(card=stack[-1], - source_id=index, - destination_id=open_bunker) + yield board_actions.BunkerizeAction( + card=stack[-1], source_id=index, destination_id=open_bunker, to_bunker=True + ) def possible_debunkerize_actions( - search_board: board.Board) -> Iterator[board_actions.RestoreAction]: + 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)] + 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: @@ -77,29 +91,41 @@ def possible_debunkerize_actions( continue if other_stack[-1].number != card.number + 1: continue - yield board_actions.RestoreAction(card=card, - source_id=index, - destination_id=other_index) + yield board_actions.BunkerizeAction( + card=card, source_id=index, destination_id=other_index, to_bunker=False + ) -def possible_goal_move_actions(search_board: board.Board) -> Iterator[board_actions.GoalAction]: +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 if isinstance(stack[-1], board.NumberCard)] - bunker_cards = [(board.Position.Bunker, index, stack) - for index, stack in enumerate(search_board.bunker) - if isinstance(stack, board.NumberCard)] + field_cards = [ + (board.Position.Field, index, stack[-1]) + for index, stack in enumerate(search_board.field) + if stack + if isinstance(stack[-1], board.NumberCard) + ] + bunker_cards = [ + (board.Position.Bunker, index, stack) + for index, stack in enumerate(search_board.bunker) + if isinstance(stack, board.NumberCard) + ] top_cards = field_cards + bunker_cards for suit, number in search_board.goal.items(): for source, index, stack in top_cards: if not (stack.suit == suit and stack.number == number + 1): continue - yield board_actions.GoalAction(card=stack, source_id=index, source_position=source) + yield board_actions.GoalAction( + card=stack, source_id=index, source_position=source + ) break -def possible_field_move_actions(search_board: board.Board) -> Iterator[board_actions.MoveAction]: +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""" for index, stack in enumerate(search_board.field): if not stack: @@ -107,17 +133,16 @@ def possible_field_move_actions(search_board: board.Board) -> Iterator[board_act if not isinstance(stack[-1], board.NumberCard): continue 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 == stack[-1].suit: - continue - if other_stack[-1].number != stack[-1].number + 1: - continue - yield board_actions.MoveAction(card=stack[-1], - source_id=index, - destination_id=other_index) + if other_stack: + if not isinstance(other_stack[-1], board.NumberCard): + continue + if other_stack[-1].suit == stack[-1].suit: + continue + if other_stack[-1].number != stack[-1].number + 1: + continue + yield board_actions.MoveAction( + cards=[stack[-1]], source_id=index, destination_id=other_index + ) def possible_actions(search_board: board.Board) -> Iterator[board_actions.Action]: diff --git a/main.py b/main.py index 92d56cf..c09256e 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,86 @@ """Main module""" from typing import List, Tuple -import board +from board import Board, NumberCard, SpecialCard +import board_possibilities import board_actions class SolitaireSolver: """Solver for Shenzhen Solitaire""" - search_board: board.Board + + search_board: Board stack: List[Tuple[board_actions.Action, int]] + + +def main() -> None: + t: Board = Board() + t.field[0] = [ + SpecialCard.Fa, + NumberCard(NumberCard.Suit.Black, 8), + SpecialCard.Bai, + NumberCard(NumberCard.Suit.Black, 7), + SpecialCard.Zhong, + ] + + t.field[1] = [ + NumberCard(NumberCard.Suit.Red, 9), + SpecialCard.Zhong, + SpecialCard.Zhong, + NumberCard(NumberCard.Suit.Black, 4), + NumberCard(NumberCard.Suit.Black, 3), + ] + + t.field[2] = [ + SpecialCard.Hua, + NumberCard(NumberCard.Suit.Red, 1), + NumberCard(NumberCard.Suit.Red, 4), + NumberCard(NumberCard.Suit.Green, 1), + NumberCard(NumberCard.Suit.Red, 6), + ] + + t.field[3] = [ + SpecialCard.Zhong, + SpecialCard.Bai, + NumberCard(NumberCard.Suit.Red, 3), + NumberCard(NumberCard.Suit.Red, 7), + NumberCard(NumberCard.Suit.Green, 6), + ] + + t.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), + ] + + t.field[5] = [ + NumberCard(NumberCard.Suit.Green, 3), + SpecialCard.Bai, + SpecialCard.Fa, + NumberCard(NumberCard.Suit.Black, 2), + NumberCard(NumberCard.Suit.Black, 5), + ] + + t.field[6] = [ + SpecialCard.Fa, + NumberCard(NumberCard.Suit.Green, 9), + NumberCard(NumberCard.Suit.Green, 2), + NumberCard(NumberCard.Suit.Black, 9), + NumberCard(NumberCard.Suit.Red, 8), + ] + + t.field[7] = [ + SpecialCard.Bai, + NumberCard(NumberCard.Suit.Red, 2), + SpecialCard.Fa, + NumberCard(NumberCard.Suit.Black, 1), + NumberCard(NumberCard.Suit.Green, 8), + ] + + print(t.check_correct()) + print(*list(board_possibilities.possible_actions(t)), sep='\n') + + +if __name__ == "__main__": + main()