75 lines
1.8 KiB
Python
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)))
|