Match-3-0 builds and renders a randomly generated 8×8 board. It also introduces the architecture used by every later Match-3 lesson: reusable framework code lives in lib/, while game-specific code lives in src/.
Architecture
Bootstrap main.js
main.js composes the game. It creates the canvas and Game, gives the game its asset definitions, registers the available states, selects the first state, and starts the animation loop.
const game = new Game( new Canvas(CANVAS_WIDTH, CANVAS_HEIGHT), fontDefinitions, imageDefinitions, soundDefinitions);
game.addState(StateName.Play, PlayState);game.services.stateMachine.change(StateName.Play);game.start();The Game instance creates the runtime services once and injects them into every state. Game-specific objects never need global variables to find the canvas, images, input, or timer.
lib/: Game-Independent Code
The Match-3 template’s lib/ folder is shared infrastructure rather that can be used for any game.
- Runtime:
Canvascreates and scales the pixel-art canvas;Gameowns the animation loop;Statesupplies a common state interface; andStateMachinechanges, updates, and renders the active state. - Services:
Inputreads keyboard and pointer input,Timerruns delayed work and tweens (covered in a later lesson), andFonts,Images, andSoundsload and serve assets.SoundPoolmanages overlapping sound playback. - Rendering and game building blocks:
GraphicandSpritedescribe drawable image regions;Randomprovides random helpers;Vector,Collision,Particle, andParticleSystemsupport movement, effects, and collision work used in later lessons. - Motion:
Easingcontains interpolation curves for the tween lesson. It is available now but Match-3-0 does not animate tiles yet.
src/: Game-Dependent Code
src/ contains the code that defines this game:
config.js,assets.js, andenums.jsdefine dimensions, loaded resources, and stable names.objects/Board.jscreates and renders the tile grid;objects/Tile.jsrepresents one tile.states/PlayState.jsowns the current gameplay screen.globals.d.tsdeclares game-specific types used by typed JavaScript.
“Typed” JavaScript with JSDoc
This project is JavaScript, but its jsconfig.json enables checkJs and noImplicitAny. JSDoc comments provide the type information that TypeScript normally gets from .ts syntax. The editor can therefore catch mistakes while the browser still runs ordinary JavaScript.
/** @type {Board | undefined} */board;
/** @returns {void} */initializeBoard() { // Build the tile grid.}Use @type for fields, @param for function inputs, and @returns for return values. Types shared by the framework are declared in lib/globals.d.ts. For example, GameServices describes the injected canvas, context, input, images, sounds, stateMachine, and timer services.
For a more thorough walkthrough for JSDoc, watch this video.
globals.d.ts
A .d.ts file is a type declaration file. It contains type information only: it does not produce JavaScript and does not exist at runtime in the browser. globals.d.ts uses declare global to make shared type names (such as GameServices, SoundDefinition, and this lesson’s PlayStateProps) available throughout the typed JavaScript project.
JSDoc lives beside implementation. Use it to type a class field, method parameter, or return value where that JavaScript is written.
globals.d.ts centralizes reusable type shapes. Use it for types that multiple files need, especially types with several fields or types that describe framework services.
For example, Board.js can write @param {GameServices} game without repeating the full service object shape. The declaration file defines that shape once; JSDoc applies it where the value is used.
States and Transition Props
State is generic: State<Props> describes the data a state receives when it becomes active. Runtime services are injected in the constructor; short-lived transition data is passed to enter().
/** * @extends {State<PlayStateProps>} */export default class PlayState extends State { enter(props) { super.enter(props); }}PlayStateProps is declared in this lesson’s src/globals.d.ts:
type PlayStateProps = Record<string, never>;Record<Key, Value> is a TypeScript utility type for an object whose keys have type Key and whose values have type Value. Record<string, never> therefore describes an object that may have string keys, but cannot store a value for any of them because never is the type with no possible values.
In practice, it is a useful checked-JavaScript way to say “this state accepts an empty props object.” {} is valid; { level: 1 } is not, because 1 cannot be never. Use this instead of a loose object type when a state deliberately receives no transition data.
Match-3-0 does not need transition data, so main.js changes to PlayState without extra props. Later states can define a shape such as { level: number; score: number }; State.activeProps then exposes that typed data after super.enter(props) has stored it.
Board and Tiles
PlayState creates the centered board. The Board constructor generates its own grid, so the state only needs to render it.
this.board = new Board( this.game, Board.POSITION_CENTER.x, Board.POSITION_CENTER.y);Board.initializeBoard() creates an array for each row and fills it with randomized Tile instances. The grid is indexed as tiles[row][column]; a tile’s boardX is its column and boardY is its row. Board.render() walks that two-dimensional array and asks every tile to render its sprite.
for (let row = 0; row < this.height; row++) { for (let column = 0; column < this.width; column++) { const tile = this.tiles[row][column]; if (tile) tile.render(context, this.x, this.y); }}This complete, static board is the foundation for the cursor and swapping work in the next lesson.