-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathConnectFourEngine.py
More file actions
401 lines (334 loc) · 15.7 KB
/
ConnectFourEngine.py
File metadata and controls
401 lines (334 loc) · 15.7 KB
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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# import all of the necessary libraries
import sys,pygame
import copy
import time
from pygame.locals import *
import pdb
# import helper libraries
import ConnectFourGraphics
white = 1
brown = 2
blank = 0
#takes col and row and returns colour of token at index
#takes col and row and returns colour of token in inputted direction
def other_token(token):
if token == white:
return brown
else:
return white
class ConnectFour:
# The `__init__` method is called to initialise the `ConnectFour` game.
# It is called automatically when a call `app = ConnectFour()` is made.
def __init__(self,
set_width = 9, set_height = 8,
set_rewards = [0.000002,0.0001,1,1,1,1,1,1,1], #[0, 1, 2, 4, 8, 16, 32, 64, 128],
set_winscore = 1,
set_white_player = None,
set_brown_player = None,
set_ai_delay = 10
):
## initialise pygame
pygame.init()
pygame.font.init()
## game constants
self.field_width = set_width
self.field_height = set_height
self.rewards = set_rewards
self.score_win = set_winscore
### PLAYER SETTINGS ###
self.white_player = set_white_player
self.brown_player = set_brown_player
self.ai_delay = set_ai_delay
## state of the game (board, scoreboard, etc.)
self.field_state = [[0, 0, 0, 0, 0, 0, 0, 0], # each mini list represents a column
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]]
#same as:
self.col_heights = [0] * self.field_width
self.score_white = 0
self.score_brown = 0
self.winner = 0
self.game_running = True
self.white_turn = True
if self.white_player is None:
self.player_turn = True
else:
self.player_turn = False
## interface
self.selected_index = -1
self.DISPLAY = ConnectFourGraphics.setup_display(self.field_width, self.field_height)
## draw initial board
self.draw()
def copy(self):
newself = ConnectFour()
newself.field_state = copy.deepcopy(self.field_state)
newself.col_heights = copy.deepcopy(self.col_heights)
newself.field_width = copy.deepcopy(self.field_width)
newself.field_height = copy.deepcopy(self.field_height)
newself.white_turn = self.white_turn
return newself
def draw(self):
# A wrapper around the `ConnectFourGraphics.draw_board` function that picks all
# the right components of `self`.
ConnectFourGraphics.draw_board(self.DISPLAY,
self.field_state, self.field_width, self.field_height,
self.score_white, self.score_brown,
self.selected_index, self.game_running,
self.player_turn, self.white_turn, self.winner)
# current player attempts to insert into a column
def attempt_insert(self, col):
# is it possible to insert into this column?
if self.col_heights[col] < self.field_height:
row = self.col_heights[col] #the row is the height of the column + 1 (but as heights don't start at 0, dont need to add one)
# add a token in the selected column
if self.white_turn:
self.field_state[col][row] = 1
if self.brown_player is None: #if white is playing check if brown is an ai -- brown is an ai if self.brown_player is NOT None
self.player_turn = True #if brown is HUMAN then set player_turn to True -> can play human turn
else:
self.player_turn = False
else:
self.field_state[col][row] = 2
if self.white_player is None:
self.player_turn = True
else:
self.player_turn = False
self.col_heights[col] += 1
# who is playing next?
self.white_turn = not(self.white_turn)
# is the game over?
if self.over():
self.set_winner()
# else do nothing: this forces the player to choose again
def game_loop(self):
n_moves_w = -2 #number of moves made by white (start as -2 to ignore first two random moves!
n_moves_b = -2
time_w = 0 #total time taken by white... and brown
time_b = 0
while self.game_running:
# Let the AI play if it's its turn
if not self.player_turn:
pygame.time.wait(80)
pygame.time.delay(self.ai_delay)
if self.white_turn:
start_time = time.time()
move = self.white_player(self)
time_w += (time.time()-start_time)
n_moves_w+=1
print('White move: ', move)
else:
start_time = time.time()
move = self.brown_player(self)
time_b += (time.time()-start_time)
n_moves_b +=1
if sum(self.col_heights)>=16:
print('Brown move: ',move)
self.attempt_insert(move)
if sum(self.col_heights) >= 6:
if self.over():
self.set_winner()
# Process all events, expecially mouse events.
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit(0)
if event.type == MOUSEMOTION:
if self.player_turn:
self.selected_index = ConnectFourGraphics.cursor_check(self.field_width, self.field_height)
if event.type == MOUSEBUTTONDOWN and event.button == 1:
if self.player_turn:
self.attempt_insert(self.selected_index)
if sum(self.col_heights) >= 6:
if self.over():
self.set_winner()
# Refresh the display and loop back
self.draw()
pygame.display.update()
pygame.time.wait(40)
print('SCORES WHITE: ', self.score_white, ' BROWN: ', self.score_brown)
print('AVERAGE TIME TAKEN WHITE: ',(time_w/n_moves_w),' BROWN: ',(time_b/n_moves_b))
# Once the game is finish, simply wait for the `QUIT` event
while True:
event = pygame.event.wait()
if event.type == QUIT:
pygame.quit()
sys.exit(0)
pygame.time.wait(40)
def calculate_points(self,n_of_tokens_in_row):
return self.rewards[n_of_tokens_in_row - 2]
def score_list(self,list):
#JUST CHECKS FOR - 4 - IN A ROW
white_ = 0
brown_ = 0
for start_index in range(len(list)-4+1):
token = list[start_index+1]
if token!= 0:
items = [list[start_index],list[start_index+1], list[start_index+2],list[start_index+3]]
if all(x == token for x in items):
if token == 1:
white_+= 1
else: brown_ +=1
return (white_,brown_)
def score(self):
#ONLY gives points for - 4 - in a row!!
final_white = 0
final_brown=0
#Check rows
for row in range(self.field_height):
row_list = self.make_row_list(row)
if sum(row_list) >=4:
white1,brown1 = self.score_list(row_list)
final_white += white1
final_brown += brown1
#Check columns
for col in self.field_state:
if sum(col) >= 4:
white2,brown2 = self.score_list(col)
final_white +=white2
final_brown +=brown2
right_diagonal_start_col = [0]*self.field_height+[x for x in range(1,8)]
right_diagonal_start_row = [x for x in range(7,0,-1)]+[0]*self.field_height
right_diagonal_zipped = zip(right_diagonal_start_col,right_diagonal_start_row)
left_diagonal_start_col = [8]*self.field_height+[x for x in range(7,-1,-1)]
left_diagonal_start_row = [x for x in range(7,-1,-1)]+[0]*self.field_height
left_diagonal_zipped = zip(left_diagonal_start_col,left_diagonal_start_row)
#Check right diagonals
for coordinates in right_diagonal_zipped:
right_diagonal_list = self.make_diagonal_right_list(coordinates)
white3,brown3 = self.score_list(right_diagonal_list)
final_white+=white3
final_brown+=brown3
# coordinates is just the coordinate of each of the starting points of each diagonal
# -> the make_diagonal method takes each start point and generates a list of tokens in the diagonal
# -> the score_list method then updates white and brown scores based on pairs in the diagonal
#Check left diagonals
for coordinates in left_diagonal_zipped:
left_diagonal_list = self.make_diagonal_left_list(coordinates)
white4,brown4 = self.score_list(left_diagonal_list)
final_white+=white4
final_brown+=brown4
#returns whitescore,brownscore
return (final_white, final_brown)
'''
#count number of whites and browns
white = 0 #whites are 1
brown = 0 #browns are 2
for col in range(self.field_width):
white += field[col].count(1)
brown += field[col].count(2)
'''
#1. Do Horizontal:
# for each column for each token: check colour, if white: check if there is a white next to it: if so add 1 to number_of_tokens_in_row
# else: add check its score value and add to score_white
# make a method which returns colour of token in inputted direction
def eval(self,list):
#returns score of lists containing a potential winning row -> more points for rows containing 3s than 2s and more for 4s than 3s!
white_ = 0
brown_ = 0
for start_index in range(len(list)-4+1):
#For each row of 4 tokens -> if all tokens are white or none -> count up how many whites there are
# -> score using rewards at the top
items = [list[start_index], list[start_index + 1], list[start_index + 2], list[start_index + 3]]
if sum(items) >=2:
if all(token == white or token == blank for token in items):
whites = sum(token == white for token in items)
white_ += self.calculate_points(whites)
elif all(token == brown or token == blank for token in items) and sum(items)>=4:
browns = sum(token == brown for token in items)
brown_ += self.calculate_points(browns)
return (white_,brown_)
def evaluate_score(self):
#SCORES board using REWARDS AT THE TOP -> Can be used to give points depending on how many in a row (ie not just 4) + to evaluate how good a board is!!
final_open_white = 0
final_open_brown = 0
# Check rows
for row in range(self.field_height):
row_list = self.make_row_list(row)
if sum(row_list) >= 2:
white1, brown1 = self.eval(row_list)
final_open_white += white1
final_open_brown += brown1
# Check columns
for col in self.field_state:
if sum(col) >= 2:
white2, brown2 = self.eval(col)
final_open_white += white2
final_open_brown += brown2
right_diagonal_start_col = [0] * self.field_height + [x for x in range(1, 8)]
right_diagonal_start_row = [x for x in range(7, 0, -1)] + [0] * self.field_height
right_diagonal_zipped = zip(right_diagonal_start_col, right_diagonal_start_row)
left_diagonal_start_col = [8] * self.field_height + [x for x in range(7, -1, -1)]
left_diagonal_start_row = [x for x in range(7, -1, -1)] + [0] * self.field_height
left_diagonal_zipped = zip(left_diagonal_start_col, left_diagonal_start_row)
# Check right diagonals
for coordinates in right_diagonal_zipped:
right_diagonal_list = self.make_diagonal_right_list(coordinates)
white3, brown3 = self.eval(right_diagonal_list)
final_open_white += white3
final_open_brown += brown3
# coordinates is just the coordinate of each of the starting points of each diagonal
# -> the make_diagonal method takes each start point and generates a list of tokens in the diagonal
# -> the score_list method then updates white and browns scores based on pairs in the diagonal
# Check left diagonals
for coordinates in left_diagonal_zipped:
left_diagonal_list = self.make_diagonal_left_list(coordinates)
white4, brown4 = self.eval(left_diagonal_list)
final_open_white += white4
final_open_brown += brown4
return (final_open_white, final_open_brown)
def make_row_list(self,row):
row_list = []
for col in range(self.field_width):
row_list.append(self.field_state[col][row])
return row_list
def make_diagonal_right_list(self,coordinates):
starting_col,starting_row = coordinates
diagonal_list = []
#find diagonal
diagonal_list.append(self.field_state[starting_col][starting_row])
for offset in range(1,self.field_width):
if starting_row+offset<self.field_height and starting_col+offset<self.field_width:
diagonal_list.append(self.field_state[starting_col+offset][starting_row+offset])
return diagonal_list
def make_diagonal_left_list(self,coordinates):
starting_col,starting_row = coordinates
diagonal_list = []
diagonal_list.append(self.field_state[starting_col][starting_row])
for offset in range(1,self.field_width):
if starting_row+offset<self.field_height and starting_col-offset>=0:
diagonal_list.append(self.field_state[starting_col-offset][starting_row+offset])
return diagonal_list
def refresh_scores(self):
(white_, brown_) = self.score()
self.score_white = white_
self.score_brown = brown_
def full_check(self):
return all([ height == self.field_height for height in self.col_heights ])
# is the game finished?
# return True if that is the case otherwise return False
def winner_is(self):
#if board is full or score of white or brown is more than score needed to win:
self.refresh_scores()
if self.score_white >= 1:
return 1
elif self.score_brown >= 1:
return 2
return 0 #Either draw or game is not over yet
def set_winner(self):
if self.score_white >=1:
self.winner = 1 #white wins
elif self.score_brown >=1:
self.winner = 2 #brown wins
else: self.winner = 0
self.game_running = False
def over(self):
self.refresh_scores()
if self.full_check() or self.score_white >= 1 or self.score_brown >= 1: #if gameboard is full or a player has won...
return True
return False #not won yet