phina.js で15パズル(Sliding puzzle)を作ってみた

phi phi on javascript, phina.js, game

@alkn203 さんの書かれた

【phina.js】ゲーム作成チュートリアル(15パズル) | Keep Coding

step 形式で少しずつ理解しながら作ることができる形になっていて, とてもわかりやすく解説してくれています. 私も参考にしながら作ってみたので紹介したいと思います.

Runstant Demo

今回作ったやつです, @alkn203 さんのものを見た目と実装ともに若干アレンジしています.

Code

ざくっと実装しただけなのでコメントをほとんど無いですが良かったら参考にしてください.

/*
 * runstant
 */

phina.globalize();

var SCREEN_WIDTH      = 640;  
var BOARD_PADDING     = 40;  
var BOARD_WIDTH       = SCREEN_WIDTH-BOARD_PADDING*2;  
var PIECE_HORIZON_NUM = 3;  
var PIECE_VERTICAL_NUM= 3;  
var PIECE_NUM         = PIECE_HORIZON_NUM*PIECE_VERTICAL_NUM;  
var PIECE_SIZE        = (BOARD_WIDTH/PIECE_HORIZON_NUM)*0.9;  
var PIECE_COLOR       = 'hsl({0}, 80%, 60%)';  
var PIECE_TEXT_GENERATOR = function(n) {  
  return n;
};

phina.define('MainScene', {  
  superClass: 'CanvasScene',

  init: function() {
    this.superInit();

    this.pieceTable = [];
    this.pieceGroup = CanvasElement().addChildTo(this);

    var grid = Grid(BOARD_WIDTH, PIECE_HORIZON_NUM);
    grid.offset = grid.span(1)/2 + BOARD_PADDING;

    (PIECE_NUM).times(function(i) {
      var xIndex = i%PIECE_HORIZON_NUM;
      var yIndex = (i/PIECE_HORIZON_NUM).floor();
      var p = Piece(i).addChildTo(this.pieceGroup);

      p.x = grid.span(xIndex);
      p.y = grid.span(yIndex)+220;
      p.label.text = PIECE_TEXT_GENERATOR(i);

      p.setInteractive(true);
      p.onpointend = function() {
        this.movePiece(p);
      }.bind(this);

      this.pieceTable.push(p);
    }, this);

    var cursorPiece = this.pieceGroup.children.last;
    cursorPiece.hide();
    cursorPiece.color = null;
    this.cursorPiece = cursorPiece;

    this.time = 0.0;
    this.fromJSON({
      children: {
        timerLabel: {
          className: 'Label',
          x: this.gridX.span(15),
          y: this.gridY.span(2.5),
          align: 'right',
          fontSize: 100,
          text: '15.01',
        },
      },
    });

    this.shufflePiece();
  },

  update: function(app) {
    this.time += app.deltaTime;
    this.timerLabel.text = (this.time/1000).toFixed(2);
  },

  getPieceByPosIndex: function(xIndex, yIndex) {
    if (xIndex < 0 || (PIECE_HORIZON_NUM) <= xIndex) return null;
    if (yIndex < 0 || (PIECE_VERTICAL_NUM) <= yIndex) return null;

    var index = xIndex + yIndex*PIECE_HORIZON_NUM;
    return this.pieceTable[index];
  },

  piece2Index: function(p) {
    return this.pieceTable.indexOf(p);
  },

  piece2PosIndex: function(p) {
    var index = this.piece2Index(p);
    return {
      xIndex: index%PIECE_HORIZON_NUM,
      yIndex: (index/PIECE_HORIZON_NUM).floor(),
    };
  },

  movePiece: function(p) {
    var posIndexA = this.piece2PosIndex(p);
    var posIndexB = this.piece2PosIndex(this.cursorPiece);
    var dx = Math.abs(posIndexA.xIndex - posIndexB.xIndex);
    var dy = Math.abs(posIndexA.yIndex - posIndexB.yIndex);

    if ((dx === 0 && dy === 1) || (dx === 1 && dy === 0)) {
      var flow = this.swapPiece(p, this.cursorPiece, 100);
      flow.then(function() {
        if (this.isClear()) {
          this.gameClear();
        }
      }.bind(this));
    }
  },

  swapPiece: function(a, b, time) {
    var indexA = this.piece2Index(a);
    var indexB = this.piece2Index(b);
    this.pieceTable[indexA] = b;
    this.pieceTable[indexB] = a;

    if (time) {
      return Flow(function(resolve) {
        a.tweener.clear()
          .to({
            x: b.x,
            y: b.y,
          }, time, 'easeOutCubic')
          ;
        b.tweener.clear()
          .to({
            x: a.x,
            y: a.y,
          }, time, 'easeOutCubic')
          .call(function() {
            resolve();
          })
          ;
      });
    }
    else {
      var temp = a.x; a.x = b.x; b.x = temp;
      var temp = a.y; a.y = b.y; b.y = temp;

      return Flow.resolve();
    }
  },

  shufflePiece: function() {
    var c = this.cursorPiece;

    (128).times(function() {
      var cursorPosIndex = this.piece2PosIndex(c);
      var xIndex = cursorPosIndex.xIndex;
      var yIndex = cursorPosIndex.yIndex;
      var left  = this.getPieceByPosIndex(xIndex-1, yIndex);
      var right = this.getPieceByPosIndex(xIndex+1, yIndex);
      var up    = this.getPieceByPosIndex(xIndex, yIndex-1);
      var down  = this.getPieceByPosIndex(xIndex, yIndex+1);
      var target= [left,right,up,down].filter(function(p) {
        return p != null;
      }).pickup();

      this.swapPiece(c, target);
    }, this);
  },

  isClear: function() {
    return this.pieceTable.every(function(p, i) {
      return p.number === i;
    });
  },

  gameClear: function() {
    this.exit({
      score: this.time/1000 + ' s',
      url: 'http://phiary.me/phina-js-sliding-puzzle-game/',
    });
  },
});

