zoukankan      html  css  js  c++  java
  • 五子棋

    五子棋

     

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

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

    工作上一直跟游戏开发毫无关联,自己也不怎么热衷玩游戏,不过五子棋还是玩过的。简单构思了下,决定用 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;
        }
        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 实现。人机对战部分没有实现,没去研究五子棋赢棋策略。由于没花太多时间,代码比较粗糙,界面也比较丑。如果大家有更好的实现方式,欢迎交流。

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

  • 相关阅读:
    leetcode 279. Perfect Squares
    leetcode 546. Remove Boxes
    leetcode 312. Burst Balloons
    leetcode 160. Intersection of Two Linked Lists
    leetcode 55. Jump Game
    剑指offer 滑动窗口的最大值
    剑指offer 剪绳子
    剑指offer 字符流中第一个不重复的字符
    leetcode 673. Number of Longest Increasing Subsequence
    leetcode 75. Sort Colors (荷兰三色旗问题)
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/6943350.html
Copyright © 2011-2022 走看看