games/tictactoe.py

169 lines
3.9 KiB
Python
Raw Normal View History

2023-06-01 07:58:45 +00:00
from __future__ import annotations
2023-06-07 04:35:36 +00:00
import pdb
import random
2023-06-01 07:58:45 +00:00
import typing as tp
from dataclasses import dataclass
from functools import cache
2023-06-01 07:58:45 +00:00
2023-06-07 04:35:36 +00:00
import pdbp
2023-06-08 03:22:30 +00:00
import util
2023-06-07 04:35:36 +00:00
if hasattr(pdb, "DefaultConfig"):
pdb.DefaultConfig.sticky_by_default = False # type:ignore
2023-06-01 07:58:45 +00:00
Player = tp.Literal["X", "O"]
2023-06-02 05:58:42 +00:00
Winner = Player | tp.Literal["draw"]
Square = Player | tp.Literal[" "]
2023-06-01 07:58:45 +00:00
Board = tuple[Square, Square, Square, Square, Square, Square, Square, Square, Square]
2023-06-07 04:35:36 +00:00
Score = int
2023-06-02 05:58:42 +00:00
# TODO: "get best move"
2023-06-07 04:35:36 +00:00
# TODO: alpha-beta pruning (elegantly)
2023-06-02 05:58:42 +00:00
# TODO: rotational / reflectional board parity (for less total nodes)
2023-06-01 07:58:45 +00:00
2023-06-07 04:35:36 +00:00
class QuitException(Exception):
pass
2023-06-01 07:58:45 +00:00
@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:
position: int
2023-06-07 04:35:36 +00:00
def __str__(self) -> str:
return str(self.position + 1)
2023-06-01 07:58:45 +00:00
2023-06-08 03:22:30 +00:00
# @cache
def get_valid_moves(state: GameState) -> tp.Iterable[Move]:
2023-06-01 07:58:45 +00:00
return tuple(
2023-06-02 05:58:42 +00:00
Move(position=i) for i, square in enumerate(state.board) if square == " "
2023-06-01 07:58:45 +00:00
)
2023-06-08 03:22:30 +00:00
# @cache
2023-06-01 07:58:45 +00:00
def apply_move(state: GameState, move: Move) -> GameState:
new_player = "X" if state.player == "O" else "O"
2023-06-01 07:58:45 +00:00
new_board = list(state.board)
new_board[move.position] = state.player
2023-06-01 07:58:45 +00:00
new_board = tuple(new_board)
return GameState(player=new_player, board=new_board)
2023-06-08 03:22:30 +00:00
# @cache
def get_winner(state: GameState) -> Winner | None:
2023-06-01 07:58:45 +00:00
# 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):
2023-06-02 05:58:42 +00:00
return "draw"
2023-06-01 07:58:45 +00:00
# no winner
return None
2023-06-08 03:22:30 +00:00
# @cache
2023-06-07 04:35:36 +00:00
def get_next_states(state: GameState) -> tuple[tuple[Move, GameState], ...]:
2023-06-02 05:58:42 +00:00
assert get_winner(state) is None, "should not be called if game ended"
2023-06-07 04:35:36 +00:00
return tuple((move, apply_move(state, move)) for move in get_valid_moves(state))
2023-06-01 07:58:45 +00:00
2023-06-08 03:22:30 +00:00
# @cache
@util.count_calls
2023-06-02 05:58:42 +00:00
def get_score(target: Player, state: GameState) -> Score:
winner = get_winner(state)
if winner == target:
return 1
if winner == "draw":
return 0
if winner is not None:
# winner must be the opponent
return -1
2023-06-01 07:58:45 +00:00
2023-06-02 05:58:42 +00:00
agg = max if state.player == target else min
2023-06-07 04:35:36 +00:00
score = agg(
get_score(target, next_state) for _, next_state in get_next_states(state)
)
2023-06-02 05:58:42 +00:00
return score
2023-06-01 07:58:45 +00:00
2023-06-02 05:58:42 +00:00
2023-06-08 03:22:30 +00:00
# @cache
def get_scored_moves(
target: Player, state: GameState
) -> tuple[tuple[Score, Move], ...]:
return tuple(
(get_score(target, state), move) for move, state in get_next_states(state)
2023-06-07 04:35:36 +00:00
)
def get_human_move(state: GameState) -> Move:
print("123\n456\n789")
while True:
choice = input("choice: ")
if choice.endswith("b"):
breakpoint()
choice = choice[:-1]
if choice == "":
continue
elif choice == "q":
raise QuitException
move = Move(position=int(choice) - 1)
if move not in get_valid_moves(state):
print("bad move")
continue
return move
2023-06-02 05:58:42 +00:00
REAL = GameState(player="X", board=(" ",) * 9)
2023-06-08 03:22:30 +00:00
# if __name__ == "__main__":
# # total_nodes=5478
# # print(f"{get_score('X', REAL)=}, {CALL_COUNTS['get_score']=}")
# # best_moves = get_best_moves(REAL)
# play_human(human_player="X", state=REAL)