Riot.jsでブロックを落とすゲーム

背景

前回、蛇のゲームつくったので他のも作ってみようと思った。
そして完成したものは下記
ブロック落とすゲーム
http://hasito.com/block-down/

※蛇のゲーム
https://qiita.com/hashito/items/a38a25665ad7befb5483
http://hasito.com/snake/

仕様

  • ブロックは2個1組で落下で回転可能
  • 色はランダムで決定
  • 同一色4つがつながった場合に削除
  • 下のブロックが消えた場合は落下

コード

ブロックオブジェクト

  • 位置情報、色情報、ポーズ情報を保持
  • 実態としてマップは参照しない
//色は3種類として、poseは全4種類と定義  
    var colors = [0xff0000,0xffff00,0x0000ff];  
    var poses  = [[[0,0],[-1,0]],[[0,0],[0,-1]],[[-1,0],[0,0]],[[0,-1],[0,0]]];  
    var Blok=(point,pose,cols)=>{  
//初期化、とりあえず指定ない場合はランダム  
      var _point  = point;  
      var _pose   = (pose!=undefined)?pose:Math.floor(Math.random()*poses.length);  
      var _colors = (cols!=undefined)?cols:[colors[Math.floor(Math.random()*colors.length)],colors[Math.floor(Math.random()*colors.length)]];  
      return {  
//Point取得関数  
        point   :()=>{return _point; },  
//pose取得関数  
        pose    :()=>{return poses[_pose];  },  
//次のposeへ変更  
        poseNext:()=>{  
          _pose = (++_pose<poses.length)?_pose:0;  
          },  
//color取得関数  
        colors  :()=>{return _colors;},  
//ブロック位置情報取得  
        get     :function(){return this._get([0,0]);},  
//特定の場所分移動させた場合のPointの取得  
        _get    :(p)=>{  
          return  [  
            [poses[_pose][0][0]+_point[0]+p[0],poses[_pose][0][1]+_point[1]+p[1]],  
            [poses[_pose][1][0]+_point[0]+p[0],poses[_pose][1][1]+_point[1]+p[1]]  
          ];  
        },  
//ポイント移動用関数  
        set     :(p)=>{_point = [_point[0]+p[0],_point[1]+p[1]];}  
      };  
    };  

