Python Tutorial: Build a Gomoku Game from Scratch with Pygame

Step-by-step guide to building a two-player Gomoku game in Python with Pygame across six core modules.
This tutorial walks through building a fully functional Gomoku (Five in a Row) game in Python using Pygame, structured across six modules: initialization, constants, fonts, game state, core functions, and the main loop. It covers board rendering, mouse event handling, win-condition detection, and event-driven programming — with tips on extending the project to include an AI opponent using Minimax and Alpha-Beta pruning.
Why Gomoku Makes a Great Python Practice Project
For beginners learning Python, building a complete small game from scratch is one of the best ways to solidify programming fundamentals and understand program structure. Gomoku has simple rules and clean logic, yet it covers multiple core concepts — graphical interface rendering, mouse event handling, game state management, and win-condition algorithms — making it an entry-level project that's both fun and educational.
It's worth noting that Gomoku holds a storied place in the field of Computer Game Playing research. Originating in ancient China and later refined into its modern competitive form in Japan before spreading to Europe in the late 19th century, Gomoku is theoretically proven to be a first-player win under renju rules (which restrict black from playing double-threes, double-fours, or overlines). Yet the complete state space of standard 15×15 Gomoku has never been fully enumerated, making it an excellent teaching case for search algorithms and heuristic pruning — from beginner practice all the way to AI development.
This article is based on a Python Gomoku tutorial shared by a Bilibili content creator, walking through the complete implementation of the project. The code is broken into six clear modules — from imports to the main loop — each building on the last, making it ideal for beginners to follow step by step.
Six Core Modules: Project Structure at a Glance
As laid out by the tutorial author, the complete Gomoku game consists of six parts, each with a distinct responsibility — a great demonstration of clean program structure thinking.
Part 1: Module Imports and Initialization
The project uses pygame, the classic Python game development library, to handle the graphical interface. Pygame is built on top of SDL (Simple DirectMedia Layer) and has been the de facto standard for beginner Python game development since its release in 2000. It provides a full toolkit covering window management, graphics rendering, audio playback, and keyboard/mouse event handling — letting developers quickly prototype 2D games without needing to understand low-level graphics APIs.
SDL was developed by Sam Lantinga in 1998, originally to port Civilization: Call to Power to Linux. It uses a hardware abstraction layer (HAL) design — calling DirectX or GDI+ on Windows, Metal/OpenGL on macOS, and X11, Wayland, or KMS/DRM on Linux — for true cross-platform graphics output. Pygame wraps SDL's C interfaces into Python-callable modules via CPython's C extension mechanism. For example, pygame.Surface maps to SDL's internal SDL_Surface struct, and pygame.display.flip() calls SDL_Flip() to swap frame buffers. This layered architecture means that even though Python itself is interpreted, the core graphics computations still run efficiently at the C level — which is why beginners can achieve smooth graphical interfaces with just a few hundred lines of Python.
At the start of the program, you'll import the necessary modules and call pygame.init() to initialize everything, laying the groundwork for window creation and event listening.
Part 2: Color and Game Parameter Definitions
This section centralizes all the constants the program will use repeatedly: RGB values for the board color, black and white piece colors, and line colors, as well as game parameters like board size (15×15), window dimensions, and grid spacing. Managing these values in one place makes them easy to reference and adjust — a hallmark of good coding practice.
RGB (Red, Green, Blue) is the most common color model in computer graphics, with each channel ranging from 0 to 255, yielding approximately 16.7 million possible colors. The physical basis for this model lies in the three types of cone cells in the human retina — S-type (short wavelength, blue ~420nm), M-type (medium wavelength, green ~530nm), and L-type (long wavelength, red ~560nm) — a theory proposed by Thomas Young in 1802 and later refined by Hermann von Helmholtz into the Young-Helmholtz trichromatic theory. Each pixel on a computer monitor consists of three sub-pixels (red, green, blue), and at 8-bit depth the three channels combine to produce 256³ ≈ 16.77 million colors — more than the human eye can distinguish (roughly 10 million). In Pygame, colors are typically passed as a tuple (R, G, B), with optional alpha support via (R, G, B, A) for transparency. Defining all color constants in one place (e.g., BLACK = (0, 0, 0), BOARD_COLOR = (220, 179, 92)) eliminates magic numbers and makes theme changes a one-line update — an important habit for writing maintainable code.
Part 3: Font Setup
The game needs to display text such as the current turn and victory messages, so font objects must be set up in advance. The pygame.font module is used to load the appropriate font and size, supporting status displays and win dialogs.
Once created, a font object can have its render() method called repeatedly to generate text Surfaces, which are then drawn to the screen at specified positions using blit(). Under the hood, render() rasterizes the string into a pixel image: modern font files (such as TrueType .ttf or OpenType .otf) describe glyph outlines using Bézier curves, and Pygame calls the FreeType2 library to rasterize them — converting Bézier curves to polygon outlines, filling pixels with a scanline algorithm, and applying sub-pixel anti-aliasing to smooth edges. This process becomes computationally expensive at large font sizes, so the "create once, render many times" approach is crucial. For static labels shown frequently (like "Black's Turn"), render them once at initialization and cache the resulting Surface. For dynamic text (like a timer), only call render() when the value changes — balancing flexibility with performance.