phina.define('Piece', {  
  superClass: 'RectangleShape',

  init: function(n) {
    var color = PIECE_COLOR.format(360/15*n);
    this.superInit({
      stroke: false,
      width: PIECE_SIZE,
      height: PIECE_SIZE,
      cornerRadius: 10,
      fill: color,
    });

    this.number = n;

    this.label = Label({
      text: n,
      fontSize: PIECE_SIZE*0.5,
      fill: 'white',
    }).addChildTo(this);
  },
});

phina.main(function() {  
  var app = GameApp({
    title: 'Sliding puzzle',
    backgroundColor: 'white',
    fontColor: '#444',
    startLabel: 'title',
  });

  app.run();
});

Arrange

今回のサンプルはちゃんとロジックとデータを切り分けて作ってあるので, 上部分で定義している定数を変更するだけで簡単にアレンジできます.

みなさんも, 色々といじって遊んでみてください♪

4x4 にしてみる

ss

var PIECE_HORIZON_NUM = 4;  
var PIECE_VERTICAL_NUM= 4;  
↓
var PIECE_HORIZON_NUM = 3;  
var PIECE_VERTICAL_NUM= 3;  

runstant

色を一色にしてみる

ss

var PIECE_COLOR       = 'hsl({0}, 80%, 60%)';  
↓
var PIECE_COLOR       = 'red';  

runstant

表示をアルファベットにしてみる

ss

var PIECE_TEXT_GENERATOR = function(n) {  
  return n;
};
↓
var PIECE_TEXT_GENERATOR = function(n) {  
  return String.fromCharCode(65 + n);
};

runstant

Finally

@alkn203 さんの解説を参考にしながら作ってはみたのものの私はコーディングに対してこだわりが強い方なので結局オレオレ実装になってしまいました.

とはいえ, コアな機能実装部分は大差なく, 細かい部分で『なるほど, 確かにこういった実装方法もあるなぁ』と色々勉強になりました.

目的は同じでも, 作る人の数だけ実装方法があります. 同じゲームを, 複数のプログラマで作ってそれぞれのアプローチについてレビューしあうといったことも今後やってみたいなと思いました.

Reference