ゲームオブジェクト

  • メインのオブジェクト
  • 描画先マップ、仮想のマップ、ブロック、ゲーム状態などを保持
    var Game=(view)=>{//view=描画用2次元配列  
      var _top_buff = 2;//高さの余分な部分(不要?)  
      var _w        = view.length;//幅情報(描画範囲を参照)  
      var _h        = view[0].length+_top_buff;//縦幅  
      var _map      = false;//マップの実態 初期化されるまではfalse  
      var _blok     = false;//ブロックの実態 初期化されるまではfalse  
      var _view     = view;//表示用マップ  
      var _status   = 0;//ゲームステータス 0:停止,1:ゲーム中,2:ゲーム終了  
      var _his      = [];//削除履歴 cnt:連鎖情報,delblock:削除ブロック数,step:何step目情報  
      var _step_cnt = 0;//stepカウント  
      return {  
//削除履歴取得  
        get_his:function(){  
          return _his;  
        },  
//表示処理  
        drawn:function(){  
          _map.forEach((w,wi)=>{  
              w.forEach((h,hi)=>{  
                if(hi<_top_buff){  
                  return;  
                }else{  
                  _view[wi][hi-_top_buff]=h;  
                }  
              });  
          });  
          /*blockの表示 */  
          var bp  = _blok._get([0,-_top_buff]);  
          var bpc = this.map2block_chk(bp);  
          if(bpc[0]===0)_view[bp[0][0]][bp[0][1]]=_blok.colors()[0];  
          if(bpc[1]===0)_view[bp[1][0]][bp[1][1]]=_blok.colors()[1];  
        },  
//ブロック初期化関数   
//戻り値として出現場所にブロックがあるかを返す(呼び出し元で参照しゲーム終了か判断するのに使っています…)  
        block_init:function(){  
          var p=[Math.floor(_w/2),1];  
          _blok=Blok(p);  
          return (this.map2block_chk([p])[0]===0);  
        },  
//マップ初期化関数  
        map_init:()=>{  
          _map=new Array(_w);  
          for(i=0;i<_w;i++){  
            _map[i] = new Array(_h);  
            _map[i].fill(0);  
          }  
        },  
//マップ情報確認関数  
//ポイントの配列で対象箇所にブロック(数値)があるか枠外(false)かを返します  
        map2block_chk:function(points,f){  
          var ret=[];  
          points.forEach(v=>{  
            var f1 = _w >  v[0];  
            var f2 = 0  <= v[0];  
            var f3 = _h >  v[1];  
            var f4 = 0  <= v[1];  
            ret.push(f1&&f2&&f3&&f4&&(_map[v[0]][v[1]]));  
          });  
          return ret;  
        },  
// 削除対象ブロック検索処理  
// 戻り値は削除件数  
        map2dels:function (){  
          var _search=(p,chain,c,zumi)=>{  
            var cc = zumi.filter((v)=>{return (v[0]===p[0])&&(v[1]===p[1]);})  
            zumi.push(p);  
            if((cc.length==0)&& this.map2block_chk([p])[0]&&(_map[p[0]][p[1]]==c)){  
              chain.push(p);  
              _search([p[0]+1,p[1]  ],chain,c,zumi);  
              _search([p[0]-1,p[1]  ],chain,c,zumi);  
              _search([p[0]  ,p[1]+1],chain,c,zumi);  
              _search([p[0]  ,p[1]-1],chain,c,zumi);  
            }  
          };  
          // 削除処理  
          var dels=[];  
          _map.forEach((w,wi)=>{  
            w.forEach((h,hi)=>{  
              var _chain = [];  
              var _zumi  = dels.slice(0, dels.length);;  
              (h)&&_search([wi,hi],_chain,h,_zumi);  
              if(_chain.length>=4){  
                dels = _chain.concat(dels);  
              }  
            });  
          });  
          return dels;    
        },  
// 降下処理  
        map2down:function(){  
          var cnt=0;  
          _map.forEach((w,wi)=>{  
            w.forEach((h,hi)=>{  
              var f={};  
              var np    = [wi,hi+1];  
              var npchk = this.map2block_chk([np],f)[0];  
              var f1 = h;  
              var f2 = (npchk===0);  
              if(f1 && f2){  
                cnt++;  
                _map[np[0]][np[1]]=_map[wi][hi];  
                _map[wi][hi]=0;  
              }  
            });  
          });  
          return cnt;  
        },  
//ブロック移動イベント  
        left:function(){  
          var c=this.map2block_chk(_blok._get([-1,0]));  
          if((c[0]===0) && (c[1]===0)){_blok.set([-1,0])}  
        },  
//ブロック移動イベント  
        right:function(){  
          var c=this.map2block_chk(_blok._get([1,0]));  
          if((c[0]===0) && (c[1]===0)){_blok.set([1,0])}  
        },  
//ブロック降下イベント  
        down:function(){  
            var p=_blok.get();  
            _map[p[0][0]][p[0][1]]=_blok.colors()[0];  
            _map[p[1][0]][p[1][1]]=_blok.colors()[1];  
            this.block_init();  
        },  
//ブロック回転イベント  
        space:()=>{  
          _blok.poseNext();  
        },  
//ブロック/マップ初期化関数  
        init:function(){  
          this.map_init();  
          this.block_init();  
        },  
//ステータスの変更  
        statusChange:function(){  
          if(++_status>=3){  
            _status=0;  
            this.init();  
          }  
        },  
//step処理(main)  
        step:function(){  
          _step_cnt++;  
          if(_status!=1) return;  
          // 降下処理  
          var f = {};  
          var c = this.map2block_chk(_blok._get([0,1]),f);  
          if((c[0]===0) && (c[1]===0)){  
            _blok.set([0,1]);  
          }else{  
            // 移動できない場合は固定ブロックに変換  
            var p=_blok.get();  
            _map[p[0][0]][p[0][1]]=_blok.colors()[0];  
            _map[p[1][0]][p[1][1]]=_blok.colors()[1];  
            if(!this.block_init()){  
              _status=2;  
            }  
          }  
          var _delcnt=0;  
          do{  
            var downs= this.map2down();// 全体降下処理  
            var dels = this.map2dels();// 削除対象の検索  
            dels.forEach(v=>{  
              _map[v[0]][v[1]]=0;  
            });  
            if(dels.length>0){  
              _delcnt++;  
              _his.push({step:_step_cnt,cnt:_delcnt,delblock:dels.length});  
            }  
          }while((dels.length>0)||(downs!=0))// 落下移動がなく削除もない場合は終了  
        }  
      }  
    }  
  

ソース全体

<html>  
  <head>  
    <title>Hello Riot.</title>  
    <meta charset="UTF-8"/>  
  </head>  
  <body>  
    <block-down></block-down>  
    <script type="riot/tag" src="block-down.tag"></script>  
   <script src="riot/riot+compiler.min.js"></script>  
    <script>riot.mount('block-down')</script>  
  
  
  </body>  
</html>  
  
