Skip to content

Instantly share code, notes, and snippets.

@kunalb
Last active May 2, 2020 18:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kunalb/0a17f1221cb03612c01c288326cb432d to your computer and use it in GitHub Desktop.
Save kunalb/0a17f1221cb03612c01c288326cb432d to your computer and use it in GitHub Desktop.
RC Application: Tic Tac Toe
#!/bin/env python3
#!/bin/env python3
"""Entry point for the game
TODO: Extend to keep score across multiple games."""
from ttt.core import Sym
from ttt.play import Game
from ttt.player import AutoPlayer
def play() -> None:
"""Run a single game
TODO: Add nicer exit handling."""
game = Game(x = AutoPlayer(Sym.X))
game.play()
if __name__ == "__main__":
play()
#!/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"""
...
#!/bin/env python3
"""Input/Output routines"""
from __future__ import annotations
from typing import TYPE_CHECKING, Set
from ttt.core import Pt
if TYPE_CHECKING:
from ttt.core import Grid
def draw(grid: Grid):
"""
- Define an indexing system for the separators
- Simplest way would be to just use the pair of adjacent cells
- Eg. for A it'll be AB, AD
- Find the indexes of the separators from the list of the winning points
- Break up the drawing scheme to draw in pieces (|, ---, +)
- Use the over-ridden indexes to draw (-, |, \, /) instead
"""
# TODO Refactor this api
if grid.winner():
winner, move = grid.winner()
else:
winner, move = None, None
print() # Leading blank line
for i in range(5):
for j in range(5):
# Points
if (i & 1) == 0 and (j & 1) == 0:
pt = _get_pt(i, j)
pt = grid.get(pt) or pt
print(f" {pt.name} ", end='')
continue
# Corners
if (i & 1) and (j & 1):
sym = '+'
connected = [
((i - 1, j - 1), (i + 1, j + 1), '\\'),
((i - 1, j + 1), (i + 1, j - 1), '/'),
]
# Vertical lines
elif (j & 1):
sym = '|'
connected = [((i, j - 1), (i, j + 1), '-')]
# Horizontal lines
elif (i & 1):
sym = '---'
connected = [((i - 1, j), (i + 1, j), '-|-')]
else:
assert False, "Invalid state"
output = sym
# Over-ride winning connections
if move:
for pts in connected:
y, z = _get_pt(*pts[0]), _get_pt(*pts[1])
if y in move and z in move:
output = pts[2]
break
print(output, end='')
print()
def _get_pt(i, j) -> Optional[Pt]:
if i >= 0 and i < 5 and j >= 0 and j < 5:
return Pt((i // 2) * 3 + (j // 2))
return None
def read_value(prompt: str, valid: Set[str]) -> str:
"""Accepts a whitelisted value"""
while (val := input(prompt)) not in valid:
...
return val
#!/bin/env python3
"""A single game of Tic-Tac-Toe"""
from typing import Optional
from ttt.core import Grid, Pt, Sym
from ttt.player import Player
from ttt.draw import draw
class Game:
"""Runs the mechanics of playing the game"""
def __init__(self,
grid: Optional[Grid] = None,
o: Optional[Player] = None,
x: Optional[Player] = None) -> None:
self._grid = grid or Grid()
self._players = [o or Player(Sym.O),
x or Player(Sym.X)]
def play(self) -> Optional[Sym]:
"""Start the game!"""
cur = 0
while (not (winner := self._grid.winner()) and
self._grid.get_empty_pts()):
draw(self._grid)
print()
player = self._players[cur]
next_move = player.next_move(self._grid)
self._grid.set(next_move, self._players[cur].sym())
cur = not cur
draw(self._grid)
print()
if winner:
print(f"Player {winner[0].name} won!")
return winner[0]
else:
print("This match was a draw!")
return None
#!/bin/env python3
"""Players to interact with the game.
TODO: Add an AI player"""
import random
import ttt.core as core
import ttt.draw as draw
class Player:
"""A Human Player"""
def __init__(self, sym: core.Sym) -> None:
self._sym = sym
def sym(self) -> core.Sym:
"""Player's symbol"""
return self._sym
def next_move(self, grid: core.Grid) -> core.Pt:
"""Return the next point to mark"""
valid_pts = set(pt.name for pt in grid.get_empty_pts())
sorted_pts = sorted(list(valid_pts))
prompt = f"Player {self._sym.name} [{','.join(sorted_pts)}] > "
return core.Pt[draw.read_value(prompt, valid_pts)]
class AutoPlayer(Player):
"""An automatic player"""
def next_move(self, grid: core.Grid) -> core.Pt:
"""Randomly choose a point to play next"""
return random.choice(list((grid.get_empty_pts())))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment