-
Notifications
You must be signed in to change notification settings - Fork 0
/
connect4_ai.py
339 lines (290 loc) · 11.9 KB
/
connect4_ai.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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
import numpy
import pygame
import math
import random
import sys
# global variables
# colour RGB values
# board colour
BLUE = (15, 100, 255)
# text colour
BLACK = (0, 0, 0)
# empty space
WHITE = (255, 255, 255)
# player chip
RED = (255, 0, 0)
# ai chip
YELLOW = (255, 255, 0)
# number of rows and columns in the connect 4 board
ROWS = 6
COLUMNS = 7
# the side length of the square that each circle is bounded within
SIDELENGTH = 100
# radius of each chip
RADIUS = int(SIDELENGTH / 2 - 5)
# player is represented by 0, AI is represented by 1
PLAYER = 0
AI = 1
# since the board is filled with zeros at the start, we will use 1 to represent the player piece and 2 to represent the AI piece
PLAYER_CHIP = 1
AI_CHIP = 2
# empty spot is represented by 0
EMPTY = 0
# the "window" is the length of partitions of the board we'll be inspecting to check for wins
# since the game is connect 4, the window length will be 4
WIN_LENGTH = 4
# this function create a new board
def create_board():
# np.zeros returns a new array of a given size, filled with zeros.
myBoard = numpy.zeros((ROWS, COLUMNS))
return myBoard
# this function drops a new chip in the board at a specified position
def drop_chip(myBoard, myRow, myCol, chip):
myBoard[myRow][myCol] = chip
# this function draws the connect 4 board
def draw_board(myBoard):
# draws the blue background and the white circles representing empty spots
for c in range(COLUMNS):
for r in range(ROWS):
pygame.draw.rect(screen, BLUE, (c * SIDELENGTH, r * SIDELENGTH + SIDELENGTH, SIDELENGTH, SIDELENGTH))
pygame.draw.circle(screen, WHITE, (int(c * SIDELENGTH + SIDELENGTH / 2), int(r * SIDELENGTH + SIDELENGTH + SIDELENGTH / 2)), RADIUS)
# draws all the chips put in by the AI and the player
for c in range(COLUMNS):
for r in range(ROWS):
if myBoard[r][c] == PLAYER_CHIP:
pygame.draw.circle(screen, RED, (int(c * SIDELENGTH + SIDELENGTH / 2), height - int(r * SIDELENGTH + SIDELENGTH / 2)), RADIUS)
elif myBoard[r][c] == AI_CHIP:
pygame.draw.circle(screen, YELLOW, (int(c * SIDELENGTH + SIDELENGTH / 2), height - int(r * SIDELENGTH + SIDELENGTH / 2)), RADIUS)
pygame.display.update()
# this function checks whether the specified column has any remaining spots
def has_spots_left(myBoard, myCol) -> bool:
return myBoard[ROWS - 1][myCol] == 0
# this function gets the next available row in the specified column
def next_empty_row(myBoard, myCol) -> int:
for r in range(ROWS):
if myBoard[r][myCol] == 0:
return r
# this function returns a list of columns that have empty spaces left
def get_valid_columns(myBoard) -> list:
valid_columns = []
for c in range(COLUMNS):
if has_spots_left(myBoard, c):
valid_columns.append(c)
return valid_columns
# this function checks the entire board for winning moves
def has_win(myBoard, chip) -> bool:
# check horizontal locations for win
for c in range(COLUMNS - 3):
for r in range(ROWS):
if myBoard[r][c] == chip and myBoard[r][c + 1] == chip and myBoard[r][c + 2] == chip and myBoard[r][c + 3] == chip:
return True
# check vertical locations for win
for c in range(COLUMNS):
for r in range(ROWS - 3):
if myBoard[r][c] == chip and myBoard[r + 1][c] == chip and myBoard[r + 2][c] == chip and myBoard[r + 3][c] == chip:
return True
# check positively sloped diagonals for win
for c in range(COLUMNS - 3):
for r in range(ROWS - 3):
if myBoard[r][c] == chip and myBoard[r + 1][c + 1] == chip and myBoard[r + 2][c + 2] == chip and myBoard[r + 3][c + 3] == chip:
return True
# check negatively sloped diagonals for win
for c in range(COLUMNS - 3):
for r in range(3, ROWS):
if myBoard[r][c] == chip and myBoard[r - 1][c + 1] == chip and myBoard[r - 2][c + 2] == chip and myBoard[r - 3][c + 3] == chip:
return True
return False
# this function determines the score of a certain window on the connect 4 board
def score_window(window, chip) -> int:
# initialize the opponent piece based who's turn it is
opponent_chip = PLAYER_CHIP
if chip == PLAYER_CHIP:
opponent_chip = AI_CHIP
score = 0
# a win is weighted 100
if window.count(chip) == 4:
score += 150
# 3 pieces in a window and an empty space is almost a win, so it's weighted 10
elif window.count(chip) == 3 and window.count(EMPTY) == 1:
score += 50
# 2 pieces in a window and 2 empty spaces is weighted 5
elif window.count(chip) == 2 and window.count(EMPTY) == 2:
score += 2
# if the opponent is close to a win, the weighting is negative
if window.count(opponent_chip) == 3 and window.count(EMPTY) == 1:
score -= 100
if window.count(opponent_chip) == 2 and window.count(EMPTY) == 2:
score -= 5
return score
# this function returns the total score of a specified player based on the current board
def get_total_score(myBoard, chip) -> int:
score = 0
window = []
# count the number of pieces for a specified player in the center column
# make it prefer the center column
# center_col = [i for i in list(myBoard[:, 3])]
center_col = [myBoard[i][3] for i in range(ROWS)]
center_col_count = center_col.count(chip)
score += center_col_count * 8
# count the total score for the rows
for r in range(ROWS):
row_array = list(myBoard[r])
for c in range(COLUMNS-3):
window = row_array[c:c+WIN_LENGTH]
score += score_window(window, chip)
# count the total score for the columns
for c in range(COLUMNS):
col_array = [int(i) for i in list(myBoard[:, c])]
for r in range(ROWS - 3):
window = col_array[r:r+WIN_LENGTH]
score += score_window(window, chip)
# count the total score for positively sloped diagonals
for r in range(ROWS - 3):
for c in range(COLUMNS - 3):
window = [myBoard[r+i][c+i] for i in range(WIN_LENGTH)]
score += score_window(window, chip)
# count the total score for negatively sloped diagonals
for r in range(ROWS - 3):
for c in range(COLUMNS - 3):
window = [myBoard[r+3-i][c+i] for i in range(WIN_LENGTH)]
score += score_window(window, chip)
return score
# helper function for my minimax algorithm
# checks whether the node is a terminal node (somebody won, or a tie occurred)
def is_terminal_node(myBoard) -> bool:
return has_win(myBoard, PLAYER_CHIP) or has_win(myBoard, AI_CHIP) or len(get_valid_columns(myBoard)) == 0
# this function determines the best column to drop the AI chip in
def minimax(myBoard, depth, maximizingPlayer) -> tuple:
# find all the columns that still have empty spaces
valid_locations = get_valid_columns(myBoard)
is_terminal = is_terminal_node(myBoard)
# base case
if depth == 0 or is_terminal:
if is_terminal:
# AI won
if has_win(myBoard, AI_CHIP):
return None, 100000000
# Player won
elif has_win(myBoard, PLAYER_CHIP):
return None, -100000000
# tie
else:
return None, 0
# depth == 0
else:
return None, get_total_score(myBoard, AI_CHIP)
# recursive case
# maximizing player is AI
if maximizingPlayer:
column = 0
value = -math.inf
# iterate through all the valid columns
for c in valid_locations:
# get the next open row
r = next_empty_row(myBoard, c)
# create a copy of the board in a separate memory location
# the purpose of this copy is to "look into the future"
# and determine which one is the best column to drop the chip in
b_copy = myBoard.copy()
drop_chip(b_copy, r, c, AI_CHIP)
# takes index 1 since minimax() returns a tuple of size 2
# recursive call
score = minimax(b_copy, depth-1, False)[1]
# search for the highest new score
if score > value:
value = score
column = c
return column, value
# minimizing player: person
else:
column = 0
value = math.inf
# iterate through all the valid columns
for c in valid_locations:
# create a copy of the board in a separate memory location
# the purpose of this copy is to "look into the future"
# and determine which one is the best column to drop the chip in
r = next_empty_row(myBoard, c)
b_copy = myBoard.copy()
drop_chip(b_copy, r, c, PLAYER_CHIP)
# takes index 1 since minimax() returns a tuple of size 2
# recursive call
score = minimax(b_copy, depth-1, True)[1]
# search for the lowest new score
if score < value:
value = score
column = c
return column, value
board = create_board()
game_over = False
pygame.init()
# width of game window
width = COLUMNS * SIDELENGTH
# height of game window
height = (ROWS + 1) * SIDELENGTH
# size of game window as a tuple
size = (width, height)
screen = pygame.display.set_mode(size)
draw_board(board)
pygame.display.update()
font = pygame.font.SysFont("monospace", 80)
drop_sound = pygame.mixer.Sound("dropping.wav")
# randomize who goes first
turn = random.randint(PLAYER, AI)
while not game_over:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# draw white rectangle at the top
pygame.draw.rect(screen, WHITE, (0, 0, width, SIDELENGTH))
if event.type == pygame.MOUSEMOTION:
xCoor = event.pos[0]
# have the chip follow the mouse of the player
if turn == PLAYER:
pygame.draw.circle(screen, RED, (xCoor, int(SIDELENGTH / 2)), RADIUS)
pygame.display.update()
# drop chip
if event.type == pygame.MOUSEBUTTONDOWN:
if turn == PLAYER:
xCoor = event.pos[0]
# calculate the column to drop the chip in
col = int(math.floor(xCoor / SIDELENGTH))
if has_spots_left(board, col):
row = next_empty_row(board, col)
# drop chip in the next empty row
drop_chip(board, row, col, PLAYER_CHIP)
drop_sound.play()
if has_win(board, PLAYER_CHIP):
# display message telling the user who won
label = font.render("RED WINS", 1, BLACK)
screen.blit(label, (40, 10))
game_over = True
turn += 1
turn = turn % 2
draw_board(board)
# keep the program running for 4 more seconds after the game is over
# this allows the user to confirm who won and who lost
if game_over:
pygame.time.wait(5000)
# AI Input
if turn == AI and not game_over:
# tuple unpacking the two return values
col, minimax_score = minimax(board, 3, True)
if has_spots_left(board, col):
# the AI takes 1 second to place their move
pygame.time.wait(500)
row = next_empty_row(board, col)
drop_chip(board, row, col, AI_CHIP)
drop_sound.play()
if has_win(board, AI_CHIP):
# display message telling the user who won
label = font.render("YELLOW WINS", 1, BLACK)
screen.blit(label, (40, 10))
game_over = True
draw_board(board)
turn += 1
turn = turn % 2
# keep the program running for 4 more seconds after the game is over
# this allows the user to confirm who won and who lost
if game_over:
pygame.time.wait(5000)