基于react的经典2048游戏

react是一个强大的交互式UI渲染类库,非常适合一些对dom渲染频繁的场合,比如游戏。今天我来个大家简单介绍一下基于一个react的2048游戏,废话不多少直接开始
先感受一下效果


Animation.gif
  • 创建棋盘对象(4x4)
        var initial_board = {
            a1:null,a2:null,a3:null,a4:null,
            b1:null,b2:null,b3:null,b4:null,
            c1:null,c2:null,c3:null,c4:null,
            d1:null,d2:null,d3:null,d4:null
        };
  • 设置初始函数来判断哪个位置有棋子无棋子

//  获取所有无棋子的位置
function available_spaces(board){
      return Object.keys(board).filter(function(key){
                return board[key] === null
            });
        }
//  获取有棋子的位置
        function used_spaces(board){
            return Object.keys(board).filter(function(key){
                return board[key] !== null
            });
        }
//  创建新的棋子
        var tile_counter = 0;
        function new_tile(initial) {
            return {
                id:tile_counter++,
                values:[initial]//棋子的值是一个数组,两个棋子合拼时候,就会push新的数值,在页面上展示的最后一个数值
            }
        }
//  设置某一位置上的棋子的值
        function set_tile(board, where, tile) {
            var new_board = {};
            Object.keys(board).forEach(function (key) {
                new_board[key] = (key == where) ? tile : board[key]
            });
            return new_board
        }
//  返回棋子的值
        function tile_value(tile){
            return tile ? tile.values[tile.values.length-1] : null;
        }

当我们按下键盘右箭头→时,棋盘上的棋子分为4组

无标题.png
  • 当按方向键时,棋子分组
//  棋盘的分组函数
        function fold_order(xs, ys, reverse_keys){
            return xs.map(function(x){
                return ys.map(function(y){
                    var key = [x,y];
                    if(reverse_keys){
                        return key.reverse().join("");
                    }
                    return key.join("");
                });
            });
        }
//  按左方向键时的分组
        var left = fold_order(["a","b","c","d"], ["1","2","3","4"], false);
//  按右方向键时的分组
        var right = fold_order(["a","b","c","d"], ["4","3","2","1"], false);
//  按上方向键时的分组
        var up = fold_order(["1","2","3","4"], ["a","b","c","d"], true);
//  按下方向键时的分组
        var down = fold_order( ["1","2","3","4"], ["d","c","b","a"], true);
  • 处理整个棋盘
//   处理整个棋盘
        function fold_board(board, lines){
            //copy reference
            var new_board = board;

            lines.forEach(function(line){
                var new_line = fold_line(board, line);//fold_line函数处理每行棋子
                Object.keys(new_line).forEach(function(key){
                    //mutate reference while building up board
                    new_board = set_tile(new_board, key, new_line[key]);
                });
            });
            return new_board;
        }
//   处理棋盘的每行的棋子
        function fold_line(board, line) {
            var tiles = line.map(function(key){
                return board[key];
            }).filter(function(tile){
                return tile !== null
            });
            var new_tiles = [];
            if(tiles){
                //must loop so we can skip next if matched
                for(var i=0; i < tiles.length; i++){
                    var tile = tiles[i];
                    if(tile){
                        var val = tile_value(tile);
                        var next_tile = tiles[i+1];
                        if(next_tile && val == tile_value(next_tile)){
                            //skip next tile;
                            i++;
                            new_tiles.push({
                                id: next_tile.id, //keep id
                                values: tile.values.concat([val * 2])
                            });
                        }
                        else{
                            new_tiles.push(tile);
                        }
                    }
                }
            }
            var new_line = {};
            line.forEach(function(key, i){
                new_line[key] = new_tiles[i] || null;
            });
            return new_line;
        }
  • 当我们渲染之后,我们要将原来的棋盘和处理过的棋盘进行比较,判断是否相同,方便起见使用函数调用
