Riot.jsで蛇のゲーム

背景

個人的にSVGに興味が出てきた+ゲームを作りたかったので作ってみた。

完成したものは下記
http://hasito.com/snake/

仕様

  • 2Dゲーム
  • 蛇が餌を食べると長くなる
  • 壁/体にあたったら死ぬ
  • 少しづつ動きが早くなる
  • キーボードで操作

    こんな感じ…

開発

枠を組む

SVGで下記のように書くと格子状にブロックを配置できる様子

    <svg width="30" height="60">  
      <g transform={('translate(0,0)')}>  
        <rect x="0" y="0" width="15" height="15" title=""  fill="#000000"></rect>  
        <rect x="0" y="15" width="15" height="15" title=""  fill="#000000"></rect>  
        <rect x="0" y="30" width="15" height="15" title=""  fill="#000000"></rect>  
        <rect x="0" y="45" width="15" height="15" title=""  fill="#000000"></rect>  
      </g>  
      <g transform={('translate(15,0)')}>  
        <rect x="0" y="0" width="15" height="15" title=""  fill="#000000"></rect>  
        <rect x="0" y="15" width="15" height="15" title=""  fill="#000000"></rect>  
        <rect x="0" y="30" width="15" height="15" title=""  fill="#000000"></rect>  
        <rect x="0" y="45" width="15" height="15" title=""  fill="#000000"></rect>  
      </g>  
    </svg>  

これをriot.jsで記載

    <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>  

注意点

  • viewには多次元配列が入っている予定
  • fillは16進指定。6桁必要なので0で頭をうめています。
  • xやらwidthは頭にriot-をつける必要あり

全体の流れ

全体の処理の流れとしては下記な感じを想定して作りました。

  1. 初期化処理
  2. メイン処理
  3. 蛇の死
  4. 初期化処理(2回目)
  5. メイン処理(2回目)
    …こんな感じ

初期化処理

    /* タイマーセット関数(一定ごとに時を進める用)  
    */  
    var tm_set=()=>{  
      if(self.tme)clearInterval(self.tme);  
      self.tme=setInterval(()=>{  
        self.tm-=1;  
        tm_set2(self.tm);  
      },1000);//とりあえず1秒単位で更新固定  
    };  
    /* タイマーセット関数(メイン処理用)  
    */  
    var tm_set2=(v)=>{  
      if(self.tme2)clearInterval(self.tme2);  
      self.tme2=setInterval(()=>{  
        _main_();  
      },v);  
    };  
    /* 初期化処理  
    */  
    var _init_=()=>{  
// 変数の初期化  
      self.st=1;//ゲームステータス  
      self.w=40;//横の長さ  
      self.h=40;//縦の長さ  
      self.view=new Array(self.h);//表示用変数初期化  
      for(i=0;i<self.h;i++){  
        self.view[i] = new Array(self.w);  
        self.view[i].fill(0);  
      }  
      self.map=new Array(self.h);//マップ変数の初期化(今は餌しか置かれない…)  
      for(i=0;i<self.h;i++){  
        self.map[i] = new Array(self.w);  
        self.map[i].fill(0);  
      }  
      self.snake={//蛇の初期化  
        hed:{x:Math.floor(self.w/2),y:Math.floor(self.h/2)},//頭の位置  
        body:[],//体  
        point:0,//ポイント  
        move:{x:1,y:0}//移動属性(キーダウンイベントで変化)  
      }  
      self.tm=300;//時間更新間隔(初期値)  
//キーダウンイベント  
      document.onkeydown = function (e){  
        var k = e.keyCode;  
        var s = self.snake;  
        console.log(k)  
        if(k==37)s.move={x: 0,y:-1};//左  
        if(k==38)s.move={x:-1,y: 0};//上  
        if(k==39)s.move={x: 0,y: 1};//右  
        if(k==40)s.move={x: 1,y: 0};//下  
        if(k==32)self.st=2;//game開始  
      };  
//タイマー設定  
      tm_set();  
      console.log("_init_");  
    }  

