Board manager

BoardState

class BoardState(pushers_positions: ~typing.List[int] = <factory>, boxes_positions: ~typing.List[int] = <factory>, zobrist_hash: int = 0)

Sample of board state.

NO_HASH: ClassVar[int] = 0

Integer used for situations where board hash has not been calculated.

boxes_positions: List[int]

Positions of boxes sorted by box ID.

pushers_positions: List[int]

Positions of pushers sorted by pusher ID.

zobrist_hash: int = 0

Zobrist hash of state.

BoardManager

exception BoxGoalSwitchError
exception CellAlreadyOccupiedError
class BoardManager(board: BoardGraph, boxorder: str = '', goalorder: str = '')

Memoizes, tracks and updates positions of all pieces.

  • assigns and maintains piece IDs

  • manages Sokoban+ piece IDs

  • moves pieces while preserving their IDs

  • checks if board is solved

BoardManager implements efficient means to inspect positions of pushers, boxes and goals. To be able to do that, pieces must be uniquely identified. BoardManager assigns unique numerical ID to each individual piece. This ID can then be used to refer to that piece in various contexts.

How are piece IDs assigned? Start scanning game board from top left corner, going row by row, from left to the right. First encountered box will get box.id = Config.DEFAULT_ID, second one box.id = Config.DEFAULT_ID + 1, etc… Same goes for pushers and goals.

Assigning board elements' IDs

BoardManager also ensures that piece IDs remain unchanged when pieces are moved on board. This is best illustrated by example. Let’s construct a board with 2 boxes.

>>> from sokoenginepy.game import BoardGraph, BoardManager, Config
>>> from sokoenginepy.io import SokobanPuzzle
>>> data = "\n".join([
...     "######",
...     "#    #",
...     "# $  #",
...     "#  $.#",
...     "#@ . #",
...     "######",
... ])
>>> puzzle = SokobanPuzzle(board=data)
>>> board = BoardGraph(puzzle)
>>> manager = BoardManager(board)
>>> manager.boxes_positions
{1: 14, 2: 21}

We can edit the board (simulating movement of box ID 2) directly, without using the manager. If we attach manager to that board after edit, we get expected but wrong ID assigned to the box we’d just “moved”:

>>> board[21] = " "
>>> board[9] = "$"
>>> print(board.to_board_str())
######
#  $ #
# $  #
#   .#
#@ . #
######
>>> manager = BoardManager(board)
>>> manager.boxes_positions
{1: 9, 2: 14}

Moving box through manager (via BoardManager.move_box_from) would’ve preserved ID of moved box. Same goes for pushers.

Initial board

Box edited without manager

Box moved through manager

img1

img2

img3

Note

Movement methods in BoardManager only implement board updates. They don’t implement full game logic. For game logic see Mover

Parameters
  • board – board to mange

  • boxorder – Sokoban+ data (see SokobanPlus)

  • goalorder – Sokoban+ data (see SokobanPlus)

See:
box_id_on(position: int) int
Raises

KeyError – No box on position

box_plus_id(box_id: int) int
box_position(box_id: int) int
Raises

KeyError – No box with ID box_id

disable_sokoban_plus()

Disables using Sokoban+ rules for this board.

See also

SokobanPlus

enable_sokoban_plus()

Enables using Sokoban+ rules for this board.

Enabling these, changes victory condition for given board (return value of is_solved).

See also

SokobanPlus

goal_id_on(position: int) int

ID of goal on position.

Raises

KeyError – No goal on position

goal_plus_id(goal_id: int) int
goal_position(goal_id: int) int
Raises

KeyError – No goal with ID goal_id

has_box(box_id: int) bool
has_box_on(position: int) bool
has_goal(goal_id: int) bool
has_goal_on(position: int) bool
has_pusher(pusher_id: int) bool
has_pusher_on(position: int) bool
move_box(box_id: int, to_new_position: int)

Updates board state and board cells with changed box position.

Raises
move_box_from(old_position: int, to_new_position: int)

Updates board state and board cells with changed box position.

Raises
move_pusher(pusher_id: int, to_new_position: int)

Updates board state and board cells with changed pusher position.

Raises

Note

Allows placing a pusher onto position occupied by box. This is for cases when we switch box/goals positions in reverse solving mode. In this situation it is legal for pusher to end up standing on top of the box. Game rules say that for these situations, first move(s) must be jumps.

move_pusher_from(old_position: int, to_new_position: int)

Updates board state and board cells with changed pusher position.

Raises
pusher_id_on(position: int) int
Raises

KeyError – No pusher on position

pusher_position(pusher_id: int) int
Raises

KeyError – No pusher with ID pusher_id

solutions() Iterable[BoardState]

Generator for all configurations of boxes that result in solved board.

Note

Result set depends on is_sokoban_plus_enabled.

switch_boxes_and_goals()

Switches positions of boxes and goals pairs. This is used by Mover in SolvingMode.REVERSE.

Raises

BoxGoalSwitchError – when board can’t be switched. These kinds of boards are usually also not is_playable.

property board: BoardGraph
property boxes_count: int
property boxes_ids: List[int]

IDs of all boxes on board.

property boxes_positions: Dict[int, int]

Mapping of boxes’ IDs to the corresponding board positions, ie.

{1: 42, 2: 24}
property boxorder: str

See Also: SokobanPlus.boxorder

property goalorder: str

See Also: SokobanPlus.goalorder

property goals_count: int
property goals_ids: List[int]

IDs of all goals on board.

property goals_positions: Dict[int, int]

Mapping of boxes’ IDs to the corresponding board positions, ie.

{1: 42, 2: 24}
property is_playable: bool

Checks minimal requirement for board to be playable.

property is_sokoban_plus_enabled: bool

