Author: Ma Xueqin

Gobang is a small game we are very familiar with. This article introduces how to make a simple web version of gobang game, and consider the implementation of ordinary DOM and Canvas two UI drawing modes for switching at any time. Finally the implementation effect of reference: littuomuxin. Making. IO/gobang.

Train of thought

The simple version of backgammon mainly contains the following basic functions:

  1. Chess: A game of backgammon consists of two sides, black and white, each side in turn placing a piece on the board
  2. Contrite: after one side has dropped a piece on the board, the other side is allowed to contrite before the piece has been dropped
  3. Revocation: When revocation, you can also reposition the pieces in the position before revocation
  4. Judge the outcome: there are 4 ways to win, the same color of the chess pieces in horizontal, vertical, oblique, oblique any direction into 5, its representative of the side that wins
  5. Replay: After a game has been decided, the pieces on the board can be cleared and a new game can be played

In code design, we divide the whole program into control layer and rendering layer, the controller is responsible for the logic implementation, and through the call renderer to achieve the rendering work. When it comes to web drawing, simple effects can be achieved through plain DOM, but if graphics are too complex, we should consider a more professional drawing API, such as Canvas. This article will implement both the normal DOM and Canvas versions of the renderer and show you how to easily switch between the two.

Controller implementation

The controller defines a five-piece board game called Gobang. To do this, you need to define the following private states and data in the controller class constructor: chess state, chess role, chess data, regret data, and so on. In addition, you need to initialize the checkerboard data. In this case, the implementation is a 15 by 15 checkerboard, so you need to initialize a 15 by 15 two-dimensional array. Finally, some game tricks are defined, which can be used to call the notice method of another implementation during the game for corresponding notification.

The constructor implementation code is as follows:

function Gobang() { this._status = 0; This. _role = 0; // Chess role, 0 for black, 1 for white this._chessDatas = []; This. _resetStepData = []; This._gridnum = 15; This._chessboarddatas = this._initchessboardDatas (); // Initialize the checkerboard data this._notice = window.notice; this._msgs = {'start': 'Game on! '.'reStart': 'Race starts again! '.'blackWin': 'Black wins! '.'whiteWin': 'White wins! '}; }Copy the code

The controller then exposes an instance method for external initialization calls, and relies on an instance of a renderer passed in from the outside, which draws in the backgammon by calling its various methods. The code looks like this:

/** * @param {Object} renderer */ gobang.prototype.init =function(renderer) { var _this = this; // Start the gamesetTimeout(function() {
        _this._notice.showMsg(_this._msgs.start, 1000);
    }, 1000);

    if(! renderer) throw new Error('Missing renderer! '); _this.renderer = renderer; renderer.renderChessBoard(); // Draw the board renderer.bindevents (_this); // bind events};Copy the code

The constructor and initialization method, after the next chess, back, and cancel the back, to determine the outcome and replay all operations are private state and data within the controller changes, at the same time, and then call the renderer drawing work accordingly.

The first is the implementation of chess method goStep. When playing chess, it is necessary to determine whether there are chess pieces in the corresponding position (_hasChess). Only when there are no chess pieces can the chess pieces be dropped. After the chess is dropped, it is necessary to update the board data (_chessBoardDatas) and chess data (_chessDatas). Renderer method _this.renderer. RenderStep is called to update the drawing interface. Then it is necessary to judge whether the game has been decided (_isWin), use the notice method to give corresponding hints when the game has been decided, and finally switch the role of the chess game (_role). The code is as follows:

@param {Number} x horizontal coordinates @param {Number} y vertical coordinates @returns {Boolean} Initial board data */ Gobang.prototype._hasChess =function(x, y) {
    var _this = this;
    var hasChess = false;
    _this._chessDatas.forEach(function(item) {
        if (item.x === x && item.y === y) hasChess = true;
    });
    returnhasChess; }; @param {Number} x horizontal coordinate @param {Number} y vertical coordinate @param {Boolean} normal normal chess, @returns {Boolean} whether to play successfully */ gobang.prototype. goStep =function(x, y, normal) {
    var _this = this;
    if (_this._status) return false;
    if (_this._hasChess(x, y)) return false; _this._chessBoardDatas[x][y] = _this._role; var step = { x: x, y: y, role: _this._role }; _this._chessDatas.push(step); / / stored in localstoragelocalStorage && (localStorage.chessDatas = JSON.stringify(_this._chessDatas)); _this.renderer.renderstep (step); // Decide whether to winif (_this._isWin(step. X, step. Y)) {// win _this._status = 1; var msg = _this._role ? _this._msgs.whiteWin : _this._msgs.blackWin;
        setTimeout(function() { _this._notice.showMsg(msg, 5000); }, 500); } // Switch roles _this._role = 1 - _this._role; // Clear the error dataif (normal) _this._resetStepData = [];
    return true;
};
Copy the code

