File size: 2,339 Bytes
c7928dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from __future__ import annotations

from functools import cache

import numpy as np

WIN_LINES = [
    (0, 1, 2),
    (3, 4, 5),
    (6, 7, 8),
    (0, 3, 6),
    (1, 4, 7),
    (2, 5, 8),
    (0, 4, 8),
    (2, 4, 6),
]


def winner(board: np.ndarray) -> int:
    for left, middle, right in WIN_LINES:
        value = int(board[left])
        if value and value == board[middle] == board[right]:
            return value
    return 0


def is_terminal(board: np.ndarray) -> bool:
    return winner(board) != 0 or not np.any(board == 0)


def legal_actions(board: np.ndarray) -> np.ndarray:
    return np.flatnonzero(board == 0)


def play(board: np.ndarray, action: int, player: int) -> np.ndarray:
    if board[action] != 0:
        raise ValueError(f"Cell {action} is occupied.")
    next_board = board.copy()
    next_board[action] = player
    return next_board


def canonical(board: np.ndarray, player: int) -> np.ndarray:
    return board.astype(np.float32) * player


def symmetries(
    state: np.ndarray,
    policy: np.ndarray,
) -> list[tuple[np.ndarray, np.ndarray]]:
    board = state.reshape(3, 3)
    probabilities = policy.reshape(3, 3)
    transformed = []
    for rotations in range(4):
        rotated_board = np.rot90(board, rotations)
        rotated_policy = np.rot90(probabilities, rotations)
        transformed.append(
            (rotated_board.reshape(-1).copy(), rotated_policy.reshape(-1).copy())
        )
        transformed.append(
            (
                np.fliplr(rotated_board).reshape(-1).copy(),
                np.fliplr(rotated_policy).reshape(-1).copy(),
            )
        )
    return transformed


@cache
def minimax_value(board_key: tuple[int, ...], player: int) -> int:
    board = np.asarray(board_key, dtype=np.int8)
    result = winner(board)
    if result:
        return 1 if result == player else -1
    legal = legal_actions(board)
    if not len(legal):
        return 0
    return max(
        -minimax_value(tuple(play(board, int(action), player)), -player)
        for action in legal
    )


def minimax_action(board: np.ndarray, player: int) -> int:
    legal = legal_actions(board)
    values = [
        -minimax_value(tuple(play(board, int(action), player)), -player)
        for action in legal
    ]
    best = max(values)
    return int(legal[values.index(best)])