移動処理

  
    /* run処理  
    */  
    var _run_=()=>{  
      var s = self.snake;  
      // --move-- 移動処理  
      // body  
      s.body.unshift(JSON.parse(JSON.stringify(s.hed)));  
      s.body.pop();  
      // hed  
      s.hed.x += s.move.x;  
      s.hed.y += s.move.y;  
      // --dead-- 死亡判定  
      var b1 = (s.hed.x<self.w);  
      var b2 = (s.hed.y<self.h);  
      var b3 = (s.hed.x>=0);  
      var b4 = (s.hed.y>=0);  
      var b5 = (s.body.filter((v)=>{return (s.hed.x==v.x)&&(s.hed.y==v.y);}).length==0);  
      if(!(b1&&b2&&b3&&b4&&b5)){  
        return false;  
      }  
      // --eat-- お食事判定  
      if(self.map[s.hed.y][s.hed.x]=="food"){  
        if(s.body.length==0){  
          s.body.push(JSON.parse(JSON.stringify(s.hed)));  
        }else{  
          s.body.push(JSON.parse(JSON.stringify(s.body[s.body.length-1])));  
        }  
        s.point+=1/self.tm;  
      }  
      self.map[s.hed.y][s.hed.x]=0;  
      // --food-- 餌をRANDOMに置く処理  
      var foody=Math.floor(Math.random()*self.h);  
      var foodx=Math.floor(Math.random()*self.w);  
      var fb1=(s.body.filter((v)=>{return (foodx==v.x)==(foody==v.y);}).length==0);  
      var fb2=(!((s.hed.x==foodx)&&(s.hed.y==foody)));  
      if(fb1&&fb1){  
        self.map[foody][foodx]="food";  
      }  
      // --view-- 表示を更新する処理  
      for(i=0;i<self.h;i++){self.view[i].fill(0);}  
      self.view[s.hed.y][s.hed.x]=0x0000ff;  
      s.body.forEach((v)=>{  
        self.view[v.y][v.x]=0x5555ff;  
      });  
      self.map.forEach((y,yi)=>{  
        y.forEach((x,xi)=>{  
          if(x=="food"){  
            self.view[yi][xi]=0xff00ff;  
          }  
        });          
      })  
        
      return true;  
    }  
  
  

メイン処理

ほぼ、切り替えしかしてないけど…

    /* main  
    */  
    var _main_=()=>{  
      if(self.st==2){//st:0>初期 1>停止 2>ゲーム中  
        if(!_run_()){  
          _init_();  
        }  
        self.update();  
      }  
    };  

全体コード

index.html

<html>  
  <head>  
    <title>Hello Riot.</title>  
    <meta charset="UTF-8"/>  
  </head>  
  <body>  
    <sample></sample>  
    <script type="riot/tag" src="sample.tag"></script>  
   <!-- <script src="https://cdn.jsdelivr.net/npm/riot@3.9/riot+compiler.min.js"></script>  -->  
   <script src="riot/riot+compiler.min.js"></script>  
    <script>riot.mount('sample')</script>  
  </body>  
</html>  

sample.tag

<sample>  
    <h1>蛇のやつ</h1>  
    <h3>スペースキーで開始してください</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>  
    <h3>ポイント{snake.point}点</h3>  
    <h3>体の長さ{snake.body.length}ブロック</h3>  
    <h3>頭の位置{snake.hed.x}:{snake.hed.y}</h3>  
  <script>  
    var self=this;  
    self.st=0;  
    self.tme=false;  
    self.tme2=false;  
    /* タイマーセット関数(一定ごとに時を進める用)  
    */  
    var tm_set=()=>{  
      if(self.tme)clearInterval(self.tme);  
      self.tme=setInterval(()=>{  
        self.tm-=1;  
        tm_set2(self.tm);  
      },1000);  
    };  
    /* タイマーセット関数(メイン処理用)  
    */  
    var tm_set2=(v)=>{  
      if(self.tme2)clearInterval(self.tme2);  
      self.tme2=setInterval(()=>{  
        _main_();  
      },v);  
    };  
    /* main  
    */  
    var _main_=()=>{  
      if(self.st==2){  
        if(!_run_()){  
          _init_();  
        }  
        self.update();  
      }  
    };  
    /* run処理  
    */  
    var _run_=()=>{  
      var s = self.snake;  
      // --move--  
      // body  
      s.body.unshift(JSON.parse(JSON.stringify(s.hed)));  
      s.body.pop();  
      // hed  
      s.hed.x += s.move.x;  
      s.hed.y += s.move.y;  
      // --dead--  
      var b1 = (s.hed.x<self.w);  
      var b2 = (s.hed.y<self.h);  
      var b3 = (s.hed.x>=0);  
      var b4 = (s.hed.y>=0);  
      var b5 = (s.body.filter((v)=>{return (s.hed.x==v.x)&&(s.hed.y==v.y);}).length==0);  
      if(!(b1&&b2&&b3&&b4&&b5)){  
        return false;  
      }  
      // --eat--  
      if(self.map[s.hed.y][s.hed.x]=="food"){  
        if(s.body.length==0){  
          s.body.push(JSON.parse(JSON.stringify(s.hed)));  
        }else{  
          s.body.push(JSON.parse(JSON.stringify(s.body[s.body.length-1])));  
        }  
        s.point+=1/self.tm;  
      }  
      self.map[s.hed.y][s.hed.x]=0;  
      // --food--  
      var foody=Math.floor(Math.random()*self.h);  
      var foodx=Math.floor(Math.random()*self.w);  
      var fb1=(s.body.filter((v)=>{return (foodx==v.x)==(foody==v.y);}).length==0);  
      var fb2=(!((s.hed.x==foodx)&&(s.hed.y==foody)));  
      if(fb1&&fb1){  
        self.map[foody][foodx]="food";  
      }  
      // --view--  
      for(i=0;i<self.h;i++){self.view[i].fill(0);}  
      self.view[s.hed.y][s.hed.x]=0x0000ff;  
      s.body.forEach((v)=>{  
        self.view[v.y][v.x]=0x5555ff;  
      });  
      self.map.forEach((y,yi)=>{  
        y.forEach((x,xi)=>{  
          if(x=="food"){  
            self.view[yi][xi]=0xff00ff;  
          }  
        });          
      })  
        
      return true;  
    }  
    /* 初期化処理  
    */  
    var _init_=()=>{  
      self.st=1;  
      self.w=40;  
      self.h=40;  
      self.view=new Array(self.h);  
      for(i=0;i<self.h;i++){  
        self.view[i] = new Array(self.w);  
        self.view[i].fill(0);  
      }  
      self.map=new Array(self.h);  
      for(i=0;i<self.h;i++){  
        self.map[i] = new Array(self.w);  
        self.map[i].fill(0);  
      }  
      self.snake={  
        hed:{x:Math.floor(self.w/2),y:Math.floor(self.h/2)},  
        body:[],  
        point:0,  
        move:{x:1,y:0}  
      }  
      self.tm=300;  
      document.onkeydown = function (e){  
        var k = e.keyCode;  
        var s = self.snake;  
        console.log(k)  
        if(k==37)s.move={x: 0,y:-1};//左  
        if(k==38)s.move={x:-1,y: 0};//上  
        if(k==39)s.move={x: 0,y: 1};//右  
        if(k==40)s.move={x: 1,y: 0};//下  
        if(k==32)self.st=2;//game開始  
      };  
      tm_set();  
      console.log("_init_");  
    }  
    /* マウント前イベント  
    */  
    self.on("before-mount",(v)=>{  
      _init_();  
    });  
  </script>  
</sample>  

残課題

  • 死んだら死んだとか出てほしいけど出ない
  • ポイントが後半に食った餌ほど線形に高い雑仕様
  • 死んだら得たポイント見られない
  • 毎更新に餌がおかれるため、後半めっちゃ出る