Skip to content

Instantly share code, notes, and snippets.

@ballgoesvroomvroom
Last active June 12, 2022 05:24
Show Gist options
  • Save ballgoesvroomvroom/9aa66d773856e99647a4d019932f2eb4 to your computer and use it in GitHub Desktop.
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
## 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