forked from ioistired/connect4-bot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
game.py
198 lines (156 loc) · 5.16 KB
/
game.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
#!/usr/bin/env python3
# encoding: utf-8
# © 2017 Benjamin Mintz <[email protected]>
# MIT Licensed
from typing import Union
from itertools import groupby, chain
class Board(list):
__slots__ = frozenset({'width', 'height'})
def __init__(self, width, height, player1_name=None, player2_name=None):
self.width = width
self.height = height
for x in range(width):
self.append([0] * height)
def __getitem__(self, pos: Union[int, tuple]):
if isinstance(pos, int):
return list(self)[pos]
elif isinstance(pos, tuple):
x, y = pos
return list(self)[x][y]
else:
raise TypeError('pos must be an int or tuple')
def __setitem__(self, pos: Union[int, tuple], new_value):
x, y = self._xy(pos)
if self[x, y] != 0:
raise IndexError("there's already a move at that position")
# basically self[x][y] = new_value
# super().__getitem__(x).__setitem__(y, new_value)
self[x][y] = new_value
def _xy(self, pos):
if isinstance(pos, tuple):
return pos[0], pos[1]
elif isinstance(pos, int):
x = pos
return x, self._y(x)
else:
raise TypeError('pos must be an int or tuple')
def _y(self, x):
"""find the lowest empty row for column x"""
# start from the bottom and work up
for y in range(self.height-1, -1, -1):
if self[x, y] == 0:
return y
raise ValueError('that column is full')
def _pos_diagonals(self):
"""Get positive diagonals, going from bottom-left to top-right."""
for di in ([(j, i - j) for j in range(self.width)] for i in range(self.width + self.height - 1)):
yield [self[i, j] for i, j in di if i >= 0 and j >= 0 and i < self.width and j < self.height]
def _neg_diagonals(self):
"""Get negative diagonals, going from top-left to bottom-right."""
for di in ([(j, i - self.width + j + 1) for j in range(self.height)] for i in range(self.width + self.height - 1)):
yield [self[i, j] for i, j in di if i >= 0 and j >= 0 and i < self.width and j < self.height]
def _full(self):
"""is there a move in every position?"""
for x in range(self.width):
if self[x, 0] == 0:
return False
return True
class Connect4Game:
__slots__ = frozenset({'board', 'turn_count', '_whomst_forfeited', 'names'})
FORFEIT = -2
TIE = -1
NO_WINNER = 0
PIECES = (
'\N{medium white circle}'
'\N{large red circle}'
'\N{large blue circle}'
)
def __init__(self, player1_name=None, player2_name=None):
if player1_name is not None and player2_name is not None:
self.names = (player1_name, player2_name)
else:
self.names = ('Player 1', 'Player 2')
self.board = Board(7, 6)
self.turn_count = 0
self._whomst_forfeited = 0
def move(self, column):
self.board[column] = self.whomst_turn()
self.turn_count += 1
def forfeit(self):
"""forfeit the game as the current player"""
self._whomst_forfeited = self.whomst_turn_name()
def _get_forfeit_status(self):
if self._whomst_forfeited:
status = '{} won ({} forfeited)\n'
return status.format(
self.other_player_name(),
self.whomst_turn_name()
)
raise ValueError('nobody has forfeited')
def __str__(self):
win_status = self.whomst_won()
status = self._get_status()
instructions = ''
if win_status == self.NO_WINNER:
instructions = self._get_instructions()
elif win_status == self.FORFEIT:
status = self._get_forfeit_status()
return (
status
+ instructions
+ '\n'.join(self._format_row(y) for y in range(self.board.height))
)
def _get_status(self):
win_status = self.whomst_won()
if win_status == self.NO_WINNER:
status = (self.whomst_turn_name() + "'s turn"
+ self.PIECES[self.whomst_turn()])
elif win_status == self.TIE:
status = "It's a tie!"
elif win_status == self.FORFEIT:
status = self._get_forfeit_status()
else:
status = self._get_player_name(win_status) + ' won!'
return status + '\n'
def _get_instructions(self):
instructions = ''
for i in range(1, self.board.width+1):
instructions += str(i) + '\N{combining enclosing keycap}'
return instructions + '\n'
def _format_row(self, y):
return ''.join(self[x, y] for x in range(self.board.width))
def __getitem__(self, pos):
x, y = pos
return self.PIECES[self.board[x, y]]
def whomst_won(self):
"""Get the winner on the current board.
If there's no winner yet, return Connect4Game.NO_WINNER.
If it's a tie, return Connect4Game.TIE"""
lines = (
self.board, # columns
zip(*self.board), # rows (zip picks the nth item from each column)
self.board._pos_diagonals(), # positive diagonals
self.board._neg_diagonals(), # negative diagonals
)
if self._whomst_forfeited:
return self.FORFEIT
for line in chain(*lines):
for player, group in groupby(line):
if player != 0 and len(list(group)) >= 4:
return player
if self.board._full():
return self.TIE
else:
return self.NO_WINNER
def other_player_name(self):
self.turn_count += 1
other_player_name = self.whomst_turn_name()
self.turn_count -= 1
return other_player_name
def whomst_turn_name(self):
return self._get_player_name(self.whomst_turn())
def whomst_turn(self):
return self.turn_count%2+1
def _get_player_name(self, player_number):
player_number -= 1 # these lists are 0-indexed but the players aren't
return self.names[player_number]