//  判断两个棋盘是否相等,
        function same_board(board1, board2){
            return  Object.keys(board1).reduce(function(ret, key){
                return ret && board1[key] == board2[key];
            }, true);
        }
  • 开始创建一个Tiles组件,这个组件仅负责渲染棋盘
        var Tiles = React.createClass({
            render: function(){
                var board = this.props.board;//这个组件只负责渲染棋盘
                var tiles = used_spaces(board)
                return <div className="board">{
                    tiles.map(function(key){
                        var tile = board[key];
                        var val = tile_value(tile);
                        return <span key={tile.id} className={key + " value" + val}>
                        {val}
                       </span>;
                    })}</div>
            }
        });
  • 开始创建一个board组件,这个组件负责监听按键,创建棋子,改变棋盘,将新的棋盘交给Tiles组件处理
        var GameBoard = React.createClass({
            getInitialState: function(){ //初始创建2个棋子
                return this.addTile(this.addTile(initial_board));
            },
            keyHandler:function(e){//通过按键,如果生成新的棋盘,则在100ms后添加一个新的棋子
                var directions = { 37: left,38: up, 39: right, 40: down  };
                if(directions[e.keyCode]
                      && this.setBoard(fold_board(this.state, directions[e.keyCode]))
                    ){
                    setTimeout(function(){
                        this.setBoard(this.addTile(this.state));
                    }.bind(this), 100);
                }
            },
            setBoard:function(new_board){//通过same_board判断新棋盘和旧棋盘是否一样,一样就设置新棋盘返回true,不一样直接返回false
                if(!same_board(this.state, new_board)){
                    this.setState(new_board);
                    return true;
                }
                return false;
            },
            addTile:function(board){//添加棋子,值随机是2或4
                var location = available_spaces(board).sort(function() {
                    return .5 - Math.random();
                }).pop();
                if(location){
                    var two_or_four = Math.floor(Math.random() * 2, 0) ? 2 : 4;
                    return set_tile(board, location, new_tile(two_or_four));
                }
                return board;
            },
            newGame:function(){//重新初始化棋盘
                this.setState(this.getInitialState());
            },
            componentDidMount:function(){//对按键添加事件监听
                window.addEventListener("keydown", this.keyHandler, false);
            },
            render:function(){
                return <div className="app">
            <span className="score">
            </span>
                    <Tiles board={this.state}/>
                    <button onClick={this.newGame}>New Game</button>
                </div>
            }
        });
  • 添加动画样式,
        *{ margin: 0px;  padding: 0px;    }
        .app{   margin:10px; font-family: arial;  }
        .board{
            display:block;
            position:relative;
            margin:10px 0px 10px 0px;
            border:1px solid #ccc;
            width:215px;
            height:215px;
            padding:5px;
        }
        .board span{
            font-family: arial;
            letter-spacing: -1px;
            display:block;
            width:50px;
            height:36px;
            position:absolute;
            text-align:center;
            color:white;
            font-weight:bold;
            font-size:20px;
            padding-top:14px;
            background-color:#ebe76f;
            border-radius: 5px;
            transition: all 100ms linear;
        }
        .a1, .b1, .c1, .d1{ left:5px; }
        .a2, .b2, .c2, .d2{ left:60px; }
        .a3, .b3, .c3, .d3{ left:115px; }
        .a4, .b4, .c4, .d4{ left:170px; }
        .a1, .a2, .a3, .a4{ top:5px; }
        .b1, .b2, .b3, .b4{ top:60px; }
        .c1, .c2, .c3, .c4{ top:115px; }
        .d1, .d2, .d3, .d4{ top:170px; }
        span.value2{ background-color:#ebb26f; }
        span.value4{ background-color:#ea6feb; }
        span.value8{ background-color:#eb6fa3; }
        span.value16{ background-color:#7a6feb; }
        span.value32{ background-color:#af6feb; }
        span.value64{ background-color:#6febcf; }
        span.value128{ background-color:#6fbeeb; }
        span.value256{ background-color:#afeb6f; }
        span.value512{ background-color:#7aeb6f; }
        span.value1024{ background-color:#e4eb6f; }

现在大概的运行效果就有了,

Animation2.gif

添加计分功能和游戏结束功能先添加俩个函数

//  判断是否能移动
        function can_move(board){
            var new_board = [up,down,left,right].reduce(function(b, direction){
                return fold_board(b, direction);
            }, board);
            return available_spaces(new_board).length > 0
        }
//        获取总分书
        function score_board(board){
            return used_spaces(board).map(function(key){
                return (board[key].values.reduce(function(a, b) {
                    return a + b; //记录总分
                })) - board[key].values[0]; //减去创建时的分数
            }).reduce(function(a,b){return a+b}, 0);
        }
  • 之后在GameBoard组件中render函数修改
//GameBoard组件
render:function(){
                var status = !can_move(this.state)?" - Game Over!":"";
                return <div className="app">
            <span className="score">
                Score: {score_board(this.state)}{status}//这里显示分数和游戏结束
            </span>
                    <Tiles board={this.state}/>
                    <button onClick={this.newGame}>New Game</button>
                </div>
            }

一个原始2048小游戏的就基本出来,等有时间我再写个vue版的2048。工作中写页面难免有些无聊,平时多写一下小游戏锻炼一下思维也是挺好的。

--------------------------------------------------分割线---------------------------------------------------------

vue2的2048早就写完了,只是一直没时间上传,这里我直接列一下github地址:https://github.com/bfc846958672/vue2048

其实和react 差不多,只不过是一个组件,用v-for 根据棋盘数据 渲染dom,还有就是我把所有的方法都放到了vue里面的methods里面,其实没啥太大必要的

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

推荐阅读更多精彩内容