games/connect4.py
2024-01-03 09:38:49 -08:00

75 lines
1.8 KiB
Python

from __future__ import annotations
import typing as tp
from dataclasses import dataclass
from functools import cache
Player = tp.Literal["X", "O"]
Winner = Player | tp.Literal["draw"]
Space = Player | tp.Literal[" "]
Board = tuple[Space, ...]
Score = int
C4_COLS = 7
C4_ROWS = 6
@dataclass(frozen=True)
class C4GameState:
player: Player
board: Board
def __str__(self) -> str:
b = self.board
return f"───┼───┼───┼───┼───┼───┼───\n".join(
f" {b[i+0]}{b[i+1]}{b[i+2]}{b[i+3]}{b[i+4]}{b[i+5]}{b[i+6]}\n"
for i in range(0, C4_COLS * C4_ROWS, C4_COLS)
)
@dataclass(frozen=True)
class Move:
col: int
def __str__(self) -> str:
return str(self.col + 1)
@cache
def get_valid_moves(state: C4GameState) -> tp.Iterable[Move]:
# NOTE: a slot must not be full if its top row is empty
return tuple(Move(col) for col in range(C4_COLS) if state.board[col] == " ")
@cache
def apply_move(state: C4GameState, move: Move) -> C4GameState:
new_player = "X" if state.player == "O" else "O"
# B)
for i in range(1, C4_ROWS):
idx_above = (i - 1) * C4_COLS + move.col
idx_below = (i - 0) * C4_COLS + move.col
if state.board[idx_below] != " ":
idx = idx_above
break
else:
# bottom
idx = (C4_ROWS - 1) * C4_COLS + move.col
new_board = list(state.board)
new_board[idx] = state.player
new_board = tuple(new_board)
return C4GameState(player=new_player, board=new_board)
@cache
def get_winner(state: C4GameState) -> Winner | None:
pass
START = C4GameState(player="X", board=(" ",) * C4_COLS * C4_ROWS)
if __name__ == "__main__":
print(START)
print(apply_move(START, Move(col=6)))