近日接到騰訊 CDC 前端開發團隊的求職意向詢問,在微信上簡單地聊了下技術,而後拋給我一道面試題。題目內容是編寫一個單機五子棋,用原生 web 技術實現,兼容 Chrome 便可,完成時間不做限制。同時還有幾個要求:前端
- 實現勝負判斷,並給出贏棋提示。任意一方贏棋,鎖定棋盤。
- 儘量考慮遊戲的擴展性,界面可用 DOM/ Canvas 實現,而且切換實現方式代價最小。
- 實現悔棋和撤銷悔棋功能。
- 人機對戰部分可選。
工做上一直跟遊戲開發毫無關聯,本身也不怎麼熱衷玩遊戲,不過五子棋仍是玩過的。簡單構思了下,決定用 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); }
//棋盤 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; }
//棋子 function Piece(x, y, player) { this.x = x; this.y = y; this.player = player; }
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