ResetStep is the inverse operation of chess. It is necessary to perform a POP operation on chess data array _chessDatas, restore the array elements corresponding to board data _chessBoardDatas to their initial values, and store the data _resetStepData. Renderer. renderUndo is called to update the drawing interface by switching to _role.

/ * * * regrets move * / Gobang. Prototype. ResetStep =function() {
    var _this = this;
    if (_this._chessDatas.length < 1) return; _this._status = 0; Var lastStep = _this._chessdatas.pop (); / / stored in localstoragelocalStorage && (localStorage.chessDatas = JSON.stringify(_this._chessDatas)); _this._chessBoardDatas[laststep.x][laststep.y] = undefined; _this._resetStepdata.push (lastStep); _this._role = 1 - _this._role; _this.renderer.renderUndo(lastStep, _this._chessdatas); };Copy the code

ReResetStep is the reverse operation of the reResetStep, which is the same as a chess operation, except that the position of the reResetStep is automatically retrieved from the _resetStepData:

/ withdraw back * * * * / Gobang. Prototype. ReResetStep =function() {
    var _this = this;
    if (_this._resetStepData.length < 1) return; var lastStep = _this._resetStepData.pop(); _this.goStep(lastStep.x, lastStep.y); _this.renderer.renderstep (lastStep); };Copy the code

Next, the realization of the method _isWin is introduced. We know that there are four ways to win in gobang, that is, the pieces of the same color in horizontal, vertical, diagonal, diagonal, any one direction into 5, the side on behalf of that is to win. Therefore, after the current piece is set, we need to calculate the number of pieces of the same color connected with it from four directions according to the position of the piece. The specific implementation code is as follows:

@param {Number} x horizontal coordinates @param {Number} y vertical coordinates @returns {Boolean} Specifies whether the coordinates are within the checkerboard range */ Gobang.prototype._inRange =function(x, y) {
    returnx >= 0 && x < this._gridNum && y >= 0 && y < this._gridNum; }; @param {Number} xPos horizontal coordinate @param {Number} yPos vertical coordinate @param {Number} deltaX horizontal direction of movement * @param {Number} deltaY vertical movement * @returns {Number} the same Number of pieces as the Number of pieces in the given position */ gobang.prototype. _getCount =function(xPos, yPos, deltaX, deltaY) {
    var _this = this;
    var count = 0;
    while (true) {
        xPos += deltaX;
        yPos += deltaY;
        if(! _this._inRange(xPos, yPos) || _this._chessBoardDatas[xPos][yPos] ! = _this._role)break;
        count++;
    }
    returncount; }; @param {Number} x horizontal coordinates * @param {Number} y vertical coordinates * @param {Object} direction direction * @returns {Boolean} Whether to win in a certain direction */ gobang.prototype._iswinInDirection = function(x, y, direction) {
    var _this = this;
    var count = 1;
    count += _this._getCount(x, y, direction.deltaX, direction.deltaY);
    count += _this._getCount(x, y, -1 * direction.deltaX, -1 * direction.deltaY);
    returncount >= 5; }; @param {Number} x horizontal coordinates * @param {Number} y vertical coordinates * @returns {Boolean} whether to win */ gobang.prototype. _isWin = function(x, y) {
    var _this = this;
    var length = _this._chessDatas.length;
    if (length < 9) return0; Var directions = [{deltaX: 1, deltaY: 0}, {deltaX: 0, deltaY: 1}, {deltaX: 1, deltaY: 1}, {deltaX: 1, deltaY: 1}, {deltaX: 1, deltaY: 1}, {deltaX: 1, deltaY: 1} 1 }, { deltaX: 1, deltaY: -1 }];for (var i = 0; i < 4; i++) {
        if (_this._isWinInDirection(x, y, directions[i])) {
            return true; }}};Copy the code

Finally, when a game has been won or lost, we can start a new game by clearing all the data and drawing:

/** * clear everything and start again */ gobang.prototype.clear =function() {
    var _this = this;
    _this._status = 0;
    _this._role = 0;
    if (_this._chessDatas.length < 1) return; / / remove pieces _this. The renderer. RenderClear (); _this._chessDatas = [];localStorage && (localStorage.chessDatas = ' ');
    this._resetStepData = [];
    _this._chessBoardDatas = _this._initChessBoardDatas();
    _this._notice.showMsg(_this._msgs.reStart, 1000);
};
Copy the code

Renderer implementation

The renderer’s work mainly includes the following:

  1. The drawing of the checkerboard
  2. The drawing of the next piece
  3. Regret a chess piece drawing work
  4. Clear all pieces of the drawing work
  5. Checkerboard interface event interaction works: the user clicks on a position in the board to drop a move

The event interaction work needs to call the controller to control the chess-playing logic.

Because you need to implement both the normal DOM and Canvas versions of the renderer and allow the controller to switch flexibly, both renderers need to expose the same instance methods. Based on the five jobs of the renderer described above, it needs to expose the five methods as follows:

  1. renderChessBoard
  2. renderStep
  3. renderUndo
  4. renderClear
  5. bindEvents

Below are the concrete implementations of the normal DOM renderer and Canvas renderer respectively.

Normal DOM renderer

A normal DOM renderer needs to draw a 15 * 15 grid of 15 * 15 div elements, and each element can be initialized to indicate its grid position by defining the attr-data attribute. The related implementation is as follows:

/** * The normal Dom version of the backgammon renderer constructor * @param {Object} container renders the Dom container */functionDomRenderer(container) { this._chessBoardWidth = 450; // Width this._chessBoardPadding = 4; // This._gridNum = 15; This._griddoms = []; DOM this._chessboardContainer = container; // The container this.chessboardRendered =false; // Whether the board is rendered this.eventsbinded =false; } / / / whether the binding events apply colours to a drawing board * * * * / 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';
    _this._chessboardContainer.style.backgroundImage = 'url(./imgs/board.jpg)';
    _this._chessboardContainer.style.backgroundSize = 'cover';

    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;
};

Copy the code

The div corresponding to each grid has three states, including no chess pieces, black chess and white chess. These three states can be realized by adding three different styles to the div. Then, the drawing of the next piece and a piece is achieved by switching the style of the corresponding div. To clear all the pieces, restore all div styles to a state with no pieces:

/ render step chess pieces * * * * @ param {Object} step chess position. * / DomRenderer prototype. RenderStep =function(step) {
    var _this = this;

    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'); }; / regret step chess pieces * * * * @ param {Object} step chess position * @ param Array allSteps attach. The location of all the rest of the game * / DomRenderer prototype. RenderUndo =function(step) {
    var _this = this;

    if(! step)return;
    var index = step.x + _this._gridNum * step.y;
    var domGrid = _this._gridDoms[index];
    domGrid.className = 'chess-grid'; }; / * * * remove all pieces * / DomRenderer prototype. RenderClear =function() {
    var _this = this;

    for (var i = 0; i < _this._gridDoms.length; i++) {
        _this._gridDoms[i].className = 'chess-grid'; }};Copy the code

Finally, it is the event interaction of the checkerboard interface. When users click any grid div, they need to make a response. The response event is the next move, which is realized through the goStep method of the passed controller object. For performance, instead of binding a click event to each checkerboard grid div, we can bind a click event to the checkerboard container, which can be easily calculated using the real Target’s attr-data property and passed to the goStep method. Here is the concrete implementation:

/ * * * * @ binding events param {Object} * / DomRenderer controllerObj controller Object. The 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;
};
Copy the code

Canvas renderer

Next is the concrete implementation of the Canvas renderer. For the sake of performance, we can superimpose multiple Canvas canvases to achieve the entire drawing effect. Each Canvas is responsible for the drawing of a single element, and constant elements and changing elements are drawn to different canvases as far as possible. In this example, three canvases are created: a canvas for the background, a canvas for the shadows, and a canvas for the pieces. The relevant implementation code is as follows:

/** * Canvas version of gobang renderer constructor * @param {Object} container render in the DOM container */functionCanvasRenderer(container) { this._chessBoardWidth = 450; // Width this._chessBoardPadding = 4; // This._gridNum = 15; This._padding = 4; This. _gridWidth = 30; // Checkerboard width this._chessRadius = 13; // The radius of the piece this._container = container; // Create the Canvas DOM container this.chessboardRendered =false; // Whether the board is rendered this.eventsbinded =false; // whether the event this._init() is bound; } / * * * initialization, create a canvas. * / CanvasRenderer prototype. _init =function() { var _this = this; 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');
};
Copy the code

The drawing process of chess pieces is to use the 2D drawing environment of chess canvas to draw a circle, and the specific code is as follows:

/ render step chess pieces * * * * @ param {Object} step chess position. * / CanvasRenderer prototype. RenderStep =function(step) {
    var _this = this;

    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);if (step.role) {
        _this._context.fillStyle = '#FFFFFF';
    } else {
        _this._context.fillStyle = '# 000000';
    }
    _this._context.fill();
    _this._context.closePath();
};
Copy the code

