Last active
June 12, 2022 05:24
-
-
Save ballgoesvroomvroom/9aa66d773856e99647a4d019932f2eb4 to your computer and use it in GitHub Desktop.
Connect 4 within a single file, "4" is just a variable, can be "Connect 5" too
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## Connect 4 | |
import os | |
import re | |
import time | |
class Screen: | |
""" | |
Uses re module | |
""" | |
def clear(): | |
os.system("clear") | |
def out(*args, sep=" ", clear=False): | |
o = "" | |
for a in args: | |
o += sep +str(a) | |
if clear: Screen.clear() | |
print(o[len(sep):]) ## remove leading seperator | |
def inp(prompt="", pattern="^.*$"): | |
## with built-in input validation | |
pattern = re.compile(pattern) | |
patternMatch = None | |
while patternMatch == None: | |
inp = input(prompt) | |
patternMatch = pattern.match(inp) | |
return inp, patternMatch.groups() | |
def render(sequence, frame_rate=0.2): | |
## renders the sequence with each frames lasting for frame_rate | |
for frame in sequence: | |
Screen.out(frame, clear=True) | |
time.sleep(frame_rate) | |
class Board: | |
def __init__(self, width, height, playerSprites=["O", "X"]): | |
## reference by [y][x] | |
## occupied by 0 means its a free cell | |
## 1 - player 1; 2 - player 2 occupied it | |
self.data = [[0 for x in range(width)] for y in range(height)] ## store cell data | |
self.width = width | |
self.height = height | |
self.floor = height -1 ## last index | |
self.winningCond = 4 ## 4 in a row | |
self.playerAmt = len(playerSprites) | |
self.playerSprites = playerSprites ## player 1 takes "O", player 2 takese "X" | |
def generateSequence(self, playerId, x, floor): | |
## for animation-like movement | |
## floor detection is not handled here, trust this source | |
## create a new ghost class to generate the sequence | |
b = Board(self.width, self.height, self.playerSprites) ## pass in playerSprites so it uses the same sprites | |
b.data = [[x for x in y] for y in self.data] ## copy data so as to no modify actual board | |
sequence = [] | |
for i in range(floor): | |
## floor is the value (0 at the top) on the y-axis | |
## floor determines where game piece will stop dropping | |
## unset previous iteration if any | |
if i > 0: | |
b.data[i -1][x] = 0 ## make it empty again | |
b.data[i][x] = playerId | |
sequence.append(str(b)) | |
## let b get gc'ed | |
return sequence | |
def _findAdjacentCells(self, playerId, x, y): | |
## takes the coordinates and find for self.winningCond consecutive | |
## cells occupied by playerId (an integer) | |
## handles boundary issues | |
## initial position represented by an i | |
## BEHAVIOR: CHECKS FOR cell[y][x] TOO | |
## check for horizontal lines first | |
## direction: left <-- i | |
for a in range(x, x -self.winningCond, -1): | |
if a < 0: | |
## out of bounds | |
break | |
cell = self.data[y][a] | |
if cell != playerId: | |
break | |
elif a == x -self.winningCond +1: | |
## hit winning condition | |
return True | |
## direction: i --> right | |
for a in range(x, x +self.winningCond): | |
if a >= self.width: | |
## out of bounds | |
break | |
cell = self.data[y][a] | |
if cell != playerId: | |
break | |
elif a == x +self.winningCond -1: | |
## hit winning condition | |
return True | |
## vertical | |
## direction: moving up | |
for b in range(y, y -self.winningCond, -1): | |
if b < 0: | |
## out of bounds | |
break | |
cell = self.data[b][x] | |
if cell != playerId: | |
break | |
elif b == y -self.winningCond +1: | |
return True | |
## direction: moving down | |
for b in range(y, y +self.winningCond): | |
if b >= self.height: | |
## out of bounds | |
break | |
cell = self.data[b][x] | |
if cell != playerId: | |
break | |
elif b == y +self.winningCond -1: | |
return True | |
## diagonals | |
## moving north-east | |
for s in range(self.winningCond): | |
a, b = x +s, y -s ## a, b being the new x, y coordinates respectively | |
if a >= self.width or b < 0: | |
## out of bounds | |
break | |
cell = self.data[b][a] | |
if cell != playerId: | |
break | |
elif s == self.winningCond -1: | |
## winning cond hit | |
return True | |
## moving south-east | |
for s in range(self.winningCond): | |
a, b = x +s, y +s | |
if a >= self.width or b >= self.height: | |
## out of bounds | |
break | |
cell = self.data[b][a] | |
if cell != playerId: | |
break | |
elif s == self.winningCond -1: | |
return True | |
## moving south-west | |
for s in range(self.winningCond): | |
a, b = x -s, y +s | |
if a < 0 or b >= self.height: | |
## out of bounds | |
break | |
cell = self.data[b][a] | |
if cell != playerId: | |
break | |
elif s == self.winningCond -1: | |
return True | |
## moving north-west | |
for s in range(self.winningCond): | |
a, b = x -s, y -s | |
if a < 0 or b < 0: | |
## out of bounds | |
break | |
cell = self.data[b][a] | |
if cell != playerId: | |
break | |
elif s == self.winningCond -1: | |
return True | |
## nothing hit, return False | |
return False | |
def check(self): | |
## check if any 4 in a rows are detected | |
## returns None if no winning condition is detected | |
## returns playerId if winning condition is detected | |
for playerId in range(1, self.playerAmt +1): | |
## find cell occupied by this playerSprite | |
for y in range(self.height): | |
row = self.data[y] | |
for x in range(self.width): | |
cell = row[x] | |
if cell == playerId: | |
hitWinningCond = self._findAdjacentCells(playerId, x, y) | |
if hitWinningCond: | |
return playerId | |
return None | |
def play(self, playerId, column): | |
## returns False is play move is invalid | |
## column being 0 to (self.width -1) (the x value) | |
## left --> right | |
## playerId (1 - self.playerAmt; inclusive), should be verified by calling function (GameModel) | |
## find floor | |
for y in range(self.height): | |
if self.data[y][column] != 0: | |
## hit a game piece | |
break | |
elif y == self.height -1: | |
y = self.height ## offset it by +1 since nothing hit | |
floor = y -1 | |
if floor < 0: | |
## column is occupied | |
return False | |
## generate frames | |
sequence = self.generateSequence(playerId, column, floor) | |
## play frames | |
Screen.render(sequence, .2) | |
## occupy actual slot | |
self.data[floor][column] = playerId | |
return True | |
def __str__(self): | |
## constants | |
CELL_WIDTH = 3 | |
EMPTY = " " | |
PLAYERS = self.playerSprites | |
## starting | |
header = ("|" +" " *CELL_WIDTH) *self.width +"|" | |
entrance_separator = ("-" +" " *CELL_WIDTH) *self.width +"-" | |
separator = "-" *(self.width *(CELL_WIDTH +1) +1) | |
## build column | |
board = "" | |
for y in range(self.height): | |
for x in range(self.width): | |
cell = self.data[y][x] ## represents 0 - 2, empty and player id | |
replacement = EMPTY | |
if cell > 0: | |
replacement = PLAYERS[cell -1] | |
board += "|{1:^{0}}".format(CELL_WIDTH, replacement) ## centre align it | |
board += "| {}\n{}\n".format(y, separator) ## close the border and add the next line separator | |
## build last line (x-axis) | |
axis = "" | |
for x in range(self.width): | |
axis += " {1:^{0}}".format(CELL_WIDTH, x) | |
return "{}\n{}\n{}{}".format(header, entrance_separator, board, axis) | |
class GameModel: | |
def __init__(self): | |
## data | |
self.board_width = 0 | |
self.board_height = 0 | |
self.playerAmt = 2 | |
self.playerSprites = ["O", "X"] ## default; overridden in .initialise() | |
self.winningCond = 4 ## score 4 in a row to win | |
## memory | |
self.instances = {} | |
## states | |
self.alreadyInitialised = False | |
self.isrunning = False | |
def initialise(self): | |
## custom initialise function; initialisation handled by Game object | |
if self.alreadyInitialised: | |
## catch (already initialised) | |
return | |
else: | |
self.alreadyInitialised = True | |
## ask for game settings | |
Screen.out("[ Game settings ]") | |
width = Screen.inp("Board's width: ", "^\d+$")[0] | |
height = Screen.inp("Board's height: ", "^\d+$")[0] | |
playerAmt = int(Screen.inp("Amount of players: ", "^([2-9]|[1-9]\d+)$")[0]) ## accept positive integers from 2 (inclusive) onwards only (non-zero not included) | |
## generate sprites | |
sprites = [] | |
Screen.out("\n[ Players' sprite ]\n(single character only)") | |
for i in range(playerAmt): | |
s = Screen.inp("Sprite for player {}: ".format(i +1), "^\w$")[0] | |
sprites.append(s) | |
self.playerAmt = playerAmt | |
self.playerSprites = sprites | |
self.instances["board"] = Board(int(width), int(height), self.playerSprites) | |
def start(self): | |
## run the game | |
self.isrunning = True | |
## reference board | |
board = self.instances["board"] | |
## game states | |
playerTurn = 1 ## player 1's turn | |
turnCount = 0 ## only start checking when turnCount >= self.PLAYERS_AMT *self.winningCond -1 | |
## show board first | |
Screen.out(board, clear=True) | |
while self.isrunning: | |
Screen.out("Player {}'s turn'".format(playerTurn)) | |
turnCount += 1 ## increment turnCount | |
valid_cond = False | |
while not valid_cond: | |
input_col = int(Screen.inp("Column to play: ", "^\d+$")[0]) | |
if input_col < 0 or input_col >= board.width: | |
Screen.out("Column out of range") | |
else: | |
## try playing | |
success = board.play(playerTurn, input_col) | |
if not success: | |
## return false by .play() because selected column is full | |
Screen.out("Column is full") | |
else: | |
valid_cond = True | |
Screen.out(board, clear=True) | |
## check for winning cond | |
if turnCount >= self.playerAmt *self.winningCond -1: | |
playerId = board.check() ## returns playerId if hit; else None | |
if playerId: | |
Screen.out("[ WINNER ]: player {}".format(playerId)) | |
return | |
## move on to the next player | |
playerTurn += 1 | |
if playerTurn > self.playerAmt: | |
playerTurn = 1 | |
if __name__ == "__main__": | |
g_obj = GameModel() | |
g_obj.initialise() | |
g_obj.start() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment