# python - Connect 4 Terminal Game

I developed a python Connect 4 game that only uses sys & numpy everything else is regular python! All I'm requesting is a stronger code. This is what I made:

import sys
import numpy

BOARD_SIZE_X = 7
BOARD_SIZE_Y = 6
SEARCH_DEPTH = 4

COMPUTER_PLAYER = 1
HUMAN_PLAYER = -1

#
# Method that runs the minimax algorithm and returns
# the move and score of each call.
#

def minimax(gameState, depth, player, opponent):
availableMoves = BOARD_SIZE_X
for i in range(0, BOARD_SIZE_X):
if gameState[0][i] != 0:
availableMoves -= 1

if depth == 0 or availableMoves == 0:
score = evaluateScore(gameState, player, opponent)
return None, score

bestScore = None
bestMove = None

for i in range(0, BOARD_SIZE_X):
# If moves cannot be made on column, skip it
if gameState[0][i] != 0:
continue

currentMove = [0, i]

for j in range(0, BOARD_SIZE_Y - 1):
if gameState[j + 1][i] != 0:
gameState[j][i] = player
currentMove[0] = j
break
elif j == BOARD_SIZE_Y - 2:
gameState[j+1][i] = player
currentMove[0] = j+1

# Recursive minimax call, with reduced depth
move, score = minimax(gameState, depth - 1, opponent, player)

gameState[currentMove[0]][currentMove[1]] = 0

if player == COMPUTER_PLAYER:
if bestScore == None or score > bestScore:
bestScore = score
bestMove = currentMove
else:
if bestScore == None or score < bestScore:
bestScore = score
bestMove = currentMove

return bestMove, bestScore

#
# Method that calculates the heuristic value of a given
# board state. The heuristic adds a point to a player
# for each empty slot that could grant a player victory.
#

def evaluateScore(gameState, player, opponent):
# Return infinity if a player has won in the given board
score = checkWin(gameState)

if score == player:
return float("inf")
elif score == opponent:
return float("-inf")
else:
score = 0

for i in range(0, BOARD_SIZE_Y):
for j in range(0, BOARD_SIZE_X):
if gameState[i][j] == 0:
score += scoreOfCoordinate(gameState, i, j, player, opponent)

return score

#
# Method that evaluates if a given coordinate has a possible win
# for any player. Each coordinate evaluates if a possible win can be
# found vertically, horizontally or in both diagonals.
#

def scoreOfCoordinate(gameState, i, j, player, opponent):
score = 0

# Check vertical line
score += scoreOfLine(
gameState=gameState,
i=i,
j=j,
rowIncrement=-1,
columnIncrement=0,
firstRowCondition=-1,
secondRowCondition=BOARD_SIZE_Y,
firstColumnCondition=None,
secondColumnCondition=None,
player=player,
opponent=opponent
)

# Check horizontal line
score += scoreOfLine(
gameState=gameState,
i=i,
j=j,
rowIncrement=0,
columnIncrement=-1,
firstRowCondition=None,
secondRowCondition=None,
firstColumnCondition=-1,
secondColumnCondition=BOARD_SIZE_X,
player=player,
opponent=opponent
)

# Check diagonal /
score += scoreOfLine(
gameState=gameState,
i=i,
j=j,
rowIncrement=-1,
columnIncrement=1,
firstRowCondition=-1,
secondRowCondition=BOARD_SIZE_Y,
firstColumnCondition=BOARD_SIZE_X,
secondColumnCondition=-1,
player=player,
opponent=opponent
)

# Check diagonal \
score += scoreOfLine(
gameState=gameState,
i=i,
j=j,
rowIncrement=-1,
columnIncrement=-1,
firstRowCondition=-1,
secondRowCondition=BOARD_SIZE_Y,
firstColumnCondition=-1,
secondColumnCondition=BOARD_SIZE_X,
player=player,
opponent=opponent
)

return score

#
# Method that searches through a line (vertical, horizontal or
# diagonal) to get the heuristic value of the given coordinate.
#

def scoreOfLine(
gameState,
i,
j,
rowIncrement,
columnIncrement,
firstRowCondition,
secondRowCondition,
firstColumnCondition,
secondColumnCondition,
player,
opponent
):
score = 0
currentInLine = 0
valsInARow = 0
valsInARowPrev = 0

# Iterate in one side of the line until a move from another
# player or an empty space is found
row = i + rowIncrement
column = j + columnIncrement
firstLoop = True
while (
row != firstRowCondition and
column != firstColumnCondition and
gameState[row][column] != 0
):
if firstLoop:
currentInLine = gameState[row][column]
firstLoop = False
if currentInLine == gameState[row][column]:
valsInARow += 1
else:
break
row += rowIncrement
column += columnIncrement

