4
\$\begingroup\$

I am currently working on a Connect 4 game for a project, and it works. We added support so that it can be played by 3 players instead of 2. I know there are multiple instances of repeated code or un-optimized functions or code. I would appreciate any help on that front.

Regarding the 3-player connect 4, we pretty much had it act just like normal connect 4 but with 3 players. Player 1 is red, Player 2 is yellow and Player 3 is green. All 3 players are trying to get "Connect 4" like normal and prevent the other players from getting "Connect 4". We are doing that in hopes of adding some complexity in play and add another variable for players to have to deal with while playing.

import sys
import numpy as np
import pygame
import math
# Global Values for the Game
BLACK = (0, 0, 0)
BLUE = (0, 0, 255)
RED = (255, 0, 0)
WHITE = (255, 255, 255)
YELLOW = (255, 255, 0)
GREEN = (0, 255, 0)
ROW_COUNT = 7
COLUMN_COUNT = 8
SQUARE_SIZE = 75
WIDTH = COLUMN_COUNT * SQUARE_SIZE
HEIGHT = (ROW_COUNT + 1) * SQUARE_SIZE
RADIUS = int(SQUARE_SIZE / 2 - 5)
# Function to Create the Board Full of Zeros
def create_board():
 board = np.zeros((ROW_COUNT, COLUMN_COUNT))
 return board
# Function for Dropping the Player Pieces
def drop_piece(board, row, col, piece):
 board[row][col] = piece
# Function to See Where the Player Drops is Valid
def is_valid(board, col):
 return board[ROW_COUNT - 1][col] == 0
# Function to See Where the Next Open Row to Place Player Piece
def next_open_row(board, col):
 for r in range(ROW_COUNT):
 if board[r][col] == 0:
 return r
# Function to Print the Actual Board Because it is Flipped
def print_board(board):
 print(np.flip(board, 0))
# Function to Check the Multiple Winning Move Cases
def winning_move(board, piece):
 # Check Horizontal Locations
 for c in range(COLUMN_COUNT - 3):
 for r in range(ROW_COUNT):
 if board[r][c] == piece and board[r][c + 1] == piece and board[r][c + 2] == piece and board[r][c + 3] == \
 piece:
 return True
 # Check Vertical Locations
 for c in range(COLUMN_COUNT):
 for r in range(ROW_COUNT - 3):
 if board[r][c] == piece and board[r + 1][c] == piece and board[r + 2][c] == piece and board[r + 3][c] == piece:
 return True
 # Check Positive Diagonals
 for c in range(COLUMN_COUNT - 3):
 for r in range(ROW_COUNT - 3):
 if board[r][c] == piece and board[r + 1][c + 1] == piece and board[r + 2][c + 2] == piece and board[r + 3][c + 3] == piece:
 return True
 # Check Negative Diagonals
 for c in range(COLUMN_COUNT - 3):
 for r in range(3, ROW_COUNT):
 if board[r][c] == piece and board[r - 1][c + 1] == piece and board[r - 2][c + 2] == piece and board[r - 3][c + 3] == piece:
 return True
# Draws the Graphical Game Board
def draw_board(board):
 # This nested for loop makes the empty starting board
 for c in range(COLUMN_COUNT):
 for r in range(ROW_COUNT):
 pygame.draw.rect(screen, BLUE, (c * SQUARE_SIZE, r * SQUARE_SIZE + SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE))
 pygame.draw.circle(screen, BLACK, (
 int(c * SQUARE_SIZE + SQUARE_SIZE / 2), int(r * SQUARE_SIZE + SQUARE_SIZE + SQUARE_SIZE / 2)), RADIUS)
 # This nested for loop is what is used to fill the board in with the player pieces
 for c in range(COLUMN_COUNT):
 for r in range(ROW_COUNT):
 if board[r][c] == 1: # Fill the board with Player 1 pieces
 pygame.draw.circle(screen, RED, (
 int(c * SQUARE_SIZE + SQUARE_SIZE / 2), HEIGHT - int(r * SQUARE_SIZE + SQUARE_SIZE / 2)), RADIUS)
 if board[r][c] == 2: # Fill the board with Player 2 pieces
 pygame.draw.circle(screen, YELLOW, (
 int(c * SQUARE_SIZE + SQUARE_SIZE / 2), HEIGHT - int(r * SQUARE_SIZE + SQUARE_SIZE / 2)), RADIUS)
 elif board[r][c] == 3:
 pygame.draw.circle(screen, GREEN, (
 int(c * SQUARE_SIZE + SQUARE_SIZE / 2), HEIGHT - int(r * SQUARE_SIZE + SQUARE_SIZE / 2)), RADIUS)
 pygame.display.update()
