-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathbot.py
217 lines (189 loc) · 6.98 KB
/
bot.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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import random
import math
from common import (
ROW_COUNT,
COLUMN_COUNT,
MINIMAX,
MONTE_CARLO,
RANDOM,
RANDOM_IMPR,
Observer,
)
YELLOW_PLAYER = 1
RED_PLAYER = -1
PLAYERS = {1: "Yellow", -1: "Red"}
class Bot(Observer):
"""
This class handles the different bots that were used.
It includes a Random Bot, an Improved Random Bot, the MCTS bot,
and the MiniMax bot.
"""
def __init__(
self, game, bot_type=None, depth=None, iteration=None, pruning=True
):
"""
Constructor of the Bot class.
:param game: corresponding Connect4Game instance
:param bot_type: specifies the bot (MCTS, MiniMax, Random, ...)
:param depth: depth used in the Minimax algorithm if the Minimax bot is used
:param iteration: number of iterations used in the MCTS algorithm in case the MCTS bot is used
:param pruning: boolean used for the pruning in the Minimax algorithm if the Minimax bot is used
"""
self._game = game
# Bot type determines how the bot picks his moves
self._type = bot_type
if self._type == MINIMAX:
self._depth = depth
self._pruning = pruning
elif self._type == MONTE_CARLO:
self._iteration = iteration
def __repr__(self):
return self._type
def update(self, obj, event, *argv):
print(obj)
def make_move(self):
"""
Picks the column in which the bot should place the next disc.
The considered moving options depend on the bot type.
:return: the column number where the bot should play the next move
"""
# print(PLAYERS[self._game._turn] + " is about to play :")
column = None
# In case the bot type is RANDOM, the bot checks for winning moves, and if there aren't,
# then picks a valid random move.
if self._type == RANDOM:
win_col = self.get_winning_move()
if win_col is not None:
column = win_col
else:
column = self.get_random_move()
# In case the bot type is RANDOM IMPROVED, the bot checks for winning moves, and if there aren't,
# then checks if there is any move that blocks a direct winning move for the opponent.
# If there is no such move, it picks a valid random move.
elif self._type == RANDOM_IMPR:
win_col = self.get_winning_move()
if win_col is not None:
# print("Winning column :", win_col)
column = win_col
else:
def_move = self.get_defensive_move()
if def_move is not None:
# print("Defensive column :", def_move)
column = def_move
else:
column = self.get_random_move()
# print("Random move", column)
elif self._type == MINIMAX:
column, minimax_score = self.minimax(
self._game._board,
self._depth,
-math.inf,
math.inf,
True,
self._pruning,
)
# print(column)
elif self._type == MONTE_CARLO:
o = Node(self._game.copy_state())
column = self.monte_carlo_tree_search(self._iteration, o, 2.0)
else:
column = 0
# print("-------------------------")
self._game.place(column)
def get_winning_move(self):
"""
Checks whether there is a winning column available for the next
move of the bot.
:return: winning column
"""
column = None
for c_win in range(self._game._cols):
for r in range(self._game._rows):
if self._game._board[c_win][r] == 0:
self._game._board[c_win][r] = self._game._turn
is_winner = self._game.check_win((c_win, r))
self._game._board[c_win][r] = 0
if is_winner:
column = c_win
return column
break
return column
def get_valid_locations(self, board):
"""
Returns all the valid columns where the player can play, aka the columns
that are not full
:param board: actual state of the game, board of the game
:return: list of all valid column indices
"""
free_cols = []
for i in range(COLUMN_COUNT):
if board[i][ROW_COUNT - 1] == 0:
free_cols.append(i)
# print()
if len(free_cols) == 0:
return None
return free_cols
def get_random_move(self):
"""
Picks a valid random column where the bot can play his next move.
:return: valid random column
"""
free_cols = self.get_valid_locations(self._game._board)
column = random.choice(free_cols)
return column
def get_defensive_move(self):
"""
Checks whether the bot could play a move that blocks a direct winning
move from the opponent.
:return: column to be played to avoid losing immediatly
"""
column = None
for c_win in range(self._game._cols):
for r in range(self._game._rows):
if self._game._board[c_win][r] == 0:
self._game._board[c_win][r] = -1 * self._game._turn
is_winner = self._game.check_win((c_win, r))
self._game._board[c_win][r] = 0
if is_winner:
column = c_win
return column
break
return column
class Node:
"""
This class is used to represent nodes of the tree of boards used during
Monte-Carlo Tree Search.
"""
def __init__(self, state, parent=None):
self.visits = 1
self.reward = 0.0
self.state = state # Instance of Connect4Game
self.children = []
self.children_moves = []
self.parent = parent
def add_child(self, child_state, move):
"""
Add a child to the current node.
:param child_state: state of the child to add
:param move: move to do to get to the newly added child
"""
child = Node(child_state, parent=self)
self.children.append(child)
self.children_moves.append(move)
def update(self, reward):
"""
Update the node's reward (indicates how good a certain node is
according to the MCTS algorithm)
:param reward: reward to be added to the node
"""
self.reward += reward
self.visits += 1
def fully_explored(self):
"""
Checks if the node is fully explored (which means we can not add
any more children to this node)
:return: True of False depending on if it is fully epxlored or not
"""
if len(self.children) == len(self.state.get_valid_locations()):
return True
return False