腾讯前端面试题:一言不合就写个五子棋

近日接到腾讯 CDC 前端开发团队的求职意向询问,在微信上简单地聊了下技术,然后抛给我一道面试题。题目内容是编写一个单机五子棋,用原生 web 技术实现,兼容 Chrome 即可,完成时间不作限制。同时还有几个要求:

  1. 实现胜负判断,并给出赢棋提示。任意一方赢棋,锁定棋盘。
  1. 尽可能考虑游戏的扩展性,界面可用 DOM/ Canvas 实现,并且切换实现方式代价最小。
  2. 实现悔棋和撤销悔棋功能。
  3. 人机对战部分可选。

工作上一直跟游戏开发毫无关联,自己也不怎么热衷玩游戏,不过五子棋还是玩过的。简单构思了下,决定用 DOM 实现。当天晚上在家忙活了两个多小时,基本完成。最终效果图如下:

简易五子棋

在线 Demo

因为代码规模较小,总共不到 250 行,就没有考虑分文件模块化的设计,一个 game.js文件搞定。主要定义了 三个类:Board, Piece 和 Game,分别代表棋盘、棋子和整个游戏。

游戏状态数据

用一个二维数组保存棋盘数据,data[x][y] = 0表示该位置为空,data[x][y] = 1表示放置了黑子,data[x][y] = 2表示放置了白子。

下棋动作

监听棋盘的点击事件,计算出点位。玩家可能不是精确地点击交叉点,所以要进行纠偏计算。黑白交替进行,如果点位合法,就创建一个 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;
    }
    this.currentPlayer = this.currentPlayer === BLACK ? WHITE : BLACK;
    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.move.el.remove();
    var piece = this.move.piece;
    this.data[piece.x][piece.y] = 0;
}

Game.prototype.redo = function() {
    if(this.ended) {
        return;
    }
    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 实现。人机对战部分没有实现,没去研究五子棋赢棋策略。由于没花太多时间,代码比较粗糙,界面也比较丑。如果大家有更好的实现方式,欢迎交流。

后记
多年前也折腾过一些小游戏,比如:
7X7小游戏
用Vue.js和Webpack开发Web在线钢琴
止增笑耳。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • 为什突然做这个,因为这是个笔试题,拖了一个月才写(最近终于闲了O(∩_∩)O),废话不多说,说说这个题吧 题目要求...
    Stevenzwzhai阅读 2,725评论 0 5
  • 一、功能模块 先看下现在做完的效果: 二、代码详解 2.1 人机对战功能实现 从效果图可以看到,棋盘的横竖可以放的...
    eraser123阅读 1,464评论 4 19
  • 最近一直在学习Android自定义View方面的知识,正好看到一个讲解制作五子棋小游戏的案例,遂学习一番,记录下学...
    冰鉴IT阅读 3,135评论 5 16
  • 导读 五子棋是程序猿比较熟悉的一款小游戏,相信很多人大学时期就用多种语言写过五子棋小游戏.笔者工作闲暇之余,试着用...
    HelloYeah阅读 3,086评论 9 52
  • 1.旅游,和三五好友,来到西塘,一个小桥流水人家的水乡。碎石铺的小路,江南的白墙黑瓦,别致的小店,古朴的饭店,斜挑...
    疏韵86阅读 719评论 2 1