#ぷよぷよプログラミング(改造)

ぷよぷよプログラミング の改造版。 左右の回転、ネクスト、おじゃま、影の表示、他、のサンプルコード。 2020.07.10

 

回転操作用に、rollLeft と rollRight を用意する。また、押し戻し判定が 左回転 専用なので、右回転の判定も作る。

playing() の if (this.keyStatus.up) {} を
if (this.keyStatus.rollLeft) {} ~ if (this.keyStatus.rollRight) {} とに分岐。
const angle = this.keyStatus.rollLeft ? 90 : -90 ; // 回転方向をangleとして、角度をもたせる。

// let distRotation = (this.puyoStatus.rotation + 90) % 360;
→
let distRotation = (this.puyoStatus.rotation + angle) % 360; // angleは、回転後の角度の計算に使う
if (this.keyStatus.rollLeft) { // この左回転を複製する
  if (rotation === 0) {
→
if (this.keyStatus.rollRight) {
  if (rotation === 180) { // rotationは回転前の座標なので注意

左右の移動と回転操作、それぞれで ぷよの座標を計算しているので、これを1つの計算式につなぎあわせる。

this.puyoStatus.left =
  ratioMove * (this.moveDestination - this.moveSource) + this.moveSource // 移動によるx軸を含む位置を求める
  + (this.rotateAfterLeft - this.rotateBeforeLeft) * ratioRotate + this.rotateBeforeLeft; // 回転による相対的な位置の変化を求める
 

移動と回転 を単純に足し合わせてもうまくいかない。どちらの計算にも x軸をベースとした結果が返るため、 回転操作からは x軸の計算を除外するとよい。

移動によるx軸の結果
this.moveSource = x * Config.puyoImgWidth;
this.moveDestination = (x + cx) * Config.puyoImgWidth;

回転によるx軸の結果
// this.rotateBeforeLeft = x * Config.puyoImgHeight;
// this.rotateAfterLeft = (x + cx) * Config.puyoImgHeight;
→
this.rotateBeforeLeft = 0;
this.rotateAfterLeft = cx * Config.puyoImgHeight;
 

また、キー入力がなくても上の計算に関する”変化がない”という計算を行っておく必要がある。

  if (this.keyStatus.right || this.keyStatus.left) {
    ..
  } else {
    // 左右の移動がなくても計算する
    const x = this.puyoStatus.x;
    this.moveSource = x * Config.puyoImgWidth;
    this.moveDestination = x * Config.puyoImgWidth;
  }

  if (this.keyStatus.rollLeft || this.keyStatus.rollRight) {
    ..
  } else {
    // 回転しなくても計算する
    this.rotateBeforeLeft = 0;
    this.rotateAfterLeft = 0;
    this.rotateFromRotation = this.puyoStatus.rotation;
  }
 

※移動しながら回転させると y軸が 許容を超えて大きく変化する場合がある。(たとえば、右から下への回転でy+1、下入力でy+1 あわせて飛び出しが発生する。床だけではなく天井に対しても同様。) 。これを制御しておく。

  if(this.puyoStatus.y >= Config.stageRows - 2) { this.puyoStatus.y = Config.stageRows - 2 }; // > 行き過ぎたら戻す
  if(this.puyoStatus.y < -1) { this.puyoStatus.y = -1 };
 

座標計算だけでは、キー入力の受付が単発なので同時操作できない。そこで、複数のキーを同時に認識できるようにする。 配列のkeyCodesに、入力したキーをキャッシュしていく。PCで見ている場合は、試しにキーボードで何か複数同時入力してみて。操作結果→ *

    function handleMultiple(event) {
      var keyCode = event.keyCode,
        keyCodes = this.keyCodes,
        i = keyCodes.indexOf(keyCode);

      switch (event.type) {
        case "keydown":
          if (i === -1) {
            keyCodes.push(keyCode);
          }
          if (keyCodes.indexOf(37) !== -1) { // 左向きキー
            Player.keyStatus.left = true;
          }
          if (keyCodes.indexOf(38) !== -1) { // 上向きキー
            Player.keyStatus.rollLeft = true;
          }
          if (keyCodes.indexOf(39) !== -1) { // 右向きキー
            Player.keyStatus.right = true;
          }
          if (keyCodes.indexOf(40) !== -1) { // 下向きキー
            Player.keyStatus.down = true;
          }

          break;
        case "keyup":
          keyCodes.splice(i, 1);

          if (keyCodes.indexOf(37) === -1) {
            Player.keyStatus.left = false;
          }
          if (keyCodes.indexOf(38) === -1) {
            Player.keyStatus.rollLeft = false;
          }
          if (keyCodes.indexOf(39) === -1) {
            Player.keyStatus.right = false;
          }
          if (keyCodes.indexOf(40) === -1) {
            Player.keyStatus.down = false;
          }

          break;
      }
    }
    var listener = { keyCodes: [], handleEvent: handleMultiple };
    document.addEventListener("keydown", listener, false);
    document.addEventListener("keyup", listener, false);

スマホを下へ大きくスワイプすると、画面更新が発生して、操作の邪魔になる。これを停止する。

    function handleVoid(event) {
      // preventDefaultが無視されるのでaddEventListenerの第三引数をpassive:falseにする
      event.preventDefault();
    }
    // スクロールによる画面更新を禁止
    document.addEventListener('touchmove', handleVoid, { passive: false });

createNewPuyo()の中で、新規ぷよを作っているので、ネクストの一覧から取り出す仕組みに変更する。 仮に、128個分のネクストを用意する。getNextPuyo(0)で1手目、getNextPuyo(1)で2手目が得られる。

  static createNextTable() {
    const puyoColors = Math.max(1, Math.min(5, Config.puyoColors));
    // let nextTable = Array(127);
    let tmpCenterPuyo;
    let tmpMovablePuyo;
    // テーブルサイズ
    for (let i = 0; i < Config.nextTableSize; i++) { // Config.nextTableSize = 128
      // 新しいぷよの色を決める
      tmpCenterPuyo = Math.floor(Math.random() * puyoColors) + 1;
      tmpMovablePuyo = Math.floor(Math.random() * puyoColors) + 1;
      this.nextTable.push({'centerPuyo':tmpCenterPuyo, 'movablePuyo':tmpMovablePuyo});
    }
  }
  // index: 0-127
  static getNextPuyo(index) {
    index = index % this.nextTable.length;
    return this.nextTable[index];
  }
 

ネクストを準備できたら、画面に表示する。ステージの作成と同じように配置する。 本来HTMLで生成するコードをjsからコントロールしている。

  <div id="next"></div>を、HTML側へ。
  for (let i = 0; i < Config.nextVolume; i++) { // Config.nextVolume は ネクストの表示数 = 2
    let nextPuyo = this.getNextPuyo(index + (i + 1));
    Stage.setNextPuyo(i, 1, nextPuyo['centerPuyo']);
    Stage.setNextPuyo(i, 0, nextPuyo['movablePuyo']);
  }
  const nextElement = document.getElementById("next");
  this.nextElement = nextElement;
  static setNextPuyo(x, y, puyo) {
    // 画像を作成し配置する
    const puyoImage = PuyoImage.getPuyo(puyo);
    puyoImage.style.left = (x * Config.puyoImgWidth + (x * Config.puyoImgWidth / 2)) + "px"; // xでネクネクの位置をズラす
    puyoImage.style.top = (y * Config.puyoImgHeight + (x * Config.puyoImgHeight / 4)) + "px"; // xでネクネクの位置をズラす
    this.nextElement.appendChild(puyoImage);
  }

URLパラメータを使って、牌譜を指定できるようにする。1手ごとにURLパラメータを変更するには、window.history.pushState() または window.history.replaceState() を使う。

// Stage.boardの中身はそのままだと長い
?q=[[null,null,null,null,null,null],[null,null,null,null,null,null],[null,null,null,null,null,null],[null,null,null,null,null,null],[null,null,null,null,null,null],[null,null,null,null,null,null],[null,null,null,null,null,null],[null,null,null,null,null,null],[null,null,null,null,null,null],[null,null,null,null,null,null],[null,null,{%22puyo%22:2,%22element%22:{}},null,null,null],[null,null,{%22puyo%22:3,%22element%22:{}},null,null,null]]
→
// 短くまとめる
?q=[[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]]
  // 牌譜を人がわかる程度に短くする
  static encodeBoard(board) {
    let ret = [];
    if(!Array.isArray(board)) return null;
    for(var y in board) {
      let line = [];
      for(var x in board[y]) {
        line.push(board[y][x] && board[y][x].puyo || 0); // 0~4
      }
      ret.push(line);
    }
    return ret;
  }
 

牌譜をサーバーへ送って、保存や復元ができる。サーバー側では、GETまたはPOSTデータを受け取り、データベースへ保存する。*リクエストを受け取るサーバー側のプログラムと状態を保持するデータベースが必要

    var xhr = new XMLHttpRequest();
    xhr.open('GET', server+'/add?q='+query, true);
    xhr.send(null);

CSSアニメーションを使う。以下のような ぷるぷるアニメーションを用意し、タイミングをあわせて class を与える。

.jiggly {
	animation: jiggly ease 0.7s 0s 1;
}
@keyframes jiggly {
	0%   { transform: scale(1.0, 1.0) translate(0%, 0%); }
	15%  { transform: scale(0.8, 0.8) translate(0%, 5%); }
	30%  { transform: scale(1.4, 0.8) translate(0%, 10%); }
	50%  { transform: scale(0.8, 1.4) translate(0%, -10%); }
	70%  { transform: scale(1.2, 0.8) translate(0%, 5%); }
	100% { transform: scale(1.0, 1.0) translate(0%, 0%); }
}
  puyoImage.classList.add('jiggly');
 

ブロックの存在チェックは1つのメソッドにまとめるとよい。コードの可読性が改善される。

  // 指定した座標にブロックがあるかどうか
  static isBlocked(x, y) {
    if (
      y < 0 ||
      x < 0 ||
      y >= Config.stageRows ||
      x >= Config.stageCols ||
      Stage.board[y][x]
    ) return true;
    return false;
  }
 

以下のような判定が10か所以上あるが、すべて1行に置き換えることができる

  if (
        my < 0 ||
        mx + cx < 0 ||
        mx + cx >= Config.stageCols ||
        Stage.board[my][mx + cx]
      ) { ..  
  →
  if (this.isBlocked(mx + cx, my)) { .. // この1行で済む
 

また、回転方向は4方向に限定しておく。マイナスは扱わず、0°, 90°, 180°, 270°の4つにする。

    var distRotation = (this.puyoStatus.rotation + angle) % 360;
    if (distRotation < 0) distRotation += 360; // > マイナス時は一回転
  if (rotation === 0) { // 反時計回りで0°→90°の分岐 であることがわかりにくい(0°→270°の処理かもしれないし、0°に対する処理かもしれない。この1行では判断できない)
    → if (distRotation === 90) { // 90°の方向に対する処理であることが明確

 // 他の角度も同様
  if (rotation === 90) { → if (distRotation === 180) {
  if (rotation === 180) { → if (distRotation === 270) {
  if (rotation === 270) { → if (distRotation === 0) {
 

※ここまで対応すると、270°以外の処理は 左回転と右回転の処理は共通化できる。さらに、残った270°の処理も、キー入力(this.keyStatus)の値に置き換えると同一のコードとなり、複製した右回転の処理は一切不要になる。

    const _rx = this.keyStatus.rollRight ? 1 : -1;
    if (this.isBlocked(x + _rx, y + 2)) { // 左回転のときは x - 1, 右回転のときは x + 1 なので、(this.keyStatus.rollRight ? 1 : -1)に置き換えることができる。
      if (y + 2 >= 0) {
        // ブロックがある。上に引き上げる
        cy = -1;
      }
    }

このプログラムはES6サポートのブラウザに限り動作する。IE11では動作しない。 (対応状況)
なお、クラスの意義を考えさせられるが、クラスメソッドがすべてstaticなので どこからでもどの値でも書き換えや参照ができてしまう。

デモ
 

Puyo Puyo Champions. By Joro.