|
#!/bin/env python3 |
|
|
|
"""Data Structures used to represent the game state""" |
|
|
|
from __future__ import annotations |
|
|
|
from enum import Enum, IntEnum |
|
from typing import Dict, Tuple, Optional, Set, List |
|
|
|
|
|
Pt = IntEnum( |
|
"Pt", |
|
{chr(ord('A') + i): i for i in range(9)}) |
|
|
|
|
|
class Sym(Enum): |
|
"""Noughts and crosses""" |
|
O = 'O' |
|
X = 'X' |
|
|
|
|
|
class Grid: |
|
"""Maintains a valid state of the game""" |
|
_grid: Dict[Pt, Sym] |
|
_winner: Optional[Sym] |
|
_winning_move: Optional[Set[Pt]] |
|
|
|
_L_DIAGONAL = {Pt.A, Pt.E, Pt.I} |
|
_R_DIAGONAL = {Pt.C, Pt.E, Pt.G} |
|
|
|
def __init__(self) -> None: |
|
self._grid = {} |
|
self._empty = set(pt for pt in Pt) |
|
self._winner = None |
|
self._winning_move = None |
|
|
|
def set(self, pt: Pt, val: Sym) -> None: |
|
"""Marks a point with the given symbol. |
|
|
|
Raises an InvalidSetException if that point has been filled.""" |
|
if pt in self._grid: |
|
raise InvalidSetException( |
|
f"{pt} is already set to {self._grid[pt]}" |
|
) |
|
self._grid[pt] = val |
|
self._empty.remove(pt) |
|
|
|
self._winning_move = self._check_victory(pt) |
|
if self._winning_move: |
|
self._winner = val |
|
|
|
def get(self, pt: Pt) -> Optional[Sym]: |
|
"""Current value at pt""" |
|
return self._grid.get(pt) |
|
|
|
def winner(self) -> Optional[Tuple[Sym, Set[Pt]]]: |
|
"""Returns the winning symbol and line""" |
|
if self._winner and self._winning_move: |
|
return (self._winner, self._winning_move) |
|
return None |
|
|
|
def get_empty_pts(self) -> Set[Pt]: |
|
"""Currently open spaces""" |
|
return self._empty |
|
|
|
def _check_victory(self, new_pt: Pt) -> Optional[Set[Pt]]: |
|
val = int(new_pt) |
|
sym = self.get(new_pt) |
|
assert sym is not None |
|
|
|
if (new_pt in self._L_DIAGONAL and |
|
self._check_line(self._L_DIAGONAL, sym)): |
|
return self._L_DIAGONAL |
|
|
|
if (new_pt in self._R_DIAGONAL and |
|
self._check_line(self._R_DIAGONAL, sym)): |
|
return self._R_DIAGONAL |
|
|
|
row = val // 3 |
|
row_pts = set(Pt(row * 3 + i) for i in range(3)) |
|
if self._check_line(row_pts, sym): |
|
return row_pts |
|
|
|
col = val % 3 |
|
col_pts = set(Pt(col + 3 * i) for i in range(3)) |
|
if self._check_line(col_pts, sym): |
|
return col_pts |
|
|
|
return None |
|
|
|
|
|
def _check_line(self, diagonal: Set[Pt], sym: Sym) -> bool: |
|
return all(self.get(Pt(pt)) == sym for pt in diagonal) |
|
|
|
|
|
class InvalidSetException(Exception): |
|
"""Thrown if the point was already occupied""" |
|
... |