# Iterate on second side of the line
row = i - rowIncrement
column = j - columnIncrement
firstLoop = True
while (
row != secondRowCondition and
column != secondColumnCondition and
gameState[row][column] != 0
):
if firstLoop:
firstLoop = False

# Verify if previous side of line guaranteed a win on the
# coordinate, and if not, continue counting to see if the
# given coordinate can complete a line from in between.
if currentInLine != gameState[row][column]:
if valsInARow == 3 and currentInLine == player:
score += 1
elif valsInARow == 3 and currentInLine == opponent:
score -= 1
else:
valsInARowPrev = valsInARow

valsInARow = 0
currentInLine = gameState[row][column]

if currentInLine == gameState[row][column]:
valsInARow += 1
else:
break
row -= rowIncrement
column -= columnIncrement

if valsInARow + valsInARowPrev >= 3 and currentInLine == player:
score += 1
elif valsInARow + valsInARowPrev >= 3 and currentInLine == opponent:
score -= 1

return score

#
# Method that executes the first call of the minimax method and
# returns the move to be executed by the computer. It also verifies
# if any immediate wins or loses are present.
#

def bestMove(gameState, player, opponent):
for i in range(0, BOARD_SIZE_X):
# If moves cannot be made on column, skip it
if gameState[0][i] != 0:
continue

currentMove = [0, i]

for j in range(0, BOARD_SIZE_Y - 1):
if gameState[j + 1][i] != 0:
gameState[j][i] = player
currentMove[0] = j
break
elif j == BOARD_SIZE_Y - 2:
gameState[j+1][i] = player
currentMove[0] = j+1

winner = checkWin(gameState)
gameState[currentMove[0]][currentMove[1]] = 0

if winner == COMPUTER_PLAYER:
return currentMove[1]

for i in range(0, BOARD_SIZE_X):
# If moves cannot be made on column, skip it
if gameState[0][i] != 0:
continue

currentMove = [0, i]

for j in range(0, BOARD_SIZE_Y - 1):
if gameState[j + 1][i] != 0:
gameState[j][i] = opponent
currentMove[0] = j
break
elif j == BOARD_SIZE_Y - 2:
gameState[j+1][i] = opponent
currentMove[0] = j+1

winner = checkWin(gameState)
gameState[currentMove[0]][currentMove[1]] = 0

if winner == HUMAN_PLAYER:
return currentMove[1]

move, score = minimax(gameState, SEARCH_DEPTH, player, opponent)
return move[1]

#
# Method that verifies if the current board is in a winning state
# for any player, returning infinity if that is the case.
#

def checkWin(gameState):
current = 0
currentCount = 0
computer_wins = 0
opponent_wins = 0

# Check horizontal wins
for i in range(0, BOARD_SIZE_Y):
for j in range(0, BOARD_SIZE_X):
if currentCount == 0:
if gameState[i][j] != 0:
current = gameState[i][j]
currentCount += 1
elif currentCount == 4:
if current == COMPUTER_PLAYER:
computer_wins += 1
else:
opponent_wins += 1
currentCount = 0
break
elif gameState[i][j] != current:
if gameState[i][j] != 0:
current = gameState[i][j]
currentCount = 1
else:
current = 0
currentCount = 0
else:
currentCount += 1

if currentCount == 4:
if current == COMPUTER_PLAYER:
computer_wins += 1
else:
opponent_wins += 1
current = 0
currentCount = 0

# Check vertical wins
for j in range(0, BOARD_SIZE_X):
for i in range(0, BOARD_SIZE_Y):
if currentCount == 0:
if gameState[i][j] != 0:
current = gameState[i][j]
currentCount += 1
elif currentCount == 4:
if current == COMPUTER_PLAYER:
computer_wins += 1
else:
opponent_wins += 1
currentCount = 0
break
elif gameState[i][j] != current:
if gameState[i][j] != 0:
current = gameState[i][j]
currentCount = 1
else:
current = 0
currentCount = 0
else:
currentCount += 1

if currentCount == 4:
if current == COMPUTER_PLAYER:
computer_wins += 1
else:
opponent_wins += 1
current = 0
currentCount = 0

# Check diagonal wins
np_matrix = numpy.array(gameState)
diags = [np_matrix[::-1,:].diagonal(i) for i in range(-np_matrix.shape[0]+1,np_matrix.shape[1])]
diags.extend(np_matrix.diagonal(i) for i in range(np_matrix.shape[1]-1,-np_matrix.shape[0],-1))
diags_list = [n.tolist() for n in diags]

