games/tictactoe.py

136 lines
3.1 KiB
Python
Raw Normal View History

2023-06-01 07:58:45 +00:00
from __future__ import annotations
import typing as tp
from dataclasses import dataclass
from functools import cache, cached_property
Player = tp.Literal["X", "O"]
Draw = tp.Literal["-"]
Square = tp.Literal["X", "O", " "]
Board = tuple[Square, Square, Square, Square, Square, Square, Square, Square, Square]
@dataclass(frozen=True)
class GameState:
player: Player
board: Board
def __str__(self):
b = self.board
return (
f" {b[0]}{b[1]}{b[2]}\n"
"───┼───┼───\n"
f" {b[3]}{b[4]}{b[5]}\n"
"───┼───┼───\n"
f" {b[6]}{b[7]}{b[8]}"
)
@dataclass(frozen=True)
class Move:
player: Player
position: int
def get_valid_moves(state: GameState) -> tuple[Move]:
return tuple(
Move(state.player, position=i)
for i, square in enumerate(state.board)
if square == " "
)
@cache
def apply_move(state: GameState, move: Move) -> GameState:
new_player = "X" if move.player == "O" else "O"
new_board = list(state.board)
new_board[move.position] = move.player
new_board = tuple(new_board)
return GameState(player=new_player, board=new_board)
2023-06-01 08:17:35 +00:00
@cache
2023-06-01 07:58:45 +00:00
def get_winner(state: GameState) -> Player | Draw | None:
# abc
# def
# ghi
a, b, c, d, e, f, g, h, i = state.board
# horizontal
if a == b == c and a != " ":
return a
if d == e == f and d != " ":
return d
if g == h == i and g != " ":
return g
# vertical
if a == d == g and a != " ":
return a
if b == e == h and b != " ":
return b
if c == f == i and c != " ":
return c
# diagonal
if a == e == i and a != " ":
return a
if c == e == g and c != " ":
return c
# draw
if not any(square == " " for square in state.board):
return "-"
# no winner
return None
# monte-carlo style tree search
START = GameState(player="X", board=(" ",) * 9)
# with dynamic programming B)
# next_states: dict[GameState, tuple[GameState]] = {}
total_nodes = 0
2023-06-01 07:58:45 +00:00
@cache
def get_next_states(state: GameState) -> tuple[GameState]:
if get_winner(state) is not None:
return tuple()
return tuple(apply_move(state, move) for move in get_valid_moves(state))
2023-06-01 07:58:45 +00:00
@cache
def get_score(state: GameState, target: Player) -> float:
global total_nodes
total_nodes += 1
opponent = "X" if target == "O" else "O"
winner = get_winner(state)
if winner == target:
return float("inf")
if winner == "-":
return 0
if winner == opponent:
return float("-inf")
assert winner is None
next_scores = (
get_score(next_state, target) for next_state in get_next_states(state)
)
if state.player == target:
# target player plays at this node
return max(next_scores)
else:
# opponent plays at this node
return min(next_scores)
2023-06-01 07:58:45 +00:00
if __name__ == "__main__":
# TODO: "get best move"
print(f"{START=}, {get_score(START, 'X')=}")
print(f"{total_nodes=}")