from __future__ import annotations
import numpy as np
from typing import (
Optional,
TYPE_CHECKING
)
from .enum import Color
if TYPE_CHECKING:
from .board import Board
[docs]
class Territory:
"""Represents a territory i.e. a list of nearby vertices of the same color"""
def __init__(self, *,
x: Optional[int] = None,
y: Optional[int] = None,
vertices: Optional[list[tuple[int, int]]] = None,
board: Board
):
"""
Note:
When using the `x, y` initializer, the territory is built by exploring the board while with `vertices` it only uses the given vertices without any exploration
Args:
x: The x coordinate of the vertice to use to initiate the territory. This cannot be mixed with the `vertices` parameter
y: The y coordinate of the vertice to use to initiate the territory. This cannot be mixed with the `vertices` parameter
vertices: A list of vertices to initiate the territory. This cannot be mixed with the `x` and `y` parameters
board: The board associated with the territory
Raises:
TypeError: Parameters are not of the right type
ValueError: Failed to create the territory with the given parameters
"""
self._board: Board = board
self._vertices: list[tuple[int, int]]
self._freedom: list[tuple[int, int]]
self._color: Color
if vertices is not None and x is None and y is None:
if not isinstance(vertices, list):
raise TypeError(f'Expected type list for vertices but got {vertices.__class__.__name__}')
if not vertices:
raise ValueError('Vertices list cannot be empty')
self._color = board[vertices[0]]
if any(board[v] is not self._color for v in vertices):
raise ValueError('Vertices are of different colors')
self._vertices = vertices
self._freedom = self._hypothetical_freedom() if self._color is not Color.Empty else []
if not self.is_coherent:
raise ValueError('Vertices are not all nearby')
elif x is not None and y is not None and vertices is None:
self._vertices = self._explore(x, y)
self._color = board[x, y]
self._freedom = self._hypothetical_freedom() if self._color is not Color.Empty else []
else:
raise TypeError("Please provide either vertices or both x and y")
def __repr__(self):
return f"<{self.__class__.__name__} board={self._board} size={self.size} color={self._color}>"
@property
def size(self) -> int:
"""Returns the number of vertices in the territory"""
return len(self._vertices)
@property
def vertices(self) -> list[tuple[int, int]]:
return self._vertices
[docs]
def clone(self, board: Optional[Board] = None) -> Territory:
"""Returns a deep copy of the territory
Args:
board: The board to link the new territory, if None it's the same as the current territory
Returns:
The copy of the territory
"""
new_territory = Territory(vertices=list(self._vertices), board=board if board else self._board)
return new_territory
def is_coherent(self) -> bool:
return set(self._explore(*self._vertices[0])) == set(self._vertices)
def _explore(self, x: int, y: int) -> list[tuple[int, int]]:
to_explore = set(self._board.around(x, y))
explored = [(x, y)]
while to_explore:
for i, j in list(to_explore):
if self._board[i, j] is self._board[x, y]:
explored.append((i, j))
to_explore.update({k for k in self._board.around(i, j) if k not in explored})
to_explore.remove((i, j))
return explored
[docs]
@classmethod
def merge(cls,
*territories: Territory,
with_vertice: Optional[tuple[int, int]] = None
) -> Territory:
"""Merge several connected territories into one
Args:
territories: An argument list of territories to merge
with_vertice: A vertice to connect all territories, if none is specified, territories have to be already connected
Raises:
TypeError: Parameters aren't of the right type
ValueError: Failed to merge territories
Returns:
The new territory"""
if n := next((t for t in territories if not isinstance(t, Territory)), None):
raise TypeError(f'Expected value of type Territory, but got {n.__class__.__name__}')
if len(territories) < 2:
raise TypeError('Expected at least 2 argument for merging')
if any(t._board != territories[0]._board for t in territories):
raise ValueError('Territories are not on the same board')
vertices = []
if with_vertice:
if not isinstance(with_vertice, (tuple, list, np.ndarray)) or not len(with_vertice) == 2:
raise TypeError('Expected 2-len tuple for with_vertices')
vertices.append(with_vertice)
for x in territories:
vertices.extend(x._vertices)
new_territory = cls(vertices=list(set(vertices)), board=territories[0]._board)
return new_territory
[docs]
def is_nearby(self, territory: Territory) -> bool:
"""Checks if a territory is connected
Args:
territory: The territory to check
Returns:
Indicate if the territory is connected or not"""
return any(territory.is_touching(x, y) for x, y in self._vertices)
[docs]
def is_touching(self, x: int, y: int) -> bool:
"""Checks if a vertice is touching the territory
Args:
x: The x coordinate of the vertice
y: The y coordinate of the vertice
Returns:
Indicate if the vertice is touching the territory"""
for i, j in self._board.around(x, y):
if (i, j) in self._vertices:
return True
return False
[docs]
def includes(self, x: int, y: int, color: Optional[Color] = None) -> bool:
"""Checks if a vertice is included in the territory
Args:
x: The x coordinate of the vertice
y: The y coordinate of the vertice
color: The color of the targeted vertice
Returns:
Indicates if the vertice is included or not"""
return (color is None or color is self._color) and (x, y) in self._vertices
def _update(self, x: int, y: int, color: Color) -> None:
if self.is_touching(x, y):
if color is self._color:
if (x, y) not in self._vertices:
self._vertices.append((x, y))
if self._color is not Color.Empty:
if (x, y) in self._freedom:
self._freedom.remove((x, y))
self._freedom.extend([(i, j) for i, j in self._board.around(x, y) if self._board[i, j] is Color.Empty])
else:
if (x, y) in self._vertices:
self._vertices.remove((x, y))
if self._color is not Color.Empty:
if self._board[x, y] is Color.Empty:
self._freedom.append((x, y))
for i, j in self._board.around(x, y):
if (i, j) in self._freedom and not any((k, l) in self._vertices for k, l in self._board.around(i, j)):
self._freedom.remove((i, j))
else:
if (x, y) in self._freedom:
self._freedom.remove((x, y))
@property
def board(self) -> Board:
"""The board associated with the territory"""
return self._board
@property
def color(self) -> Color:
"""The color of the territory"""
return self._color
[docs]
def freedom(self) -> list[tuple[int, int]]:
"""Calculate the freedom of the territory, i.e. the vertices where it can expend
Returns:
The list of available vertices to expend the territory"""
return self._freedom
def _hypothetical_freedom(self,
x: Optional[int] = None,
y: Optional[int] = None,
color: Optional[Color] = None
) -> list[tuple[int, int]]:
if any(k is None for k in [x, y, color]) and not all(k is None for k in [x, y, color]):
raise TypeError('x, y and color have to be all specified')
free_vertices = []
if x is not None and y is not None and color is not None:
hypothetical_board = np.copy(self._board._grid)
hypothetical_board[x, y] = color
if color == self._color and self.is_touching(x, y):
hypothetical_vertices = self._vertices + [(x, y)]
else:
hypothetical_vertices = self._vertices
else:
hypothetical_board = self._board._grid
hypothetical_vertices = self._vertices
for x, y in hypothetical_vertices:
for i, j in self._board.around(x, y):
if hypothetical_board[i, j] is Color.Empty and (i, j) not in free_vertices:
free_vertices.append((i, j))
return free_vertices