Because the pieces are all drawn on one canvas, clearing all the pieces is as simple as clearing the entire canvas’s drawing. Because the Canvas content is cleared when the width or height is reset, you can quickly clear the Canvas using the following methods:

/ * * * remove all pieces * / CanvasRenderer prototype. RenderClear =function() { this._chessCanvas.height = this._chessCanvas.height; // Quickly clear the canvas};Copy the code

Regret move is a bit more complicated. The solution we take is to clear the entire canvas and then redraw the previous chess state:

/ regret step chess pieces * * * * @ param {Object} step current this move the location of the * @ param Array allSteps attach. The location of all the rest of the game * / CanvasRenderer prototype. RenderUndo =function(step, allSteps) {
    var _this = this;

    if(! step)return; _this._chessCanvas.height = _this._chessCanvas.height; // Clear the canvas quicklyif (allSteps.length < 1) return; / / redraw allSteps. ForEach (function(p) {
        _this.renderStep(p);
    });
};
Copy the code

Finally, event interaction: draw shadows as the mouse moves over the board; Mouse click on the board, through the incoming controller object’s goStep method to achieve chess operation, can successfully draw, but also need to pay attention to remove the shadow. The concrete implementation is as follows:

@param {Number} x horizontal coordinates @param {Number} y vertical coordinates @returns {Boolean} Specifies whether the coordinates are within the checkerboard range */ CanvasRenderer.prototype._inRange =function(x, y) {
    returnx >= 0 && x < this._gridNum && y >= 0 && y < this._gridNum; }; / * * * * @ binding events param {Object} * / CanvasRenderer controllerObj controller Object. The prototype. BindEvents =function(controllerObj) {
    var _this = this;

    var chessShodowContext = _this._shadowCanvas.getContext('2d'); Remove hidden when the canvas painting shadow / / mouse canvas document. The body. The addEventListener ('mousemove'.function(ev) {
        if(ev.target.nodeName ! = ='CANVAS') {
            _this._shadowCanvas.style.display = 'none'; }},false); / / the mouse moves to draw the canvas shadows _this. _container. AddEventListener ('mousemove'.function(ev) { var xPos = ev.offsetX; var yPos = ev.offsetY; var i = Math.floor((xPos - _this._padding) / _this._gridWidth); var j = Math.floor((yPos - _this._padding) / _this._gridWidth); Var x = _this._padding + (I + 0.5) * _this._gridWidth; Var y = _this._padding + (j + 0.5) * _this._gridWidth; / show/draw a shadow of the canvas. _this _shadowCanvas. Style. The display ='block'; _this._shadowcanvas. Height = _this._shadowcanvas. Height; // Do not drop shadows outside the checkerboard rangeif(! _this._inRange(i, j))return; // No shadow effect where there are piecesif(controllerObj._chessBoardDatas[i][j] ! == undefined)return;

        chessShodowContext.beginPath();
        chessShodowContext.arc(x, y, _this._gridWidth / 2, 0, 2 * Math.PI);
        chessShodowContext.fillStyle = 'rgba (0, 0, 0, 0.2)';
        chessShodowContext.fill();
        chessShodowContext.closePath();
    }, false); / / mouse is playing chess board click _this _container. AddEventListener ('click'.function(ev) {
        var x = ev.offsetX;
        var y = ev.offsetY;
        var i = Math.floor((x - _this._padding) / _this._gridWidth);
        var j = Math.floor((y - _this._padding) / _this._gridWidth);
        var success = controllerObj.goStep(i, j, true);
        if (success) {
            // 清除阴影
            _this._shadowCanvas.height = _this._shadowCanvas.height;
        }
    }, false);

    _this.eventsBinded = true;
};
Copy the code

Toggle drawing mode

The two drawing modes can be switched at any time. The renderer is called by the controller, so a method to switch the renderer needs to be exposed in the controller. There are three steps to switching renderers:

  1. The old renderer removes all of its drawing work
  2. New renderer initializes checkerboard drawing
  3. Redraws the current board game based on played data

The concrete implementation is as follows:

/ switch the renderer * * * * @ param {Object} the renderer renderer Object * / Gobang. Prototype. ChangeRenderer =function(renderer) {
    var _this = this;

    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);
    });
};
Copy the code

Because the two renderers expose exactly the same instance methods that the controller can call, these simple steps make the switch seamless, and the chess game can continue!

conclusion

To complete the production of a web backgammon game products, also need to consider the network, AI, etc. This paper is just a simple version of the web backgammon implementation, the focus is on the realization of multiple renderer and its switch, hoping to play a little reference significance in this respect.


If you think this article is valuable to you, please like it and pay attention to our official website and WecTeam. We have quality articles every week: