騰訊前端面試題:一言不合就寫個五子棋

近日接到騰訊 CDC 前端開發團隊的求職意向詢問,在微信上簡單地聊了下技術,而後拋給我一道面試題。題目內容是編寫一個單機五子棋,用原生 web 技術實現,兼容 Chrome 便可,完成時間不做限制。同時還有幾個要求:前端

  1. 實現勝負判斷,並給出贏棋提示。任意一方贏棋,鎖定棋盤。
  2. 儘量考慮遊戲的擴展性,界面可用 DOM/ Canvas 實現,而且切換實現方式代價最小。
  3. 實現悔棋和撤銷悔棋功能。
  4. 人機對戰部分可選。

工做上一直跟遊戲開發毫無關聯,本身也不怎麼熱衷玩遊戲,不過五子棋仍是玩過的。簡單構思了下,決定用 DOM 實現。當天晚上在家忙活了兩個多小時,基本完成。最終效果圖以下:web

簡易五子棋

在線 Demo面試

由於代碼規模較小,總共不到 250 行,就沒有考慮分文件模塊化的設計,一個 game.js文件搞定。主要定義了 三個類:Board, Piece 和 Game,分別表明棋盤、棋子和整個遊戲。數組

遊戲狀態數據

用一個二維數組保存棋盤數據,data[x][y] = 0表示該位置爲空,data[x][y] = 1表示放置了黑子,data[x][y] = 2表示放置了白子。微信

下棋動做

監聽棋盤的點擊事件,計算出點位。玩家可能不是精確地點擊交叉點,因此要進行糾偏計算。黑白交替進行,若是點位合法,就建立一個 DOM 元素表示棋子。app

悔棋與撤銷悔棋

每下一步棋,都保存當前棋子的座標和 DOM 元素引用。若是要悔棋,就把該位置的數據清零,同時把 DOM 移除掉。撤銷悔棋則執行相反的操做。dom

勝負斷定

按照簡單的規則,從當前下子點位的八個方向判斷。若是有一個方向知足連續5個黑子或白子,遊戲結束。模塊化

全局變量

var SIZE = 15;
var BLACK = 1;
var WHITE = 2;
var WIN = 5;

function approximate(number) {
    if(number - Math.floor(number) > 0.5) {
        return Math.ceil(number);
    }
    return Math.floor(number);
}

Board 類

//棋盤
function Board(el) {
    this.el = typeof el === 'string' ? document.querySelector(el) : el;
}

//初始化棋盤
Board.prototype.init = function() {
    this.el.innerHTML = '';
    var frag = document.createDocumentFragment();
    for (var i = SIZE - 1; i >= 0; i--) {
        var row = document.createElement('div');
        row.classList.add('row');
        for (var j = SIZE - 1; j >= 0; j--) {
            var cell = document.createElement('div');
            cell.classList.add('cell');
            row.appendChild(cell);
        }
        frag.appendChild(row);
    }
    this.el.appendChild(frag);
    var aCell = this.el.querySelector('.cell');
    var rect = aCell.getBoundingClientRect();
    var maxWidth = Math.min(document.body.clientWidth * 0.8, SIZE * 40);
    var w = ~~(maxWidth / (SIZE - 1));
    this.el.style.height = w * (SIZE - 1) + 'px';
    this.el.style.width = w * (SIZE - 1) + 'px';
    rect = aCell.getBoundingClientRect();
    this.unit = rect.width;
}

//畫棋子
Board.prototype.drawPiece = function(piece) {
    var dom = document.createElement('div');
    dom.classList.add('piece');
    dom.style.width = this.unit + 'px';
    dom.style.height = this.unit + 'px';
    dom.style.left = ~~((piece.x - .5) * this.unit) + 'px';
    dom.style.top = ~~((piece.y - .5) * this.unit) + 'px';
    dom.classList.add(piece.player === 1 ? 'black' : 'white');
    this.el.appendChild(dom);
    return dom;
}

Piece 類

//棋子
function Piece(x, y, player) {
    this.x = x;
    this.y = y;
    this.player = player;
}

Game 類

function Game(engine) {
    this.engine = engine || 'DOM';
    this.init();
}

Game.prototype.init = function() {
    this.ended = false;
    var chessData = new Array(SIZE);
    for (var x = 0; x < SIZE; x++) {
        chessData[x] = new Array(SIZE);
        for (var y = 0; y < SIZE; y++) {
            chessData[x][y] = 0;
        }
    }
    this.data = chessData;
    this.currentPlayer = WHITE;
    this.updateIndicator();
}

Game.prototype.start = function() {
    var board = new Board('.board');
    board.init();
    this.board = board;

    var rect = this.board.el.getBoundingClientRect();
    this.board.el.addEventListener('click', function(event) {
        var ptX = event.clientX - rect.left;
        var ptY = event.clientY - rect.top;
        var x = approximate(ptX / this.board.unit);
        var y = approximate(ptY / this.board.unit);
        console.log(x, y);
        this.play(x, y);
    }.bind(this));

    var btnUndo = document.querySelector('.undo');
    var btnRedo = document.querySelector('.redo');
    var btnRestart = document.querySelector('.restart');
    btnUndo.addEventListener('click', function() {
        this.undo();
    }.bind(this));

    btnRedo.addEventListener('click', function() {
        this.redo();
    }.bind(this));

    btnRestart.addEventListener('click', function() {
        this.init();
        this.board.init();
    }.bind(this));
}

Game.prototype.play = function(x, y) {
    if (this.ended) {
        return;
    }
    if (this.data[x][y] > 0) {
        return;
    }
    if(!this.lockPlayer) {
        this.currentPlayer = this.currentPlayer === BLACK ? WHITE : BLACK;
    }
    this.lockPlayer = false;
    var piece = new Piece(x, y, this.currentPlayer);
    var pieceEl = this.board.drawPiece(piece);
    this.data[x][y] = this.currentPlayer;
    this.updateIndicator();
    var winner = this.judge(x, y, this.currentPlayer);
    this.ended = winner > 0;
    if(this.ended) {
        setTimeout(function() {
            this.gameOver();
        }.bind(this), 0);
    }
    this.move = {
        piece: piece,
        el: pieceEl
    };
}

Game.prototype.updateIndicator = function() {
    var el = document.querySelector('.turn');
    if(this.currentPlayer === WHITE) {
        el.classList.add('black');
        el.classList.remove('white');
    } else {
        el.classList.add('white');
        el.classList.remove('black');
    }
}

Game.prototype.gameOver = function() {
    alert((this.currentPlayer === BLACK ? '黑方' : '白方') + '勝!');
}

Game.prototype.undo = function() {
    if(this.ended) {
        return;
    }
    this.lockPlayer = true;
    this.move.el.remove();
    var piece = this.move.piece;
    this.data[piece.x][piece.y] = 0;
}

Game.prototype.redo = function() {
    if(this.ended) {
        return;
    }
    this.lockPlayer = true;
    this.board.el.appendChild(this.move.el);
    var piece = this.move.piece;
    this.data[piece.x][piece.y] = piece.player;
}

//判斷勝負
Game.prototype.judge = function(x, y, player) {
    var horizontal = 0;
    var vertical = 0;
    var cross1 = 0;
    var cross2 = 0;

    var gameData = this.data;
    //左右判斷 
    for (var i = x; i >= 0; i--) {
        if (gameData[i][y] != player) {
            break;
        }
        horizontal++;
    }
    for (var i = x + 1; i < SIZE; i++) {
        if (gameData[i][y] != player) {
            break;
        }
        horizontal++;
    }
    //上下判斷 
    for (var i = y; i >= 0; i--) {
        if (gameData[x][i] != player) {
            break;
        }
        vertical++;
    }
    for (var i = y + 1; i < SIZE; i++) {
        if (gameData[x][i] != player) {
            break;
        }
        vertical++;
    }
    //左上右下判斷 
    for (var i = x, j = y; i >= 0, j >= 0; i--, j--) {
        if (gameData[i][j] != player) {
            break;
        }
        cross1++;
    }
    for (var i = x + 1, j = y + 1; i < SIZE, j < SIZE; i++, j++) {
        if (gameData[i][j] != player) {
            break;
        }
        cross1++;
    }
    //右上左下判斷 
    for (var i = x, j = y; i >= 0, j < SIZE; i--, j++) {
        if (gameData[i][j] != player) {
            break;
        }
        cross2++;
    }
    for (var i = x + 1, j = y - 1; i < SIZE, j >= 0; i++, j--) {
        if (gameData[i][j] != player) {
            break;
        }
        cross2++;
    }
    if (horizontal >= WIN || vertical >= WIN || cross1 >= WIN || cross2 >= WIN) {
        return player;
    }
    return 0;
}

啓動遊戲

document.addEventListener('DOMContentLoaded', function() {
    var game = new Game();
    game.start();
    console.log('DOMContentLoaded')
})

總結:總體仍是比較簡單的,遊戲邏輯已經抽象出來,界面部分可替換成 Canvas 實現。人機對戰部分沒有實現,沒去研究五子棋贏棋策略。因爲沒花太多時間,代碼比較粗糙,界面也比較醜。若是你們有更好的實現方式,歡迎交流。this

後記
多年前也折騰過一些小遊戲,好比:
7X7小遊戲
用Vue.js和Webpack開發Web在線鋼琴
止增笑耳。prototype

相關文章
相關標籤/搜索