Increased project structure

This commit is contained in:
Lukas Wölfer
2019-04-10 02:02:58 +02:00
parent 44f47f4691
commit 92e2f7f4a1
10 changed files with 166 additions and 103 deletions

View File

View File

@@ -0,0 +1,98 @@
"""Contains board class"""
import enum
from typing import Union, List, Dict, Optional, Set, Tuple
from dataclasses import dataclass
import itertools
class SpecialCard(enum.Enum):
"""Different types of special cards"""
Zhong = enum.auto()
Bai = enum.auto()
Fa = enum.auto()
Hua = enum.auto()
@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
Card = Union[NumberCard, SpecialCard]
class Position(enum.Enum):
"""Possible Board positions"""
Field = enum.auto()
Bunker = enum.auto()
Goal = enum.auto()
class Board:
"""Solitaire board"""
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

View File

@@ -0,0 +1,176 @@
"""Contains actions that can be used on the board"""
from typing import List, Tuple, Union
from dataclasses import dataclass
from . import board
@dataclass
class GoalAction:
"""Move card from field to goal"""
card: board.NumberCard
source_id: int
source_position: board.Position
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
assert action_board.goal[self.card.suit] + 1 == self.card.number
action_board.field[self.source_id].pop()
action_board.goal[self.card.suit] += 1
elif self.source_position == board.Position.Bunker:
assert action_board.bunker[self.source_id] == self.card
assert action_board.goal[self.card.suit] + 1 == self.card.number
action_board.bunker[self.source_id] = None
action_board.goal[self.card.suit] += 1
else:
raise RuntimeError("Unknown position")
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:
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.goal[self.card.suit] -= 1
@dataclass
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"""
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:
"""Moving a card from one field stack to another"""
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][-1]
if not isinstance(dest_card, board.NumberCard):
raise AssertionError()
if not all(isinstance(x, board.NumberCard) for x in self.cards):
raise AssertionError()
if dest_card.suit == self.cards[0].suit:
raise AssertionError()
if dest_card.number != self.cards[0].number + 1:
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 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
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")
action_board.bunker[self.destination_bunker_id] = None
@dataclass
class HuaKillAction:
"""Remove the flower card"""
source_field_id: int
def apply(self, action_board: board.Board) -> None:
"""Do action"""
assert not action_board.flowerGone
assert action_board.field[self.source_field_id][-1] == board.SpecialCard.Hua
action_board.field[self.source_field_id].pop()
action_board.flowerGone = True
def undo(self, action_board: board.Board) -> None:
"""Undo action"""
assert action_board.flowerGone
action_board.field[self.source_field_id].append(board.SpecialCard.Hua)
action_board.flowerGone = False
Action = Union[MoveAction, DragonKillAction, HuaKillAction, BunkerizeAction, GoalAction]

View File

@@ -0,0 +1,180 @@
"""Contains function to iterate different kinds of possible actions"""
from typing import Iterator, List
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)
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], source_id=index, destination_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, source_id=index, destination_id=other_index, 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
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
)
break
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"""
for index, stack in enumerate(_get_cardstacks(search_board)):
if not stack:
continue
# 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):
if other_stack:
if not _can_stack(other_stack[-1], substack[0]):
continue
yield board_actions.MoveAction(
cards=substack, source_id=index, destination_id=other_index
)
def possible_actions(search_board: board.Board) -> Iterator[board_actions.Action]:
"""Enumerate all possible actions on the current search_board"""
yield from possible_huakill_action(search_board)
yield from possible_dragonkill_actions(search_board)
yield from possible_goal_move_actions(search_board)
yield from possible_debunkerize_actions(search_board)
yield from possible_field_move_actions(search_board)
yield from possible_bunkerize_actions(search_board)

View File

@@ -0,0 +1,11 @@
"""Contains solver for solitaire"""
from typing import List, Tuple
from .board import Board
from . import board_actions
class SolitaireSolver:
"""Solver for Shenzhen Solitaire"""
search_board: Board
stack: List[Tuple[board_actions.Action, int]]