-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathConnectFour.py
180 lines (144 loc) · 6.23 KB
/
ConnectFour.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
# system libs
import argparse
import multiprocessing as mp
import tkinter as tk
# 3rd party libs
import numpy as np
# Local libs
from Player import AIPlayer, RandomPlayer, HumanPlayer
#https://stackoverflow.com/a/37737985
def turn_worker(board, send_end, p_func):
send_end.send(p_func(board))
class Game:
"""Sets up and manages a game of Connect 4 including turns and graphics.
Each turn is executed in a mp.Process to keep the main ui thread available
"""
def __init__(self, player1, player2, time):
self.players = [player1, player2]
self.colors = ['yellow', 'red']
self.current_turn = 0
self.board = np.zeros([6,7]).astype(np.uint8)
self.gui_board = []
self.game_over = False
self.ai_turn_limit = time
# very SIMPLE gui built here
#https://stackoverflow.com/a/38159672
root = tk.Tk()
root.title('Connect 4')
self.player_string = tk.Label(root, text=player1.player_string)
self.player_string.pack()
self.c = tk.Canvas(root, width=700, height=600)
self.c.pack()
for row in range(0, 700, 100):
column = []
for col in range(0, 700, 100):
column.append(self.c.create_oval(row, col, row+100, col+100, fill=''))
self.gui_board.append(column)
tk.Button(root, text='Next Move', command=self.make_move).pack()
root.mainloop()
def make_move(self):
"""Moves the game forward a single move."""
if not self.game_over:
current_player = self.players[self.current_turn]
if current_player.type == 'ai':
if self.players[int(not self.current_turn)].type == 'random':
p_func = current_player.get_expectimax_move
else:
p_func = current_player.get_alpha_beta_move
try:
recv_end, send_end = mp.Pipe(False)
p = mp.Process(target=turn_worker, args=(self.board, send_end, p_func))
p.start()
if p.join(self.ai_turn_limit) is None and p.is_alive():
p.terminate()
raise Exception('Player Exceeded time limit')
except Exception as e:
uh_oh = 'Uh oh.... something is wrong with Player {}'
print(uh_oh.format(current_player.player_number))
print(e)
raise Exception('Game Over')
move = recv_end.recv()
else:
move = current_player.get_move(self.board)
if move is not None:
self.update_board(int(move), current_player.player_number)
if self.game_completed(current_player.player_number):
self.game_over = True
self.player_string.configure(text=self.players[self.current_turn].player_string + ' wins!')
else:
self.current_turn = int(not self.current_turn)
self.player_string.configure(text=self.players[self.current_turn].player_string)
def update_board(self, move, player_num):
"""Updates the board UI to reflect player_num's move at column move"""
if 0 in self.board[:,move]:
update_row = -1
for row in range(1, self.board.shape[0]):
update_row = -1
if self.board[row, move] > 0 and self.board[row-1, move] == 0:
update_row = row-1
elif row==self.board.shape[0]-1 and self.board[row, move] == 0:
update_row = row
if update_row >= 0:
self.board[update_row, move] = player_num
self.c.itemconfig(self.gui_board[move][update_row],
fill=self.colors[self.current_turn])
break
else:
err = 'Invalid move by player {}. Column {}'.format(player_num, move)
raise Exception(err)
def game_completed(self, player_num):
"""Returns True if player_num is in a winning position on the gameboard"""
player_win_str = '{0}{0}{0}{0}'.format(player_num)
board = self.board
to_str = lambda a: ''.join(a.astype(str))
def check_horizontal(b):
for row in b:
if player_win_str in to_str(row):
return True
return False
def check_verticle(b):
return check_horizontal(b.T)
def check_diagonal(b):
for op in [None, np.fliplr]:
op_board = op(b) if op else b
root_diag = np.diagonal(op_board, offset=0).astype(np.int)
if player_win_str in to_str(root_diag):
return True
for i in range(1, b.shape[1]-3):
for offset in [i, -i]:
diag = np.diagonal(op_board, offset=offset)
diag = to_str(diag.astype(np.int))
if player_win_str in diag:
return True
return False
return (check_horizontal(board) or
check_verticle(board) or
check_diagonal(board))
def main(player1, player2, time):
"""
Creates player objects based on the string paramters that are passed
to it and calls play_game()
INPUTS:
player1 - a string ['ai', 'random', 'human']
player2 - a string ['ai', 'random', 'human']
"""
def make_player(name, num):
if name=='ai':
return AIPlayer(num)
elif name=='random':
return RandomPlayer(num)
elif name=='human':
return HumanPlayer(num)
Game(make_player(player1, 1), make_player(player2, 2), time)
# entrance point parses arguments from cli
if __name__=='__main__':
player_types = ['ai', 'random', 'human']
parser = argparse.ArgumentParser()
parser.add_argument('player1', choices=player_types)
parser.add_argument('player2', choices=player_types)
parser.add_argument('--time',
type=int,
default=60,
help='Time to wait for a move in seconds (int)')
args = parser.parse_args()
main(args.player1, args.player2, args.time)