-
Notifications
You must be signed in to change notification settings - Fork 0
/
cpu_player.py
175 lines (155 loc) · 6.4 KB
/
cpu_player.py
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
import math
import random
import time
from domain import IBoard, LogEntry
import copy
class GameState:
def __init__(self, board, curr_player: int, move: int) -> None:
self.move = move
"""The move (column) that led to this state"""
self.board: IBoard = board
self._curr_player = curr_player
self._children: list[GameState] = []
self._score = None
def is_terminal(self):
"""Check if this state is a terminal state (win, loss, or draw)."""
return (
self.board.check_win(1) or self.board.check_win(2) or self.board.is_full()
)
def generate_children(self) -> list[GameState]:
children: list[GameState] = []
next_player = 3 - self._curr_player
for move in possible_moves(self.board):
board_copy = copy.deepcopy(self.board)
board_copy.accept_move(move, self._curr_player)
child_state = GameState(board_copy, next_player, move)
children.append(child_state)
return children
def switch_players(self):
if self._curr_player == 1:
self._curr_player = 2
else:
self._curr_player = 1
def new_cpu_player(board: IBoard, player_no: int, opponent: int) -> AIPlayer:
return AIPlayer(board, player_no, opponent)
class AIPlayer:
def __init__(self, board: IBoard, player_no: int, opponent: int) -> None:
self.__board = board
self.__player_no = player_no
self.__opponent = opponent
self.__curr_player = player_no
self.depth = 3
self.players = [self.__opponent, self.__player_no]
self._recurse_count = 0
self._total_nodes_explored = 0
self._time_elapsed: float = 0
def stats(self) -> LogEntry:
return LogEntry(
self._recurse_count, self._total_nodes_explored, self._time_elapsed
)
def move(self) -> int:
start = time.perf_counter()
best_move = random.choice(range(7))
best_score = -math.inf
game_state = GameState(self.__board, self.__curr_player, 0)
children = game_state.generate_children()
self._recurse_count = 0
for child in children:
score = self.minimax(child, self.depth, False, -math.inf, math.inf)
print("move() score for child: ", score, "move: ", child.move)
if score > best_score:
print("Default score has been bested!")
best_score = score
best_move = child.move
print("move() best_move: ", best_move)
end = time.perf_counter()
self._time_elapsed = end - start
self._total_nodes_explored += self._recurse_count
return best_move
def minimax(
self,
game_state: GameState,
depth: int,
maximizing_player: bool,
alpha: float,
beta: float,
):
print("minimax() depth: ", depth)
self._recurse_count += 1
print("Recurse count: ", self._recurse_count)
print("game_state.__curr_player: ", game_state._curr_player)
print("maximizing: ", maximizing_player)
print("game_state board: ")
for row in game_state.board.states_grid():
print(row)
max_eval = -math.inf
if depth == 0 or game_state.is_terminal():
return self.evaluate_board(game_state.board)
if maximizing_player:
children = game_state.generate_children()
for child in children:
eval = self.minimax(child, depth - 1, False, alpha, beta)
max_eval = max(max_eval, eval)
alpha = max(alpha, eval)
if beta <= alpha:
break # Beta cut-off
return max_eval
else:
min_eval = math.inf
children = game_state.generate_children()
for child in children:
eval = self.minimax(child, depth - 1, True, alpha, beta)
min_eval = min(min_eval, eval)
beta = min(beta, eval)
if beta <= alpha:
break # Alpha cut-off
return min_eval
def evaluate_board(self, board: IBoard) -> float:
score = 0
close_to_four_self = self.close_to_four_count(self.__player_no, board)
close_to_four_opp = self.close_to_four_count(self.__opponent, board)
score += 5 * close_to_four_self
score -= 5 * close_to_four_opp
if board.check_win(self.__player_no):
score += 100
if board.check_win(self.__opponent):
score -= 100
print("score in evaluate_board(): ", score)
return score
def close_to_four_count(self, player: int, board: IBoard) -> int:
"""The number of times player has three in a row with a fourth space empty"""
count = 0
# Count horizontal instances
for row in range(board.rows()):
for col in range(board.columns() - 3):
if is_close_horiz(row, col, board, player):
count += 1
# Check vertical using same function but passing columns as rows and vice versa
for col in range(board.columns()):
for row in range(board.rows() - 3):
if is_close_vert(row, col, board, player):
count += 1
print("close_to_four_count: ", count)
return count
def possible_moves(board: IBoard) -> list[int]:
cols = []
for col in range(board.columns()):
if board.state(0, col) == 0:
cols.append(col)
return cols
def is_close_horiz(row: int, col: int, board: IBoard, player: int) -> bool:
"""Returns True if the count of `player` for a horizontal window of length 4 starting at `col` is 3 and count of 0 (empty) is 1"""
window = board.states_grid()[row][col : col + 4]
has_three_player = window.count(player) == 3
has_one_empty = window.count(0) == 1
return has_three_player and has_one_empty
def is_close_vert(row: int, col: int, board: IBoard, player: int) -> bool:
"""Returns True if the count of `player` for a vertical window of length 4 starting at `row` is 3 and count of 0(empty) is 1"""
column = [rw[col] for rw in board.states_grid()]
window = column[row : row + 4]
has_three_player = window.count(player) == 3
has_one_empty = window.count(0) == 1
return has_three_player and has_one_empty