Game State Management and Function Design
Part 4: Game Initialization State
This section maintains the game's runtime state — a 2D array tracking each board position (empty, black, or white), a variable tracking whose turn it is, and a flag indicating whether the game has ended.
Using a 2D list (List of Lists) to represent board state is the classic data structure choice for matrix-style problems. A 15×15 board maps to a board[15][15] array where each element stores the cell's state (0 = empty, 1 = black, 2 = white), with O(1) access to any position. In Python, this can be created quickly with list comprehension: board = [[0]*15 for _ in range(15)]. Important: never use [[0]*15]*15 — a classic beginner mistake. The *15 operation doesn't copy the inner list; it creates 15 references to the same inner list object (a shallow copy), so modifying board[0][0] will change column 0 across all rows. List comprehension creates a fresh list object on each iteration, ensuring the 15 rows are independent. For better memory efficiency, you can use NumPy's np.zeros((15,15), dtype=np.int8), which stores data in a contiguous C array and is tens of times faster than Python lists — more suitable if you later add an AI search algorithm with frequent board state reads. Clear state management is the foundation of correct game logic.
Part 5: Core Function Definitions
The author encapsulates core logic into multiple independent functions, embodying the principle of modular programming:
- Player name conversion: Translates internal piece markers (1, 2) into readable labels like "Black" and "White".
- Win-condition checker: Detects whether a player has achieved five in a row — the algorithmic heart of the game. The function checks four directions: horizontal, vertical, diagonal (top-left to bottom-right), and anti-diagonal (top-right to bottom-left). A common implementation extends outward from the latest move in both directions, counting consecutive same-color pieces. Since only a fixed range around the latest move needs to be checked (at most 4 cells in each direction, 8×4 = 32 cells total), the time complexity is O(1) — far more efficient than scanning the entire board (O(n²)). Advanced implementations use a bitboard representation — compressing board state into integer bit fields and using CPU bitwise operations to check multiple cells in a single instruction — for further speedups of tens of times. This combination of local checking and bit-level optimization is a vivid illustration of algorithmic locality and low-level performance tuning.
- Board drawing: Renders the 15×15 grid and all placed pieces.
- Status display: Shows the current turn and relevant messages in real time.
- Victory dialog: Displays the winner when a player achieves five in a row.

