Connect FourPlayer vs AI & Online 2 Player

What is it?

This project is my web app implementation of the classic Connect Four game. It consists of a frontend client made with Next.js and a backend game server made with Express. It allows for single player against an AI and online two player.

Features

  • AI with 3 difficulties
  • Online 2 player with 3 lobbies
  • Move timer in 2 player
  • Ability to rejoin online game
  • Animated menus and moves
  • Responsive design for mobile, tablet, and desktop

Tech Used

Next.js logo

Framework

Typescript logo

Language

Socket.io logo

Websockets

Express logo

Backend

GSAP logo

Animation

Tailwind logo

Styling

The AI

The computer player uses a typical minimax algorithm for turn-based, perfect information games. It searches to a depth of 4 moves ahead and saves the best 3 moves as scored by the evaluation function.

Minimax decision tree diagram

The main design goal of the AI was to make it feel like a fairly good but human player. To this end, I increased the odds of the AI choosing one of the sub-optimal saved moves as a function of the number of moves that had been made to that point. This had the dual effect of: 1) decreasing the occurrences of sub-optimal moves in the early game (which, in Connect Four, generally just means an automatic and unfun win for the player) and 2) increasing the occurrence of mistakes as the board state becomes more complex (which is generally expected from a human player).

Online 2 Player

Websockets were chosen over normal http requests for communication of game state between clients and the server due to their low latency nature. Socket.io was mostly chosen for its simpler API but it was nice to have automatic fallback to long polling and reconnections as additional features.

Diagram showing that the game server is the source of truth for clients

The game server is responsible for maintaining game state (being the one source of truth for both players), adding players to lobbies, keeping track of move timers, and allowing players to rejoin lobbies in case of disconnection.

Code Highlight

Game State

My initial attempt at implementing game state (and the transitions between those states) resulted in many conditionals that grew both in complexity and in number across my code. It quickly became impossible to understand what was happening so I re-wrote the code using a State pattern.

Diagram showing basic state structure and delegation of  behavior to concrete states which all implement the abstract GameState

The GameContext utilizes an abstract GameState object which delegates the behavior to one of four different concrete implementations (see diagram above). The concrete states trigger transitions to other states by using their reference to the GameContext object; depending on the current state and action. For instance, adding a player to a game in the Inactive state triggers a transition to the Waiting state, but adding a player in the Waiting state triggers a transition to the InProgress state.

Animation

Most parts of the app are animated to reinforce the idea of a fun and light-hearted experience. I chose GSAP as the animation framework because I had only used Framer Motion before and wanted to try something different. Overall, I ended up preferring GSAP as it was easier to simply drop some animation code into pre-existing components without major changes. Additionally, the timeline API makes it very simple to create staggered and delayed animations.

Code Highlight

Disk Drop Animations

Animating the disks dropping into the board, as players made their moves, provided an extra challenge, as the end point of the animation (where the disk ended up on the board) was dynamic and dependent on the number of discs that were already in that column.

Diagram showing how the end point of the disc drop animation was derived

Drawing out the problem made it much easier to derive the formula needed to calculate the end point.