# Function to track the mouse movement
def track_mouse():
 if event.type == pygame.MOUSEMOTION: # Tracks the mouse motion
 pygame.draw.rect(screen, BLACK, (0, 0, WIDTH, SQUARE_SIZE))
 posx = event.pos[0]
 if turn == 0:
 pygame.draw.circle(screen, RED, (posx, int(SQUARE_SIZE / 2)), RADIUS)
 if turn == 1:
 pygame.draw.circle(screen, YELLOW, (posx, int(SQUARE_SIZE / 2)), RADIUS)
 elif turn == 2:
 pygame.draw.circle(screen, GREEN, (posx, int(SQUARE_SIZE / 2)), RADIUS)
 pygame.display.update()
# Graphical representation of dropping the piece
def draw_drop_piece(event, turn):
 game_over = False
 colors = (RED, YELLOW, GREEN) # Container to Store Colors for easy calling
 if turn in (0, 1, 2): # Checks to See if its Players Turn
 posx = event.pos[0]
 col = int(math.floor(posx / SQUARE_SIZE))
 if is_valid(board, col): # Checks Every Turn if Where the Player is Putting Their Piece is Valid
 row = next_open_row(board, col)
 drop_piece(board, row, col, turn + 1)
 if winning_move(board, turn + 1): # Checks Every Turn if the Player Won
 label = FONT.render("PLAYER {} WINS!".format(turn + 1), 1, colors[
 turn]) # Format function allows to easily change a string in code depending on values
 screen.blit(label, (40, 10))
 game_over = True
 else:
 label = FONT.render("Enter Valid Spot", 1, BLUE)
 screen.blit(label, (40, 10))
 game_over = None # If the player made an invalid move it returns a none value
 print(turn)
 return game_over
def start_menu(screen):
 click = False
 while True:
 label = FONT.render("Main Menu", 1, BLUE)
 screen.blit(label, (WIDTH / 4, 40))
 posx, posy = pygame.mouse.get_pos()
 button_1 = pygame.Rect(WIDTH / 2, 200, 300, 70)
 button_2 = pygame.Rect(WIDTH / 2, 300, 300, 70)
 pygame.draw.rect(screen, BLACK, button_1)
 pygame.draw.rect(screen, BLACK, button_2)
 start_text = FONT.render("Start", 1, WHITE)
 screen.blit(start_text, (WIDTH / 3, 200))
 quit_text = FONT.render("Quit", 1, WHITE)
 screen.blit(quit_text, (WIDTH / 3, 300))
 if button_1.collidepoint((posx, posy)):
 if click:
 return True
 if button_2.collidepoint((posx, posy)):
 if click:
 sys.exit()
 for event in pygame.event.get():
 if event.type == pygame.QUIT: # Exiting the game
 sys.exit()
 if event.type == pygame.KEYDOWN:
 if event.key == pygame.K_ESCAPE:
 sys.exit()
 if event.type == pygame.MOUSEBUTTONDOWN:
 if event.button == 1:
 click = True
 pygame.display.update()
