Build a Match-3 Browser Game With Three.js and AI (Complete Tutorial + Source Code)
Build a Three.js match-3 browser game with AI: 3D runes, an 8×8 board, legal swaps, gravity, cascades, scoring, raycasting, tests and downloadable source code.
A browser match-3 game doesn’t have to look like a flat spreadsheet of colored squares. With Three.js, you can use real 3D meshes, materials, lighting, camera composition, and particle effects while keeping the puzzle rules in ordinary JavaScript. The challenge is making a polished-looking board that still behaves like a reliable game.
In this Blinkcade Academy tutorial, we’ll build Runefall Workshop, an original 8×8 rune-matching prototype. Players click or select two neighboring runes, create horizontal or vertical matches of three or more, trigger falling-piece cascades, and race to reach 2,600 points in 22 valid moves. Successful matches earn points; invalid swaps return without spending a move. If the board runs out of legal moves, it reshuffles.
You will learn to separate game logic from 3D graphics, build the board, identify valid swaps, resolve cascades, translate pointer positions into a 3D selection, and test before adding more effects. A companion source package contains index.html, board.mjs, game.mjs, a README, and automated board tests. The playable 3D rendering relies on a pinned Three.js module from a CDN, so an internet connection is needed unless you bundle the library locally.
Development verification: The standalone board engine passed 300 randomized board-generation and legal-move checks, plus automated tests for cascades, match detection, gravity, invalid moves, bounds, and immutability. JavaScript syntax checks passed. A full graphical-browser QA pass has not been completed in this environment; it remains required before public release. The code package is a working implementation candidate, not a certified production build.
Before publishing this WordPress draft: Add a public download URL for the accompanying
blinkcade_article_09_threejs_match3.zipfile and complete the graphical browser test. The source package is supplied separately with this editorial handoff; WordPress cannot link to a ChatGPT sandbox download URL.
1. Why use Three.js for a match-3 game?
Phaser is often the simpler option for a conventional 2D match-3 game. It has built-in scenes, sprites, animation and input systems designed for 2D projects. Three.js becomes attractive when depth, materials, dimensional pieces and a custom 3D presentation are central to the identity of the game.
Three.js is a 3D rendering library, not a full match-3 engine. You still need to write the board data, swap rules, scoring, gravity and game-state transitions. The 3D scene should display the authoritative board state rather than become the source of truth for which pieces occupy which cells. This distinction is what keeps attractive 3D visuals from breaking puzzle logic.
| System | Responsibility | Where it lives in our example |
|---|---|---|
| Board model | Piece types and cell coordinates | board.mjs |
| Match rules | Groups of 3+, valid swaps, gravity | board.mjs |
| Game state | Moves, score, win/loss, busy input | game.mjs |
| 3D scene | Meshes, lighting, camera, picking | game.mjs |
| Browser interface | Instructions, score, buttons, responsive layout | index.html |
The practical advantage is maintainability: if a cascade scores incorrectly, inspect board logic. If a rune looks too dark or the camera is clipped, inspect rendering. AI assistants can be instructed to modify one part without rewriting the other.
2. Design the smallest complete match-3 loop
Our version-one brief is intentionally narrow. The board is 8 columns by 8 rows, with six rune types distinguished by shape and color. A player chooses two orthogonally adjacent cells; diagonal and long-distance swaps aren’t allowed. A valid swap must create at least one horizontal or vertical match of three or more identical types. Valid moves cost one turn. Invalid moves leave the board unchanged and cost nothing.
Matched pieces disappear from the logical board, surviving pieces fall down each column, and new runes fill empty spaces at the top. If the new board contains another match, the process repeats as a cascade. The chain multiplier increases the points for later cascades. The game ends when the player reaches 2,600 points or uses all 22 moves. A board that has no legal swap must be reshuffled rather than trapping the player.
A clear acceptance test for the first playable milestone is: start the game, make an invalid swap, make a valid swap, observe score and move changes, resolve a cascade, use the hint button, run out of moves or reach the goal, and restart successfully. Resist adding power-ups, level maps, story dialogue or monetization until this loop works.
3. Set up the project and launch it locally
The companion folder contains five files:
runefall-workshop/
├── index.html HTML, CSS, HUD, controls and canvas container
├── board.mjs Testable, rendering-independent puzzle rules
├── game.mjs Three.js renderer, scene and gameplay integration
├── board.test.mjs Automated puzzle-logic regression tests
└── README.md Setup, controls and known limitations
After extracting the project, open a terminal in the folder and run:
python3 -m http.server 8000
Then visit http://localhost:8000. A local web server is important because game.mjs imports ./board.mjs as an ES module. Opening index.html using file:// can trigger browser module security restrictions. The tutorial uses the pinned Three.js module [email protected] from jsDelivr for convenience; the first load requires internet access. For a production build, a package manager and bundler are generally more robust than depending on an external CDN at runtime.
If the page is blank, check the browser console and Network panel for a failed CDN request, missing module, JavaScript error or disabled WebGL. Don’t tell an AI tool to rewrite the rules until you know the source of the failure.
4. Keep the board state separate from graphics
Each cell in the board holds an integer from 0 to 5, corresponding to one of the six rune types. Rows and columns are indexed from zero. A single 8×8 array is the authoritative source for the game: Three.js meshes are rebuilt or updated to reflect it.
export const SIZE = 8;
export const TYPES = 6;
export function copy(board) {
return board.map(row => row.slice());
}
export function adjacent(a, b) {
return Math.abs(a.r - b.r) + Math.abs(a.c - b.c) === 1;
}
export function swap(board, a, b) {
const result = copy(board);
[result[a.r][a.c], result[b.r][b.c]] =
[result[b.r][b.c], result[a.r][a.c]];
return result;
}
The copied board is deliberate. When evaluating a proposed move, you shouldn’t mutate the visible board and then attempt to reconstruct the original state after a failure. A pure function returns a new value and leaves the old one untouched. That also makes automated testing much easier.
AI prompt — establish a trustworthy board model:
“I’m building an 8×8 Three.js match-3 game. Create a separate
board.mjscontaining the rules only. Represent each cell as an integer from 0 to 5, and use zero-based row/column coordinates. Write pure functions for board copy, adjacency, swapping, horizontal/vertical match detection, gravity, legal-move detection and reshuffling. Don’t import Three.js, touch the DOM, or animate anything in this module. Provide Node tests for edge cases, immutability and repeated randomized boards.”
5. Detect horizontal and vertical matches
A match of three or more is a contiguous run of identical rune types. Scan every row for runs, then every column. A line of four or five is still a single group of matched cells. A T-shaped or L-shaped arrangement can belong to both a horizontal and vertical group; use a Set of coordinates so each cell gets removed once.
The companion findMatches(board) function returns an array of unique {r, c} positions. It does not change the board or award points. That separation is essential: when an AI-generated solution mixes match detection, scoring and visual explosions in one function, a tile can accidentally score twice when groups intersect.
A reliable match detector needs tests for the first row, last row, first column, last column, lines of four/five, overlapping groups, and a board containing no matches. Our test suite also verifies that a newly generated board begins without any automatic matches.
AI prompt — implement match detection without visual side effects:
“Write a match scanner for a rectangular 8×8 grid of six integer tile types. Find all horizontal and vertical runs of three or more. Return unique cell coordinates, even when a tile belongs to both a row and a column match. Do not mutate the board, update score or access the renderer. Add tests for edges, lines of four and five, crossings, and an empty result.”
6. Reject invalid swaps and guarantee available moves
Not every neighboring exchange should be accepted. We create a temporary copy with the selected two cells swapped, call the match scanner, and accept the move only if that result creates a match. Otherwise, the real board stays the same. A diagonal swap never becomes legal merely because it would form a match.
This is the essential swap decision:
const swapped = swap(board, first, second);
if (findMatches(swapped).length === 0) {
return { valid: false, board, reason: 'No match' };
}
// Otherwise, continue into match resolution.
Generating a random board is not sufficient. The puzzle can begin with accidental matches or a layout with no productive swap. Our newBoard() creates piece types while avoiding immediate groups of three and then checks for at least one legal adjacent swap. After a completed move, we check again; if no legal swap remains, the game generates a fresh valid board while preserving the player’s score and remaining moves.
AI prompt — prevent dead boards:
“Update board generation so a new game never opens with pre-existing matches and always offers at least one legal adjacent swap. After a cascade settles, check whether any legal swap remains; if none does, create a new playable layout without changing the score or move count. Add randomized tests that generate at least 300 boards, and report failures instead of claiming the generator always works.”
7. Resolve gravity and cascades in the correct order
After a valid swap, match-three resolution follows a loop: detect → clear → fall → refill → detect again. Never accept new player input while this loop is processing. Otherwise two clicks can mutate the board while columns are collapsing and leave the renderer out of sync with the game state.
Gravity is applied column by column. Read the surviving pieces from bottom to top, write them back from the bottom, and fill remaining positions at the top with randomly selected types. The newly filled board may contain fresh groups, so run match detection again until the board is stable. The tutorial’s companion engine limits the resolution loop to 64 cascades as a defensive sanity check.
The reference scoring rule is deliberately easy to explain:
const pointsForThisCascade = matchedCells.length * 60 * chain;
The first group of three earns 180 points. If refill triggers another three-piece match, that chain is worth 360 points, and so on. The chain counter starts at one for each valid swap. The finished move returns both a final stable board and a list of intermediate cascade phases, allowing the renderer to display the events in order.
A common AI coding error is to set the final board instantly, then attempt to play disappearance animations against objects that no longer exist. Keep a resolution lock in the input layer, and let it step through known logical phases while the pure board engine remains deterministic for testing.
8. Give the puzzle a real 3D appearance
The companion game displays actual Three.js meshes rather than a 2D Canvas pretending to be 3D. Each board cell has a three-dimensional tile beneath it. The six rune types use different shapes—such as octahedrons, an icosahedron, a dodecahedron, a tetrahedron and a ring—plus distinct material colors. A perspective-neutral orthographic camera looks down on the board, preserving an easy-to-read puzzle grid while dimensional lighting gives pieces volume.
For a readable look, use an ambient light for the overall scene plus two directional lights with warm and cool tones. Materials can have a modest emissive contribution, but avoid excessive bloom that makes neighboring cells blend together. Camera framing and tile spacing are more important to playability than the total number of visual effects.
AI prompt — produce high-quality but readable 3D pieces:
“Polish the existing Three.js match-3 board without changing any puzzle rules. Keep an orthographic camera, fixed 8×8 layout and click targets. Give all six rune types distinguishable silhouettes and colors, consistent scale, restrained emissive highlights, a clear selected-cell ring, and subtle match bursts. Do not add a bloom pipeline or imported assets unless necessary. Preserve frame-rate performance, score/move rules, board coordinates and keyboard input. Include before/after screenshots at the target window size.”
For a future premium art pass, replace the primitives with original low-poly or baked high-fidelity assets, but normalize pivots, scales, lighting and orientation before integration. A concept image alone is not proof that those game-ready objects render correctly.
9. Convert mouse and keyboard input into board selection
A browser pointer provides a pixel location; Three.js requires a position in normalized device coordinates for raycasting. The Raycaster can project a ray from the current camera and return intersected objects. In the companion game, every selectable board tile carries {r, c} data. We raycast against tiles, then use those coordinates to select a rune and its neighboring target. This remains stable even when the visual gem geometry changes.
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(pickTiles, false);
if (hits.length) {
const { r, c } = hits[0].object.userData;
choose(r, c);
}
Three.js documents Raycaster.setFromCamera and intersectObjects for exactly this type of mouse picking: official Raycaster documentation.
The reference also supports keyboard navigation: arrow keys move the cursor around the grid, and Enter or Space chooses the current cell. The full experience still needs a proper accessibility review; keyboard support is one important step, not a guarantee of screen-reader-accessible gameplay.
10. Keep the game responsive without wasting GPU resources
On resize, update the orthographic camera’s left, right, top and bottom bounds to preserve a consistent vertical view and enough width for the board. Resize the renderer to fit its display container, and cap the internal pixel ratio so high-density screens don’t make a small puzzle unnecessarily expensive. For deeper background and a larger future feature set, profile draw calls, material count and texture sizes before adding postprocessing.
The project shares geometries and materials among runes, rather than generating a different shape for every animation frame. It removes old meshes when the logical board changes and reuses the same animation loop for the lifetime of the page. Match particles also have a limited lifetime and get removed from the scene.
Review Three.js responsive design guidance for approaches to resolution management. After deployment, test the game in the browsers and devices you actually plan to support; neither a successful Node test nor a syntax check proves that a GPU renderer looks correct on every machine.
11. Run the test suite and manually verify the playable build
From the extracted project folder, run:
node board.test.mjs
The automated suite exercises hundreds of random boards, confirms legal moves, verifies stable post-cascade states, rejects invalid swaps, and checks that logical operations do not mutate their original input. This catches algorithmic defects without requiring WebGL.
The following manual tests still need to be completed in a graphical browser before public release:
| Test | Expected result |
|---|---|
| Initial load | 3D board and 64 rune pieces are visible, with no module-load errors |
| Rune selection | Clicking or keyboard-selecting a cell highlights the intended grid position |
| Invalid swap | Non-matching swap returns without deducting a move |
| Valid swap | One move is spent, matched tiles disappear and the score increases |
| Cascade | Falling/refill can create a new match and award a chain multiplier |
| Dead board | No legal swaps triggers an automatic playable reshuffle |
| Goal | Reaching 2,600 points shows a clear win result |
| Out of moves | Zero remaining moves ends the round cleanly |
| Hint | A suggested legal move is highlighted without spending a move |
| Restart | A fresh playable board appears and previous score/selection reset |
| Resize | Board remains visible and clickable at desktop and narrow widths |
| Performance | No growing memory or frame-time problems after many rounds |
Verification status: The rules and source syntax checks passed in the development environment. A separate Chromium preview attempt was blocked by the environment’s local-address restrictions; it did not yield a verified graphical result. Re-run these manual tests on your computer or a suitable browser testing environment before calling the visual prototype complete.
12. Common problems and focused AI debugging prompts
Nothing appears on screen. Check that the local server is running, the Three.js CDN import loaded, the browser supports WebGL, and game.mjs imported board.mjs successfully. Use DevTools Console and Network errors. Don’t ask the AI to rewrite the 8×8 board model unless the error points to it.
Clicking highlights the wrong cell. Check the renderer’s actual display bounds and the pointer-to-normalized-device-coordinate conversion. Compare selected {r,c} with the tile’s userData instead of guessing based on visual positions.
A cascade scores twice. Confirm the match scanner deduplicates overlapping horizontal and vertical matches. Ensure the scoring layer applies the points for a cascade phase once, not each frame of its animation.
The board becomes impossible to play. Test the no-legal-move condition after a completed cascade and make sure the reshuffle generates an actual playable layout rather than merely randomizing colors.
A second click is accepted mid-animation. Keep the resolution lock active until the complete cascade sequence finishes. Clearing it immediately after the first match leaves the state vulnerable to overlapping moves.
AI debugging prompt: “Inspect my match-3 game without replacing its architecture. The symptom is [EXACT ERROR], reproduced by [STEPS]. Expected behavior is [RULE]. Identify whether the cause is in board.mjs, 3D rendering, raycast input, or the asynchronous resolution state. Apply the smallest change, preserve scoring and move rules, run the board tests, and list any visual browser checks you could not perform.”
13. What should you add after the prototype works?
Once the rules and rendering pass QA, production polish can focus on animated gravity rather than instantaneous redraws, smoother swap reversals, more distinctive 3D materials, layered backgrounds, sound cues, objective variety, and a properly tuned difficulty curve. A future game might introduce locked tiles, special runes, blockers, different boards, boss encounters, or hundreds of designed levels. Each new feature should have an explicit test and should not silently change how ordinary match-3 swaps work.
Keep presentation and game logic separable so that the board engine can survive an art overhaul. The quickest path to an attractive game is rarely to ask AI for every effect at once; it is to improve a verified playable slice, compare the actual browser render against the art direction, and measure the result.
Frequently asked questions
Is Three.js a good choice for a match-3 game?
It can be, especially when you want a distinctly 3D puzzle presentation or need more control over lighting, cameras and materials. For a simpler 2D match-3 release, Phaser may require less custom rendering work. Read our Phaser vs Three.js vs Godot comparison before choosing a stack.
Can an AI assistant build a complete match-3 game?
It can help implement the board, renderer, UI and tests, but a working game still requires clear rules, verified integration, graphical QA, performance checks, asset rights and playtesting. An impressive first screenshot doesn’t prove cascades, deadlock recovery or level pacing are correct.
Does this game require a Three.js installation?
The companion example imports Three.js from a pinned CDN URL, so you don’t need to install the library manually to run the prototype. It does require a local HTTP server for the ES-module project and an initial network connection. A production deployment should bundle and test the dependency.
Why not put all the code in one HTML file?
You can build very small games that way, but separating board rules from rendering makes a match-3 game easier to debug and test. If a platform requires a single-file submission, bundle your modules into the supported output format rather than developing every system inside one increasingly difficult file. See our single-file HTML game guide.
How many levels does this prototype have?
One replayable round with a target score and a fixed move budget. It is an educational vertical slice, not a finished 200-level game. Additional levels would require authored goals, blockers, difficulty balance, content tools and thorough QA.
Conclusion: make the puzzle reliable before making it spectacular
A 3D match-3 game combines two disciplines: a clear, testable puzzle model and a renderer that communicates that model beautifully. In Runefall Workshop, the board engine owns swaps, matches and cascades; Three.js handles camera, shapes, materials and selection. That division gives AI coding assistants smaller responsibilities and gives you a much better chance of detecting regressions.
Continue with the AI game development workflow, our AI prompt guide, and the Blinkcade Academy for more practical lessons.
Official references: Three.js installation, Raycaster API, responsive design guide, and Three.js fundamentals.
Editorial disclosure: Runefall Workshop is an original teaching prototype. Board logic and source syntax were automatically tested; graphical browser QA was not completed in the current environment. No performance or commercial-readiness claims are made without that validation. Featured photograph: Gavin Phillips / Unsplash (illustrative stock photography, not a screenshot of the game).
Keep learning
The complete guide Vibe Coding Games: The Complete Guide to Building Games With AI-
Best AI Prompts for Game Mechanics, Movement and Controls (Copy-and-Paste Guide)
Use practical AI prompts to design and debug player movement, jumping, collisions, combat, camera controls, mobile input and game feel in HTML5, Phaser and Three.js games.
-
How to Write a Game Design Document With AI (+ Free GDD Template)
Write a game design document with AI using a free GDD template, a filled-in game example, copyable prompts and practical checklists for development.
-
How to Build a Single-File HTML Game With AI (Complete Tutorial + Code)
Learn to build a complete single-file HTML5 browser game with AI. Copy one index.html file, understand the game loop, test controls, fix bugs and publish.