for i in range(0, len(diags_list)):
if len(diags_list[i]) >= 4:
for j in range(0, len(diags_list[i])):
if currentCount == 0:
if diags_list[i][j] != 0:
current = diags_list[i][j]
currentCount += 1
elif currentCount == 4:
if current == COMPUTER_PLAYER:
computer_wins += 1
else:
opponent_wins += 1
currentCount = 0
break
elif diags_list[i][j] != current:
if diags_list[i][j] != 0:
current = diags_list[i][j]
currentCount = 1
else:
current = 0
currentCount = 0
else:
currentCount += 1

if currentCount == 4:
if current == COMPUTER_PLAYER:
computer_wins += 1
else:
opponent_wins += 1
current = 0
currentCount = 0

if opponent_wins > 0:
return HUMAN_PLAYER
elif computer_wins > 0:
return COMPUTER_PLAYER
else:
return 0

#
# Function that prints the game board, representing the player
# as a O and the computer as an X
#

def printBoard(gameState):
for i in range(1, BOARD_SIZE_X + 1):
sys.stdout.write(" %d " % i)

print("")
print("_" * (BOARD_SIZE_X * 3))
for i in range(0, BOARD_SIZE_Y):
for j in range(0, BOARD_SIZE_X):

if gameState[i][j] == 1:
sys.stdout.write("|X|")
elif gameState[i][j] == -1:
sys.stdout.write("|O|")
else:
sys.stdout.write("|-|")

print("")

print("_" * (BOARD_SIZE_X * 3))
print("")

#
# Method that provides the main flow of the game, prompting the user
# to make moves, and then allowing the computer to execute a move.
# After each turn, the method checks if the board is full or if a player
# has won.
#

def playGame():
gameState = [[0 for col in range(BOARD_SIZE_X)] for row in range(BOARD_SIZE_Y)]
moveHeights = [0] * BOARD_SIZE_X
player = COMPUTER_PLAYER
opponent = HUMAN_PLAYER
winner = 0
gameOver = False
remainingColumns = BOARD_SIZE_X
print("=========================")
print("= WELCOME TO CONNECT 4! =")
print("=========================\n")
printBoard(gameState)

while True:

while True:
try:
move = int(input("What is your move? (Choose from 1 to %d): " % BOARD_SIZE_X))
except ValueError:
print("That wasn't a number! Try again.")
continue
if move < 1 or move > BOARD_SIZE_X:
print("That is not a valid move. Try again.")
elif moveHeights[move - 1] == BOARD_SIZE_Y:
print("The chosen column is already full. Try again.")
else:
break

moveHeights[move - 1] += 1
gameState[BOARD_SIZE_Y - moveHeights[move - 1]][move - 1] = opponent
printBoard(gameState)

if moveHeights[move - 1] == BOARD_SIZE_Y:
remainingColumns -= 1
if remainingColumns == 0:
gameOver = True
if gameOver:
break

score = checkWin(gameState)
if score == player:
winner = player
break
elif score == opponent:
winner = opponent
break
else:
score = 0

print("Now it's the computer's turn!")
move = bestMove(gameState, player, opponent)
if move == None:
break

moveHeights[move] += 1
gameState[BOARD_SIZE_Y - moveHeights[move]][move] = player
printBoard(gameState)

if moveHeights[move] == BOARD_SIZE_Y:
remainingColumns -= 1
if remainingColumns == 0:
gameOver = True
if gameOver:
break

score = checkWin(gameState)
if score == player:
winner = player
break
elif score == opponent:
winner = opponent
break
else:
score = 0

return winner

#
# Main execution of the game. Plays the game until the user
# wishes to stop.
#

if __name__ == "__main__":
playing = True
while playing:
winner = playGame()
if winner == COMPUTER_PLAYER:
elif winner == HUMAN_PLAYER:
print("Congratulations! You won!")
else:
print("The board is full. This is a draw!")

while True:
try:
option = input("Do you want to play again? (Y/N): ")
except ValueError:
print("Please input a correct value. Try again.")
continue
if option == 'Y' or option == 'y':
break
elif option == 'N' or option == 'n':
playing = False
break
else:
$$$$


# PEP-8: the Style Guide for Python Code

PEP-8 defines many conventions that will assist in making Python programs more readable and maintainable by a wider audience. Things like:

• snake_case should be used for functions, parameters and variables. gameState and availableMoves should be game_state and available_moves, etc.
• surround binary operators with one space. eg) j+1 should be written j + 1. (Note that no white spaces should be used around the = token with keyword parameters, unless type-hints are used.)
• All continuation lines should be indented:
• wrong:
while (
row != secondRowCondition and
column != secondColumnCondition and
gameState[row][column] != 0
):
if firstLoop:
...

• right:
while (row != secondRowCondition and
column != secondColumnCondition and
gameState[row][column] != 0):
if first_loop:
...


# Loop like a Native

See the loop like a native talk by Ned Batchelder.

Code like:

    for i in range(0, BOARD_SIZE_X):
if gameState[0][i] != 0:
availableMoves -= 1


can be rewritten more efficiently, without indexing as:

    for cell in gameState[0]:
if cell != 0:
availableMoves -= 1


If both the index and the contents of the list at that index is required, then enumerate() is preferred. Instead of this:

    for i in range(0, BOARD_SIZE_Y):
for j in range(0, BOARD_SIZE_X):
if gameState[i][j] == 0:
score += scoreOfCoordinate(gameState, i, j, player, opponent)


use:

    for i, row in enumerate(gameState):
for j, cell in row:
if cell == 0:
score += scoreOfCoordinate(gameState, i, j, player, opponent)


# Don't mix indices

You are using i and j interchangeably. minimax() uses gameState[j][i] and evaluateScore() uses gameState[i][j]. This is very confusing. Use more descriptive index names, perhapsrow_idx and col_idx.

# Magic Numbers

There are way too many magic numbers in the code. 0 and 1 are usually OK, such as when used as row/column offsets. I'm not certain what the 2 means in BOARD_SIZE_Y - 2, but I can guess. The 3 in valsInARow == 3 might deserve a name, but perhaps not.

The real grievous errors are:

            if gameState[i][j] == 1:
sys.stdout.write("|X|")
elif gameState[i][j] == -1:
sys.stdout.write("|O|")


You define COMPUTER_PLAYER and HUMAN_PLAYER at the top of the file. Why aren't they used here?

Also, defining an EMPTY constant, and testing against it instead of 0, would clarify intent.

# Over-parameterization

scoreOfCoordinate() takes way too many parameters. Several of the parameters are redundant.

When the row or column increment is 0, the first and second conditions are None. When the increment is -1, the first condition is -1 and the second is the board size in the corresponding direction. If the increment is +1, the conditions are reversed with the first being the board size and the second being -1.

You could remove the condition parameters, and inside the function set internal variables based on just the row & column increment parameters.

But this is probably still too complicated. The row and column values are confined to a simple, constant ranges. Simply test that!


while (0 <= row < BOARD_SIZE_Y and 0 <= column < BOARD_SIZE_X and gameState[row][column] != 0):
...


or even:

VALID_ROWS = range(BOARD_SIZE_Y)
VALID_COLUMNS = range(BOARD_SIZE_X)
...

while (row in VALID_ROWS and column in VALID_COLUMNS and gameState[row][column] != 0):
...


# NumPy

I hate seeing numpy used for just getting the diagonals out of a matrix. It is overkill.

If you are going to require numpy, you may as well take use it. For example, 4 of the same values in a row could be written as:

    matrix = numpy.array(gameState)

# Compare all elements with their neighbour in the adjacent column
match = (matrix[:, :-1] != 0) & (matrix[:, :-1] == matrix[:, 1:])

# four-in-a-row would be three adjacent matching elements
four_in_a_row = (match[:, :-2] & match[:, 1:-1] & match[:, 2:]).any()


Checking for 4 of the same value in a column is just as easy. Of course, you could just rotate or transpose the matrix and reuse the above check

Checking diagonals is similar: matrix[:-1, :-1] == matrix[1:, 1:] does an element-wise comparison of adjacent elements in one diagonal direction, and matrix[1:, :-1] == matrix[:-1, 1:] would compare in the other direction.

# Code Structure

You should break you code up into several modules

1. A game-state management module
2. A game UI module, which displays the board, and asks the user for input. This could be replaced with a TkInter version or a colorama ANSI console version without changing other parts of the code
3. A computer player module, which could contain different strategies
• random movement
• minimax strategy

By breaking the code into separate modules, you can improve the understandability. For example, the you could ask the game-state for what the available moves are, if a particular move is valid, or if the game is in a "won" state. That code doesn't need to be duplicated in the minimax strategy code, which would make the code more focused and easier to read.

• while ( is really not a violation of PEP8. See for example their # Hanging indents should add a level.`; and also This PEP takes no explicit position on how (or whether) to further visually distinguish such conditional lines from the nested suite inside the if-statement. honestly I think OP's style is more legible in this case. – Reinderien 19 hours ago