<block-down>  
    <h1>ブロックおとすやつ</h1>  
    <h3>ESCキーで開始してください</h3>  
    <svg riot-width={ w*15 } riot-height={ h*15 } >  
      <g each={d,i in view} transform={('translate('+(i*15)+',0)')}>  
        <rect each={d2,i2 in d} x="0" riot-y={i2*15} data-point='{(JSON.stringify({x:i,y:i2}))}' width="15" height="15" title=""  fill="#{('000000'+d2.toString(16)).slice(-6)}"></rect>  
      </g>  
    </svg>  
    <div style="float:right;">  
      <h3 each={his}>{step}step目:{cnt}連鎖成功:{delblock}個</h3>  
    </div>  
  <script>  
    var self   = this;  
    self.tme   = false;  
    self.tme2  = false;  
    self.view  = [];  
    self.his   = [];  
  
    var colors = [0xff0000,0xffff00,0x0000ff];  
    var poses  = [[[0,0],[-1,0]],[[0,0],[0,-1]],[[-1,0],[0,0]],[[0,-1],[0,0]]];  
    var Blok=(point,pose,cols)=>{  
      var _point  = point;  
      var _pose   = (pose!=undefined)?pose:Math.floor(Math.random()*poses.length);  
      var _colors = (cols!=undefined)?cols:[colors[Math.floor(Math.random()*colors.length)],colors[Math.floor(Math.random()*colors.length)]];  
      return {  
        point   :()=>{return _point; },  
        pose    :()=>{return poses[_pose];  },  
        poseNext:()=>{  
          _pose = (++_pose<poses.length)?_pose:0;  
          },  
        colors  :()=>{return _colors;},  
        get     :function(){return this._get([0,0]);},  
        _get    :(p)=>{  
          return  [  
            [poses[_pose][0][0]+_point[0]+p[0],poses[_pose][0][1]+_point[1]+p[1]],  
            [poses[_pose][1][0]+_point[0]+p[0],poses[_pose][1][1]+_point[1]+p[1]]  
          ];  
        },  
        set     :(p)=>{_point = [_point[0]+p[0],_point[1]+p[1]];}  
      };  
    };  
    var Game=(view)=>{  
      var _top_buff = 2;  
      var _w        = view.length;  
      var _h        = view[0].length+_top_buff;  
      var _map      = false;  
      var _blok     = false;  
      var _view     = view;  
      var _status   = 0;  
      var _his      = [];  
      var _step_cnt = 0;  
      return {  
        get_his:function(){  
          return _his;  
        },  
        drawn:function(){  
          _map.forEach((w,wi)=>{  
              w.forEach((h,hi)=>{  
                if(hi<_top_buff){  
                  return;  
                }else{  
                  _view[wi][hi-_top_buff]=h;  
                }  
              });  
          });  
          /*blockの表示 */  
          var bp  = _blok._get([0,-_top_buff]);  
          var bpc = this.map2block_chk(bp);  
          if(bpc[0]===0)_view[bp[0][0]][bp[0][1]]=_blok.colors()[0];  
          if(bpc[1]===0)_view[bp[1][0]][bp[1][1]]=_blok.colors()[1];  
        },  
        block_init:function(){  
          var p=[Math.floor(_w/2),1];  
          _blok=Blok(p);  
          return (this.map2block_chk([p])[0]===0);  
        },  
        map_init:()=>{  
          _map=new Array(_w);  
          for(i=0;i<_w;i++){  
            _map[i] = new Array(_h);  
            _map[i].fill(0);  
          }  
        },  
        map2block_chk:function(points,f){  
          var ret=[];  
          points.forEach(v=>{  
            var f1 = _w >  v[0];  
            var f2 = 0  <= v[0];  
            var f3 = _h >  v[1];  
            var f4 = 0  <= v[1];  
            ret.push(f1&&f2&&f3&&f4&&(_map[v[0]][v[1]]));  
          });  
          return ret;  
        },  
        // 削除対象ブロック検索処理  
        map2dels:function (){  
          var _search=(p,chain,c,zumi)=>{  
            var cc = zumi.filter((v)=>{return (v[0]===p[0])&&(v[1]===p[1]);})  
            zumi.push(p);  
            if((cc.length==0)&& this.map2block_chk([p])[0]&&(_map[p[0]][p[1]]==c)){  
              chain.push(p);  
              _search([p[0]+1,p[1]  ],chain,c,zumi);  
              _search([p[0]-1,p[1]  ],chain,c,zumi);  
              _search([p[0]  ,p[1]+1],chain,c,zumi);  
              _search([p[0]  ,p[1]-1],chain,c,zumi);  
            }  
          };  
          // 削除処理  
          var dels=[];  
          _map.forEach((w,wi)=>{  
            w.forEach((h,hi)=>{  
              var _chain = [];  
              var _zumi  = dels.slice(0, dels.length);;  
              (h)&&_search([wi,hi],_chain,h,_zumi);  
              if(_chain.length>=4){  
                dels = _chain.concat(dels);  
              }  
            });  
          });  
          return dels;    
        },  
        // 降下処理  
        map2down:function(){  
          var cnt=0;  
          _map.forEach((w,wi)=>{  
            w.forEach((h,hi)=>{  
              var f={};  
              var np    = [wi,hi+1];  
              var npchk = this.map2block_chk([np],f)[0];  
              var f1 = h;  
              var f2 = (npchk===0);  
              if(f1 && f2){  
                cnt++;  
                _map[np[0]][np[1]]=_map[wi][hi];  
                _map[wi][hi]=0;  
              }  
            });  
          });  
          return cnt;  
        },  
        left:function(){  
          var c=this.map2block_chk(_blok._get([-1,0]));  
          if((c[0]===0) && (c[1]===0)){_blok.set([-1,0])}  
        },  
        right:function(){  
          var c=this.map2block_chk(_blok._get([1,0]));  
          if((c[0]===0) && (c[1]===0)){_blok.set([1,0])}  
        },  
        down:function(){  
            var p=_blok.get();  
            _map[p[0][0]][p[0][1]]=_blok.colors()[0];  
            _map[p[1][0]][p[1][1]]=_blok.colors()[1];  
            this.block_init();  
        },  
        space:()=>{  
          _blok.poseNext();  
        },  
        init:function(){  
          this.map_init();  
          this.block_init();  
        },  
        statusChange:function(){  
          if(++_status>=3){  
            _status=0;  
            this.init();  
          }  
        },  
        step:function(){  
          _step_cnt++;  
          if(_status!=1) return;  
          // 降下処理  
          var f = {};  
          var c = this.map2block_chk(_blok._get([0,1]),f);  
          if((c[0]===0) && (c[1]===0)){  
            _blok.set([0,1]);  
          }else{  
            // 移動できない場合は固定ブロックに変換  
            var p=_blok.get();  
            _map[p[0][0]][p[0][1]]=_blok.colors()[0];  
            _map[p[1][0]][p[1][1]]=_blok.colors()[1];  
            if(!this.block_init()){  
              _status=2;  
            }  
          }  
          var _delcnt=0;  
          do{  
            var downs= this.map2down();// 全体降下処理  
            var dels = this.map2dels();// 削除対象の検索  
            dels.forEach(v=>{  
              _map[v[0]][v[1]]=0;  
            });  
            if(dels.length>0){  
              _delcnt++;  
              _his.push({step:_step_cnt,cnt:_delcnt,delblock:dels.length});  
            }  
          }while((dels.length>0)||(downs!=0))// 落下移動がなく削除もない場合は終了  
        }  
      }  
    }  
  
    /* タイマーセット関数(一定ごとに時を進める用)  
    */  
    var tm_set=()=>{  
      if(self.tme)clearInterval(self.tme);  
      self.tme=setInterval(()=>{  
        self.tm-=0.1;  
        tm_set2(self.tm);  
      },1000);  
    };  
    /* タイマーセット関数(メイン処理用)  
    */  
    var tm_set2=(v)=>{  
      if(self.tme2)clearInterval(self.tme2);  
      self.tme2=setInterval(()=>{  
        self.game.step();  
        self.game.drawn();  
        self.his=self.game.get_his();  
        self.update();  
      },v);  
    };  
    /* マウント前イベント  
    */  
    self.on("before-mount",(v)=>{  
      self.h=40;  
      self.w=20;  
      self.view=new Array(self.w);  
      for(i=0;i<self.w;i++){  
        self.view[i] = new Array(self.h);  
        self.view[i].fill(0);  
      }  
      self.game=Game(self.view);  
      self.game.init();  
      self.tm=300;  
      document.onkeydown = function (e){  
        var k = e.keyCode;  
        var g = self.game;  
        if(k==37){g.left();};//左  
        if(k==40){g.down();};//落下  
        if(k==39){g.right();};//右  
        if(k==32){g.space();}//回転  
        if(k==27){g.statusChange();}//game開始  
      };  
      tm_set();  
    });  
  
  
  </script>  
</block-down>  
  

残課題

  • 2度目にいろいろな情報が残ってしまう(雑)
  • 細かすぎてどこに落ちて言っているかがわかりにくい
  • 横移動時の反応が悪い気がする
  • 消えた時に一括で消えるのでどう連鎖しているか不明