diff --git a/connect4.py b/connect4.py new file mode 100644 index 0000000..2c8be26 --- /dev/null +++ b/connect4.py @@ -0,0 +1,74 @@ +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))) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cbc3a54 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pdbp diff --git a/tictactoe.py b/tictactoe.py index f565e76..0e7d3b5 100644 --- a/tictactoe.py +++ b/tictactoe.py @@ -33,7 +33,7 @@ class TTTGameState: player: Player board: Board - def __str__(self): + def __str__(self) -> str: b = self.board return ( f" {b[0]} │ {b[1]} │ {b[2]}\n" @@ -52,14 +52,14 @@ class Move: return str(self.position + 1) -# @cache +@cache def get_valid_moves(state: TTTGameState) -> tp.Iterable[Move]: return tuple( Move(position=i) for i, square in enumerate(state.board) if square == " " ) -# @cache +@cache def apply_move(state: TTTGameState, move: Move) -> TTTGameState: new_player = "X" if state.player == "O" else "O" @@ -70,7 +70,7 @@ def apply_move(state: TTTGameState, move: Move) -> TTTGameState: return TTTGameState(player=new_player, board=new_board) -# @cache +@cache def get_winner(state: TTTGameState) -> Winner | None: # abc # def @@ -103,18 +103,18 @@ def get_winner(state: TTTGameState) -> Winner | None: if not any(square == " " for square in state.board): return "draw" - # no winner + # no winner yet return None -# @cache +@cache def get_next_states(state: TTTGameState) -> tuple[tuple[Move, TTTGameState], ...]: assert get_winner(state) is None, "should not be called if game ended" return tuple((move, apply_move(state, move)) for move in get_valid_moves(state)) -# @cache -@util.count_calls +@cache +# @util.count_calls def get_score(target: Player, state: TTTGameState) -> Score: winner = get_winner(state) if winner == target: @@ -132,7 +132,7 @@ def get_score(target: Player, state: TTTGameState) -> Score: return score -# @cache +@cache def get_scored_moves( target: Player, state: TTTGameState ) -> tuple[tuple[Score, Move], ...]: