-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathconnect4.py
215 lines (185 loc) · 7.63 KB
/
connect4.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
import random
from typing import List, Dict
import math
ROW_COUNT = 6 # used to represent number of rows in a board
COL_COUNT = 7 # used to represent number of columns in a board
EMPTY = '*' # used to represent an empty part of the board
board_piece = {EMPTY: ':blue_circle: ', 'R': ':red_circle: ',
'Y': ':yellow_circle: '}
PLAYER_PIECE = 'R'
AI_PIECE = 'Y'
class Board:
"""The board for the connect 4 game
"""
# === Private Attributes ===
# _board:
# The array representing the game board.
_board: List[List[str]]
# Methods
def __init__(self) -> None:
"""Initialize a Board with empty (blue) circles.
self._arr is an empty 7x6 board.
"""
arr = [[EMPTY] * 7 for _ in range(6)]
self._board = arr
def print_board(self) -> str:
"""Used to print the board with blue circles as empty areas,
red circles as player 1 moves, and yellow circles as player 2 moves"""
msg = ''
for row in self._board:
for item in row:
msg += board_piece[item]
msg += '\n'
return msg
def is_valid_location(self, r: int, c: int):
"""Check if the section of the board is not already filled"""
if self._board[r][c] == EMPTY:
return True
return False
def drop_piece(self, r: int, c: int, piece: str):
"""Drop piece of one of the players into the board"""
self._board[r][c] = piece
# for piece can use i from other function
def is_win(self, piece: str) -> bool:
"""Checks if there are any connect fours"""
for c in range(COL_COUNT):
for r in range(ROW_COUNT):
# Horizontal Wins
if c < COL_COUNT - 3:
if (self._board[r][c] == self._board[r][c + 1] == self.
_board[r][c + 2] == self._board[r][c + 3] == piece):
return True
# Vertical Wins
if r < ROW_COUNT - 3:
if self._board[r][c] == self._board[r + 1][c] == \
self._board[r + 2][c] == \
self._board[r + 3][c] == piece:
return True
# Positive Diagonal Wins
if c < COL_COUNT - 3 and r < ROW_COUNT - 3:
if self._board[r][c] == self._board[r + 1][c + 1] == \
self._board[r + 2][c + 2] == \
self._board[r + 3][c + 3] == piece:
return True
# Negative Diagonal Wins
if c < COL_COUNT - 3 and 3 <= r < ROW_COUNT:
if self._board[r][c] == self._board[r - 1][c + 1] == \
self._board[r - 2][c + 2] == \
self._board[r - 3][c + 3] == piece:
return True
return False
def score_position(self, piece: str):
"""
Return the Score of the position.
"""
score = 0
# Score center column
center_list = [self._board[r][COL_COUNT // 2] for r in range(ROW_COUNT)]
center_count = center_list.count(piece)
score += center_count * 3
# Score Horizontal
for r in range(ROW_COUNT):
row = self._board[r]
for c in range(COL_COUNT - 3):
section = row[c:c + 4]
score += evaluate_section(section, piece)
# Score Vertical
for c in range(COL_COUNT):
col = [self._board[r][c] for r in range(ROW_COUNT)]
for r in range(ROW_COUNT - 3):
section = col[r:r + 4]
score += evaluate_section(section, piece)
# Score Positive Diagonal
for r in range(ROW_COUNT - 3):
for c in range(COL_COUNT - 3):
section = [self._board[r + i][c + i] for i in range(4)]
score += evaluate_section(section, piece)
# Score Negative Diagonal
for r in range(ROW_COUNT - 3):
for c in range(COL_COUNT - 3):
section = [self._board[r + 3 - i][c + i] for i in range(4)]
score += evaluate_section(section, piece)
return score
def is_terminal_node(self) -> bool:
"""
Return if the game is finished or if there are no valid locations left.
"""
return self.is_win(PLAYER_PIECE) or \
self.is_win(AI_PIECE) or (len(self.get_valid_locations()) == 0)
def get_valid_locations(self) -> Dict[int, int]:
"""
Return a dict with key as column, and value as row, representing places
the piece can be dropped into.
"""
valid_locations = {}
for c in range(COL_COUNT):
for r in range(ROW_COUNT):
if self.is_valid_location(r, c):
valid_locations[c] = r
return valid_locations
def minimax(self, depth, alpha, beta, maximizing_player) -> tuple:
valid_locations = self.get_valid_locations()
is_terminal = self.is_terminal_node()
if depth == 0 or is_terminal:
if is_terminal:
if self.is_win(AI_PIECE):
return None, 100000000000000
elif self.is_win(PLAYER_PIECE):
return None, -10000000000000
else: # Game is over, no more valid moves
return None, 0
else: # Depth is zero
return None, self.score_position(AI_PIECE)
if maximizing_player: # Maximizing player (for bot turn)
value = -math.inf
column = random.choice(list(valid_locations.keys()))
for col in valid_locations:
row = valid_locations[col]
temp_board = Board()
temp_board._board = [sublist.copy() for sublist in self._board]
temp_board.drop_piece(row, col, AI_PIECE)
new_score = temp_board.minimax(depth - 1, alpha, beta, False)[1]
if new_score > value:
value = new_score
column = col
alpha = max(alpha, value)
if alpha >= beta:
break
return column, value
else: # Minimizing player (for player turn)
value = math.inf
column = random.choice(list(valid_locations.keys()))
for col in valid_locations:
row = valid_locations[col]
temp_board = Board()
temp_board._board = [sublist.copy() for sublist in self._board]
temp_board.drop_piece(row, col, PLAYER_PIECE)
new_score = temp_board.minimax(depth - 1, alpha, beta, True)[1]
if new_score < value:
value = new_score
column = col
beta = min(beta, value)
if alpha >= beta:
break
return column, value
# Helper
def evaluate_section(section: list, piece: str) -> int:
"""Evaluates the score of a specific section on the board based on
how close the section is to giving a connect 4"""
score = 0
opp_piece = PLAYER_PIECE
if piece == PLAYER_PIECE:
opp_piece = AI_PIECE
if section.count(piece) == 4:
score += 10000
elif section.count(piece) == 3 and section.count(EMPTY) == 1:
score += 10
elif section.count(piece) == 2 and section.count(EMPTY) == 2:
score += 5
if section.count(opp_piece) == 3 and section.count(EMPTY) == 1:
score -= 8
elif section.count(opp_piece) == 4:
score -= 8000
return score
if __name__ == '__main__':
pass