Are Sokoban+ rule enabled for current game?

See also

  • enable_sokoban_plus

property is_sokoban_plus_valid: bool

Validates current set of Sokoban+ rules.

See also

SokobanPlus

property is_solved: bool

Checks for game victory.

  1. Classic victory is any board position in which each box is positioned on top of each goal

  2. Sokoban+ victory is board position where each box is positioned on top of each goal with the same Sokoban+ ID as that box

Result depends on is_sokoban_plus_enabled.

property pushers_count: int
property pushers_ids: List[int]

IDs of all pushers on board.

property pushers_positions: Dict[int, int]

Mapping of pushers’ IDs to the corresponding board positions, ie.

{1: 42, 2: 24}
property state: BoardState

Snapshots current board state.

property walls_positions: List[int]

HashedBoardManager

class HashedBoardManager(board: BoardGraph, boxorder: str = '', goalorder: str = '')

Bases: BoardManager

Board manager that also manages Zobrist hashing.

Adds Zobrist hashing on top of BoardManager and keeps it up to date when pieces are moved.

Zobrist hash is 64b integer hash derived from positions of all boxes and pushers on board.

This hash is “resistant” to moving board pieces. Moving one pusher and box will change board hash. Undoing that move will return hash to previous value.

This kind of hash is reliable way for identifying board positions. Sokoban solvers might need this to operate.

When hashing, boxes with same Sokoban+ ID are treated as equal meaning that if two of these boxes switch position, hash will not change. This also means that for the same board, hash is different when Sokoban+ is enabled from the one when it is disabled.

Pushers are all treated equal, meaning that if two pushers switch position, hash will not change

Notes

  • enabling/disabling Sokoban+ rehashes the board state

  • moving pieces doesn’t need to re-hash whole board, it updates hash incrementally

  • undoing piece movement also updates hash incrementally with additional feature that returning to previous board state will return to previous hash value

disable_sokoban_plus()

Disables using Sokoban+ rules for this board.

See also

SokobanPlus

enable_sokoban_plus()

Enables using Sokoban+ rules for this board.

Enabling these, changes victory condition for given board (return value of is_solved).

See also

SokobanPlus

external_state_hash(board_state) int

Calculates Zobrist hash of given board_state as if that board_state was applied to initial board (to board where no movement happened).

board_state must meet following requirement:

len(board_state.boxes_positions) == self.boxes_count and len(board_state.boxes_positions) == self.goals_count

Returns

Value of hash or BoardState.NO_HASH if it can’t be calculated

property boxorder: str

See Also: SokobanPlus.boxorder

property goalorder: str

See Also: SokobanPlus.goalorder

property initial_state_hash: int

Zobrist hash of initial board state (before any movement happened).

property is_solved: bool

Checks for game victory.

  1. Classic victory is any board position in which each box is positioned on top of each goal

  2. Sokoban+ victory is board position where each box is positioned on top of each goal with the same Sokoban+ ID as that box

Result depends on is_sokoban_plus_enabled.

property solutions_hashes: Set[int]
property state: BoardState

Snapshots current board state.

property state_hash: int

Zobrist hash of current board state.

SokobanPlus

exception SokobanPlusDataError
class SokobanPlus(pieces_count: int, boxorder: str = '', goalorder: str = '')

Manages Sokoban+ data for game board.

Sokoban+ rules

In this variant of game rules, each box and each goal on board get number tag (color). Game objective changes slightly: board is considered solved only when each goal is filled with box of the same tag. So, for example goal tagged with number 9 must be filled with any box tagged with number 9.

Multiple boxes and goals may share same plus id, but the number of boxes with one plus id must be equal to number of goals with that same plus id. There is also default plus id that represents non tagged boxes and goals.

Sokoban+ ids for given board are defined by two strings called goalorder and boxorder. For example, boxorder “13 24 3 122 1” would give plus_id = 13 to box id = 1, plus_id = 24 to box ID = 2, etc…

Valid Sokoban+ id sequences

Boxorder and goalorder must define ids for equal number of boxes and goals. This means that in case of boxorder assigning plus id “42” to two boxes, goalorder must also contain number 42 twice.

Sokoban+ data parser accepts any positive integer as plus id.

Parameters
  • boxorder – Space separated integers describing Sokoban+ IDs for boxes

  • goalorder – Space separated integers describing Sokoban+ IDs for goals

  • pieces_count – Total count of boxes/goals on board

box_plus_id(for_box_id: int) int

Get Sokoban+ ID for box.

Returns

If Sokoban+ is enabled returns Sokoban+ ID of a box. If not, returns DEFAULT_PLUS_ID

Raises

KeyError – No box with ID for_box_id, but only if i Sokoban+ is enabled

goal_plus_id(for_goal_id: int) int

Get Sokoban+ ID for goal.

Returns

If Sokoban+ is enabled returns Sokoban+ ID of a goal. If not, returns DEFAULT_PLUS_ID

Raises

KeyError – No goal with ID for_goal_id, but only if Sokoban+ is enabled

classmethod is_valid_plus_id(plus_id: int) bool
DEFAULT_PLUS_ID: Final[int] = 0

Sokoban+ ID for pieces that don’t have one or when Sokoban+ is disabled.

Original Sokoban+ implementation used number 99 for default plus ID. As there can be more than 99 boxes on board, sokoenginepy changes this detail and uses DEFAULT_PLUS_ID as default plus ID. When loading older puzzles with Sokoban+, legacy default value is converted transparently.

LEGACY_DEFAULT_PLUS_ID: Final[int] = 99
property boxorder: str
property errors: List[str]
property goalorder: str
property is_enabled: bool

Raises: SokobanPlusDataError: Trying to enable invalid Sokoban+

property is_valid: bool
property is_validated
property pieces_count: int