Frontend Development 21 min read

How to Build a Switchable DOM & Canvas Web Gomoku Game from Scratch

This tutorial walks through creating a lightweight web Gomoku game using JavaScript, detailing both a plain‑DOM renderer and a Canvas renderer, their interchangeable architecture, and core features like move handling, undo/redo, win detection, and board resetting.

WecTeam
WecTeam
WecTeam
How to Build a Switchable DOM & Canvas Web Gomoku Game from Scratch

Gomoku is a familiar mini‑game; this article explains how to create a simple web‑based Gomoku game with both ordinary DOM and Canvas UI rendering modes that can be switched at any time. The final effect can be seen at https://littuomuxin.github.io/gobang/.

Approach

The simplified version includes the following basic functions:

Play: Black and white players take turns placing a piece on the board.

Undo move: Before the opponent moves, a player may undo the last move.

Redo move: After undoing, the player can redo the move.

Win detection: Four win patterns—horizontal, vertical, diagonal, and anti‑diagonal—where five same‑color pieces align.

Restart: After a game ends, clear the board and start a new game.

The program is divided into a controller layer and a rendering layer. The controller implements the game logic and calls the renderer for drawing. Both DOM and Canvas renderers expose the same methods, allowing seamless switching.

Controller Implementation

The controller defines a

Gobang

class. Its constructor initializes private state such as board status, current role, move data, undo data, a 15×15 board array, and message strings used by a

notice

utility.

<code>function Gobang() {
    this._status = 0; // 0: in progress, 1: finished
    this._role = 0; // 0: black, 1: white
    this._chessDatas = [];
    this._resetStepData = [];
    this._gridNum = 15;
    this._chessBoardDatas = this._initChessBoardDatas();
    this._notice = window.notice;
    this._msgs = {
        'start': '比赛开始!',
        'reStart': '比赛重新开始!',
        'blackWin': '黑棋胜!',
        'whiteWin': '白棋胜!',
    };
}</code>

The controller exposes an

init(renderer)

method to start the game, bind events, and render the initial board.

<code>/**
 * Initialize the game
 * @param {Object} renderer Rendering instance
 */
Gobang.prototype.init = function(renderer) {
    var _this = this;
    setTimeout(function() { _this._notice.showMsg(_this._msgs.start, 1000); }, 1000);
    if (!renderer) throw new Error('缺少渲染器!');
    _this.renderer = renderer;
    renderer.renderChessBoard();
    renderer.bindEvents(_this);
};</code>

Key methods include:

goStep(x, y, normal)

– place a piece, update data, render the step, check win, switch role, and optionally clear undo data.

_hasChess(x, y)

– determine if a cell already contains a piece.

resetStep()

– undo the last move, restore board data, store undo information, and update the UI.

reResetStep()

– redo a previously undone move.

_isWin(x, y)

– evaluate four directions to see if five consecutive pieces of the same color exist.

clear()

– reset the entire game state and clear the board.

changeRenderer(renderer)

– switch between DOM and Canvas renderers by clearing the old board, initializing the new one, and re‑drawing existing moves.

Renderer Implementation

Both renderers share five required methods:

renderChessBoard – draw the empty board.

renderStep – draw a single piece.

renderUndo – remove a piece.

renderClear – clear all pieces.

bindEvents – handle user clicks (and mouse movement for Canvas).

DOM Renderer

The DOM renderer creates a 15×15 grid of

div

elements, each marked with an

attr-data

attribute indicating its position.

<code>function DomRenderer(container) {
    this._chessBoardWidth = 450;
    this._chessBoardPadding = 4;
    this._gridNum = 15;
    this._gridDoms = [];
    this._chessboardContainer = container;
    this.chessBoardRendered = false;
    this.eventsBinded = false;
}

DomRenderer.prototype.renderChessBoard = function() {
    var _this = this;
    _this._chessboardContainer.style.width = _this._chessBoardWidth + 'px';
    _this._chessboardContainer.style.height = _this._chessBoardWidth + 'px';
    _this._chessboardContainer.style.padding = _this._chessBoardPadding + 'px';
    var fragment = '';
    for (var i = 0; i < _this._gridNum * _this._gridNum; i++) {
        fragment += '<div class="chess-grid" attr-data="' + i + '"></div>';
    }
    _this._chessboardContainer.innerHTML = fragment;
    _this._gridDoms = _this._chessboardContainer.getElementsByClassName('chess-grid');
    _this.chessBoardRendered = true;
};

DomRenderer.prototype.renderStep = function(step) {
    if (!step) return;
    var index = step.x + this._gridNum * step.y;
    var domGrid = this._gridDoms[index];
    domGrid.className = 'chess-grid ' + (step.role ? 'white-chess' : 'black-chess');
};

DomRenderer.prototype.renderUndo = function(step) {
    if (!step) return;
    var index = step.x + this._gridNum * step.y;
    this._gridDoms[index].className = 'chess-grid';
};

DomRenderer.prototype.renderClear = function() {
    for (var i = 0; i < this._gridDoms.length; i++) {
        this._gridDoms[i].className = 'chess-grid';
    }
};

DomRenderer.prototype.bindEvents = function(controllerObj) {
    var _this = this;
    _this._chessboardContainer.addEventListener('click', function(ev) {
        var target = ev.target;
        var attrData = target.getAttribute('attr-data');
        if (attrData === undefined || attrData === null) return;
        var position = attrData - 0;
        var x = position % _this._gridNum;
        var y = parseInt(position / _this._gridNum, 10);
        controllerObj.goStep(x, y, true);
    }, false);
    _this.eventsBinded = true;
};</code>

Canvas Renderer

The Canvas renderer creates three stacked

canvas

elements for background, shadow, and pieces. It draws pieces as circles on the piece canvas.

<code>function CanvasRenderer(container) {
    this._chessBoardWidth = 450;
    this._chessBoardPadding = 4;
    this._gridNum = 15;
    this._padding = 4;
    this._gridWidth = 30;
    this._chessRadius = 13;
    this._container = container;
    this.chessBoardRendered = false;
    this.eventsBinded = false;
    this._init();
}

CanvasRenderer.prototype._init = function() {
    var width = this._chessBoardWidth + this._chessBoardPadding * 2;
    this._bgCanvas = document.createElement('canvas');
    this._bgCanvas.setAttribute('width', width);
    this._bgCanvas.setAttribute('height', width);
    this._shadowCanvas = document.createElement('canvas');
    this._shadowCanvas.setAttribute('width', width);
    this._shadowCanvas.setAttribute('height', width);
    this._chessCanvas = document.createElement('canvas');
    this._chessCanvas.setAttribute('width', width);
    this._chessCanvas.setAttribute('height', width);
    this._container.appendChild(this._bgCanvas);
    this._container.appendChild(this._shadowCanvas);
    this._container.appendChild(this._chessCanvas);
    this._context = this._chessCanvas.getContext('2d');
};

CanvasRenderer.prototype.renderStep = function(step) {
    if (!step) return;
    var x = this._padding + (step.x + 0.5) * this._gridWidth;
    var y = this._padding + (step.y + 0.5) * this._gridWidth;
    this._context.beginPath();
    this._context.arc(x, y, this._chessRadius, 0, 2 * Math.PI);
    this._context.fillStyle = step.role ? '#FFFFFF' : '#000000';
    this._context.fill();
    this._context.closePath();
};

CanvasRenderer.prototype.renderClear = function() {
    this._chessCanvas.height = this._chessCanvas.height; // fast clear
};

CanvasRenderer.prototype.renderUndo = function(step, allSteps) {
    if (!step) return;
    this._chessCanvas.height = this._chessCanvas.height;
    if (allSteps.length < 1) return;
    var _this = this;
    allSteps.forEach(function(p) { _this.renderStep(p); });
};

CanvasRenderer.prototype._inRange = function(x, y) {
    return x >= 0 && x < this._gridNum && y >= 0 && y < this._gridNum;
};

CanvasRenderer.prototype.bindEvents = function(controllerObj) {
    var _this = this;
    var shadowCtx = this._shadowCanvas.getContext('2d');
    document.body.addEventListener('mousemove', function(ev) {
        if (ev.target.nodeName !== 'CANVAS') {
            _this._shadowCanvas.style.display = 'none';
        }
    }, false);
    this._container.addEventListener('mousemove', function(ev) {
        var i = Math.floor((ev.offsetX - _this._padding) / _this._gridWidth);
        var j = Math.floor((ev.offsetY - _this._padding) / _this._gridWidth);
        var x = _this._padding + (i + 0.5) * _this._gridWidth;
        var y = _this._padding + (j + 0.5) * _this._gridWidth;
        _this._shadowCanvas.style.display = 'block';
        _this._shadowCanvas.height = _this._shadowCanvas.height;
        if (!_this._inRange(i, j)) return;
        if (controllerObj._chessBoardDatas[i][j] !== undefined) return;
        shadowCtx.beginPath();
        shadowCtx.arc(x, y, _this._gridWidth / 2, 0, 2 * Math.PI);
        shadowCtx.fillStyle = 'rgba(0, 0, 0, 0.2)';
        shadowCtx.fill();
        shadowCtx.closePath();
    }, false);
    this._container.addEventListener('click', function(ev) {
        var i = Math.floor((ev.offsetX - _this._padding) / _this._gridWidth);
        var j = Math.floor((ev.offsetY - _this._padding) / _this._gridWidth);
        var success = controllerObj.goStep(i, j, true);
        if (success) {
            _this._shadowCanvas.height = _this._shadowCanvas.height;
        }
    }, false);
    this.eventsBinded = true;
};</code>

Switching Rendering Modes

To switch renderers, the controller calls

changeRenderer(newRenderer)

, which clears the old board, initializes the new renderer if needed, binds events, and re‑draws all existing moves.

<code>/**
 * Switch renderer
 * @param {Object} renderer New rendering instance
 */
Gobang.prototype.changeRenderer = function(renderer) {
    if (!renderer) return;
    this.renderer = renderer;
    renderer.renderClear();
    if (!renderer.chessBoardRendered) renderer.renderChessBoard();
    if (!renderer.eventsBinded) renderer.bindEvents(this);
    this._chessDatas.forEach(function(step) { renderer.renderStep(step); });
};</code>

Conclusion

Creating a full‑featured web Gomoku game would also involve networked multiplayer and AI opponents; this article focuses on a simple version that demonstrates multi‑renderer architecture and dynamic switching.

Gomoku board example
Gomoku board example
javascriptcanvasDOMWeb GameGomoku
WecTeam
Written by

WecTeam

WecTeam (维C团) is the front‑end technology team of JD.com’s Jingxi business unit, focusing on front‑end engineering, web performance optimization, mini‑program and app development, serverless, multi‑platform reuse, and visual building.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.