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
Framework
Language
Websockets
Backend
Animation
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.
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.
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.
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.
Drawing out the problem made it much easier to derive the formula needed to calculate the end point.