if __name__ == '__main__':
 pygame.init()
 board = create_board() # Creates Board
 print_board(board) # Prints Board
 game_over = False # The Game Isn't Over as it Just Started
 turn = 0 # Player One Starts First
 total_spaces = ROW_COUNT * COLUMN_COUNT
 FONT = pygame.font.SysFont("monospace", 50)
 SIZE = (WIDTH, HEIGHT)
 screen = pygame.display.set_mode(SIZE)
 start_menu(screen)
 draw_board(board)
 pygame.display.update()
 while not game_over:
 for event in pygame.event.get():
 if event.type == pygame.QUIT: # Exiting the game
 sys.exit()
 track_mouse()
 if event.type == pygame.MOUSEBUTTONDOWN:
 pygame.draw.rect(screen, BLACK, (0, 0, WIDTH, SQUARE_SIZE))
 game_over = draw_drop_piece(event, turn)
 print_board(board)
 draw_board(board)
 # This makes sure the turn indicator alternates between 1 and 0
 # Also checks if the player made a valid move and if they didn't it doesn't move to the next turn
 if game_over is not None:
 turn = (turn + 1) % 3
 total_spaces -= 1
 # Checks to see if the total empty spaces are 0 if so that means its a tie
 if total_spaces <= 0:
 label = FONT.render("TIE!", 1, BLUE)
 screen.blit(label, (40, 10))
 print_board(board)
 draw_board(board)
 game_over = True
 if game_over:
 pygame.time.wait(5000)
toolic
14.4k5 gold badges29 silver badges201 bronze badges
asked Nov 12, 2021 at 21:02
\$\endgroup\$
1
  • \$\begingroup\$ At least for me, your code isn't working (Python 3.9.4 on MacOS). It prints out some pygame boilerplate and a grid of zeros to the terminal and opens up a pygame window, but the window is mostly featureless (all black with a small blue rectangle and two small white rectangles). The window doesn't respond to any basic mouse clicks or keyboard presses. Does your program run only on certain kinds of systems? If so, you might want to add those qualifications to your question. \$\endgroup\$ Commented Nov 13, 2021 at 18:57

1 Answer 1

2
\$\begingroup\$

UX

Add a title to the GUI to show that it is the Connect 4 game. Also mention in the GUI that there are 3 players: red, yellow and green.

In addition to the GUI, the code prints a lot of output to the shell after each move. If this was meant only for debugging purposes, the output should be disabled by default.

Documentation

Add a docstring at the top of the code to summarize its purpose:

"""
Connect 4 game using the Pygame GUI
"""

The PEP 8 style guide also recommends adding a docstring for each function. For example, instead of:

# Function to Create the Board Full of Zeros
def create_board():

Convert the comment into a docstring:

def create_board():
 """ Create the Board Full of Zeros """

Note that there is no need to use the words "Function to" since it is obvious that the code declares a function due to the def keyword.

Layout

There are some very long lines and lines which use line breaks inconsistently. The black program can be used to automatically format the code for better consistency.

Efficiency

In the track_mouse function, the 2 if statements should be combined into a single if/elif because the conditions are mutually exclusive:

 if turn == 0:
 pygame.draw.circle(screen, RED, (posx, int(SQUARE_SIZE / 2)), RADIUS)
 elif turn == 1:
 pygame.draw.circle(screen, YELLOW, (posx, int(SQUARE_SIZE / 2)), RADIUS)
 elif turn == 2:

The same can be done in the draw_board function with board[r][c].

DRY

You can eliminate the duplicate checks against piece using a loop. For example, for the horizontal checks, instead of:

# Check Horizontal Locations
for c in range(COLUMN_COUNT - 3):
 for r in range(ROW_COUNT):
 if board[r][c] == piece and board[r][c + 1] == piece and board[r][c + 2] == piece and board[r][c + 3] == \
 piece:
 return True

use:

for c in range(COLUMN_COUNT - 3):
 for r in range(ROW_COUNT):
 count = 0
 for i in range(4):
 if board[r][c+i] == piece:
 count += 1
 else:
 break
 if count == 4:
 return True

The break also makes the code more efficient since it does not always need to perform 4 checks.

The following line is repeated 3 times:

int(c * SQUARE_SIZE + SQUARE_SIZE / 2), HEIGHT - int(r * SQUARE_SIZE + SQUARE_SIZE / 2)), RADIUS)

You can assign that expression to a variable, and use the variable instead.

answered Dec 10, 2024 at 12:30
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.