Breaking functionality into functions not only makes the code structure cleaner, but also significantly improves maintainability and reusability — exactly the engineering mindset beginners need to develop.
The Main Game Loop and Running Results
Part 6: The Main Game Loop
The main loop is the execution core of every Pygame game and a textbook example of the Event-Driven Programming paradigm. Unlike traditional sequential programs, event-driven programs are controlled by external events — mouse clicks, keyboard input, window close signals — rather than advancing on their own.
Pygame's main loop is essentially an infinite loop that continuously polls the event queue: each frame calls pygame.event.get() to retrieve all queued events, processes each one by type, then calls pygame.display.flip() to flush the buffer to the screen. This "get events → update state → redraw" three-phase structure is the famous Game Loop Pattern — systematically described by Robert Nystrom in Game Programming Patterns — and forms the foundational architecture of nearly all real-time interactive programs, from simple 2D games to AAA titles.
Frame rate control matters too: Pygame provides a pygame.time.Clock object, and calling clock.tick(60) caps the frame rate at 60 FPS by calculating the elapsed time of the previous frame and calling the OS sleep() function to fill the remaining time. For a turn-based game like Gomoku, frame rate limiting doesn't affect game logic but significantly reduces CPU usage (an uncapped loop can peg a single core near 100%). Understanding event-driven programming and the game loop is the key conceptual leap from linear scripting to GUI and game development.
The main loop continuously listens for user input, updates game state based on events, and redraws the screen. When the player clicks the left mouse button, the program converts the click position to board coordinates, places the current player's piece, and immediately calls the win-condition checker.

Demo Results
Once the code runs, the game window displays a standard 15×15 board with intuitive, smooth gameplay:
- Left-click any intersection on the board to place a black piece;
- The turn automatically switches to white after each move;
- Players alternate turns; the first to achieve five in a row wins;
- After the game ends, click anywhere to start a new game.

The demo shows both a black win and a white win, with the system correctly displaying "Black Wins" or "White Wins" in each case — confirming the correctness of the win-detection logic.
From Practice to Mastery: The Real Value of Hands-On Projects
Small as it is, this Python Gomoku project is a complete package. It gives beginners a chance to apply variables, lists, functions, loops, and conditionals together in a real project, while also introducing graphical interface development and the core ideas of event-driven programming.
For learners looking to move from beginner to intermediate, completing hands-on projects like this is far more effective than simply reading tutorials. After finishing the walkthrough, try extending it yourself: add an undo feature, implement a computer opponent, or add a timer and scoring system.
Implementing an AI opponent is the most challenging and educationally rewarding next step. The classic Minimax algorithm was formally introduced by John von Neumann in a 1928 game theory paper and later brought into computer chess by Claude Shannon in 1950. Its core assumption is that both players play optimally: the algorithm models the game as a tree, with the AI (Max player) maximizing its score and the human (Min player) minimizing it, recursively searching all possible move sequences and propagating scores from leaf nodes — evaluated by a static heuristic evaluation function — back up the tree. However, because Gomoku can have dozens to hundreds of legal moves at each step, pure Minimax suffers from exponential search space explosion at even moderate depths (O(b^d), where b is the branching factor and d is the search depth).
Alpha-Beta pruning was designed to address exactly this, independently discovered around 1956 by John McCarthy's team: the algorithm maintains two boundary values — α (best known Max value) and β (best known Min value) — and prunes any branch that cannot possibly beat the current bounds. In theory, this reduces the number of nodes from O(b^d) to O(b^(d/2)), effectively doubling the searchable depth for the same computation time and significantly improving AI strength. Modern Gomoku AI further stacks iterative deepening, transposition tables (caching evaluation results for previously seen positions), and move ordering (prioritizing the most promising moves) on top of Alpha-Beta, pushing practical search efficiency close to theoretical limits.
Extending a two-player game into a human-vs-AI game is not just a feature upgrade — it's an important step from basic algorithms into the field of artificial intelligence. Continuous improvement and optimization is what truly turns knowledge into skill.
Related articles

Infrastructure Architecture for Agent Applications: Four Core Patterns Explained
A deep dive into infrastructure architecture patterns for production-grade Agent applications, covering state persistence, sandbox isolation, LLM observability, and cost control.

Interpreting Anthropic's Cryptanalysis Research: A Litmus Test for AI Reasoning Capabilities
Deep analysis of Anthropic's cryptanalysis research, examining LLM capabilities in code-breaking tasks, dual implications for AI safety, and methodological value as a reasoning ability benchmark.

Infrastructure Architecture for Agent Applications: Four Core Patterns Explained
Deep dive into infrastructure architecture patterns for production-grade Agent applications, covering state persistence, sandbox isolation, LLM observability, and cost control.