from copy import deepcopy
from .sub_board import SubBoard
from .cell import Cell
from .gameplay import Player, MainBoardCoords, SubBoardCoords
from .gameplay import did_move_win
from .errors import MoveOutsideMainBoardError, MoveNotOnNextBoardError, \
BoardNotFinishedError, MoveInFinishedBoardError, \
MoveInPlayedCellError
[docs]class MainBoard(object):
"""An Ultimate TicTacToe board, containing several SubBoards where players play
When the board size is 3, the main board looks like this:
::
| SubBoard 0,0 | SubBoard 0,1 | SubBoard 0,2 |
| SubBoard 1,0 | SubBoard 1,1 | SubBoard 1,2 |
| SubBoard 2,0 | SubBoard 2,1 | SubBoard 2,2 |
Each SubBoard looks like this:
::
| Cell 0,0 | Cell 0,1 | Cell 0,2 |
| Cell 1,0 | Cell 1,1 | Cell 1,2 |
| Cell 2,0 | Cell 2,1 | Cell 2,2 |
"""
def __init__(self, board_size: int = 3):
if not board_size == 3:
raise ValueError("Size must be 3 (for now)")
self._board_size = board_size
self._board = [
[SubBoard() for board_col in range(board_size)]
for board_row in range(board_size)
]
self._next_player = Player.NONE # type: Player
self._sub_board_next_player_must_play = None # type: MainBoardCoords
self._is_finished = False # type: bool
self._winner = Player.NONE # type: Player
@property
def sub_board_next_player_must_play(self) -> MainBoardCoords:
"""The next board to play on. None if the next move can be on any board"""
return self._sub_board_next_player_must_play
@property
def is_finished(self) -> bool:
"""Whether the board is finished (tied, won or lost)"""
if self._is_finished:
return True
for row in self._board:
for sub_board in row:
if not sub_board.is_finished:
return False
return True
@property
def winner(self) -> Player:
"""The winner of the board if finished. Exception otherwise"""
if not self.is_finished:
raise BoardNotFinishedError
return self._winner
[docs] def add_my_move(self, main_board_coords: MainBoardCoords, sub_board_coords: SubBoardCoords) -> 'MainBoard':
"""Adds your move to the specified sub-board
Args:
main_board_coords: The co-ordinates (row, column) of the SubBoard to play on
move: The move (row, column) to make on the SubBoard
Returns:
A new MainBoard instance with the move applied
"""
return self._add_move(main_board_coords, sub_board_coords, Player.ME)
[docs] def add_opponent_move(self, main_board_coords: MainBoardCoords, sub_board_coords: SubBoardCoords) -> 'MainBoard':
"""Adds the opponent's move to the specified sub-board
Args:
main_board_coords: The co-ordinates (row, column) of the SubBoard to play on
sub_board_coords: The move (row, column) to make on the SubBoard
Returns:
A new MainBoard instance with the move applied
"""
return self._add_move(main_board_coords, sub_board_coords, Player.OPPONENT)
[docs] def get_sub_board(self, main_board_coords: MainBoardCoords) -> SubBoard:
row = main_board_coords.row
col = main_board_coords.col
return self._board[row][col]
[docs] def get_playable_coords(self) -> [MainBoardCoords]:
"""Returns all board co-ordinates that are valid for the next move.
If the opponents previous move co-ordinates (according to the rules)
restrict you to a single sub-board, then this will return only that board.
If not, it will return all boards that are valid for moves.
Returns Empty if board is finished.
Returns:
Array of valid board co-ordinates (Row, Col), e.g. [MainBoardCoords(2, 2),MainBoardCoords(1, 1)]
"""
if self.sub_board_next_player_must_play is not None:
return [self.sub_board_next_player_must_play]
else:
if self.is_finished:
return []
available_boards = []
for row_index in range(0, self._board_size):
for col_index in range(0, self._board_size):
if not self._board[row_index][col_index].is_finished:
available_boards.append(MainBoardCoords(row_index, col_index))
return available_boards
[docs] def is_playing_on_sub_board_allowed(self, main_board_coords: MainBoardCoords):
"""Whether this is a valid board for the next move
Args:
main_board_coords: The co-ordinates (row, column) of the SubBoard to check
"""
if self.sub_board_next_player_must_play is None or \
self.sub_board_next_player_must_play == main_board_coords:
return True
return False
def __str__(self):
"""Returns a pretty printed representation of the main board"""
pretty_printed = ''
# TODO: Shouldn't access sub-board private var
board_size = len(self._board)
board_size_range = range(board_size)
for (mb_idx, mb_row) in enumerate(self._board):
for sub_board_row_num in board_size_range:
for (sb_idx, sub_board) in enumerate(mb_row):
for cell in sub_board._board[sub_board_row_num]:
pretty_printed += str(cell) + ' '
# Print vertical separator - if not last sub_board
if sb_idx < board_size - 1:
pretty_printed += '| '
pretty_printed += '\n'
# Print horizontal separators
# Only if this is not the last row
if mb_idx < board_size - 1:
for (bm_idx, board_marker) in enumerate(board_size_range):
for cell_marker in board_size_range:
pretty_printed += '- '
if bm_idx < board_size - 1:
pretty_printed += '| '
pretty_printed += '\n'
return pretty_printed
# Private functions
def _add_move(self, main_board_coords, sub_board_coords, player) -> 'MainBoard':
"""Adds a move by a ultimate_ttt_player to a deep copy of the current board, returning the copy
Args:
main_board_coords: The location of the sub-board on the main board
Returns:
A new MainBoard instance with the move applied and all properties calculated
"""
if self._is_finished:
raise MoveInFinishedBoardError(main_board_coords, player)
if not self._is_board_in_bounds(main_board_coords):
raise MoveOutsideMainBoardError(main_board_coords)
if not self.is_playing_on_sub_board_allowed(main_board_coords):
raise MoveNotOnNextBoardError(main_board_coords, self._sub_board_next_player_must_play)
return self.copy_applying_move(main_board_coords, sub_board_coords, player)
[docs] def copy_applying_move(self, main_board_coords, sub_board_coords, player) -> 'MainBoard':
# Apply the move to the sub board first to ensure it works
try:
updated_sub_board = self._board[main_board_coords.row][main_board_coords.col] \
.add_move(sub_board_coords, player)
except MoveInPlayedCellError as e:
raise MoveInPlayedCellError(player, sub_board_coords, main_board_coords) from e
# Copy the board so we can update it
# Maybe this should all go in the constructor/classmethod
updated_main_board = deepcopy(self)
updated_main_board._board[main_board_coords.row][main_board_coords.col] = updated_sub_board
# Check that the next board to play is not finished
if not updated_main_board._board[sub_board_coords.row][sub_board_coords.col].is_finished:
updated_main_board._sub_board_next_player_must_play = MainBoardCoords(sub_board_coords.row,
sub_board_coords.col)
else:
updated_main_board._sub_board_next_player_must_play = None
# Convert to board of cells format so we can reuse check logic
cell_board = updated_main_board._as_cell_board()
if did_move_win(cell_board, main_board_coords, player):
updated_main_board._is_finished = True
updated_main_board._winner = player
return updated_main_board
def _as_cell_board(self) -> [[Cell]]:
"""Returns this main board in the form of a board of cells
Each cell represents a sub-board of this board, with
`cell.played_by` set to the ultimate_ttt_player that won the board (Player.NONE if tied)
"""
return [[Cell(sub_board.winner) if sub_board.is_finished else Cell(Player.NONE) for sub_board in row] for row in
self._board]
def _is_board_in_bounds(self, coords) -> bool:
"""Checks whether the given move is inside the boundaries of the main board
Args:
coords: The coords of the intended sub-board
Returns:
True if the move is within the bounds of the main board, False otherwise
"""
if 0 <= coords.row < len(self._board) and 0 <= coords.col < len(self._board):
return True
return False
def __iter__(self):
return self._board.__iter__()
def __getitem__(self, key):
return self._board[key]