STEP06.ゲーム結果を表示する

前回はゲームの開始と終了の処理を作りました。 今回は終了処理を行うときに、ゲームの結果を表示するようにしましょう。

今回の記事で作成する最終的なコードは次のリンクから確認してください。

結果表示用の関数を作る

最初に結果表示用の関数の枠組みだけを作成しておきます。

呼び出す場所は、修了処理の中からです。 プレイヤーの状態を初期化すると結果を計算できないので、タイトル表示と初期化の間に追加します。

scripts/main.ts(部分)
function exitGame() {
  isPlaying = false;
  overworld.runCommand(`title @a title Game Over!`);
  // 結果を表示する(記述する場所に注意)
  showResult();
  for (const player of overworld.getPlayers()) {
    initializePlayer(player);
  }
}

function showResult() {
  // ここに結果表示の処理を書いていく
}

全員の得点を表示する

最終結果の表示方法は色々と工夫できそうですが、まずは簡単に全プレイヤーの結果だけを表示してみましょう。 各プレイヤーのアクションバーに表示していた内容を、名前を加えて/sayコマンドでチャット画面に表示します。

scripts/main.ts(部分)
function showResult() {
  overworld.runCommand(`say --- 結果表示 ---`);
  // プレイヤーごとに繰り返し
  for (const player of overworld.getPlayers()) {
    const height = getHeightProperty(player);
    const score = height.max - height.min;
    const text = `${score}pt (${height.min} ~ ${height.max})`;
    // /sayでチャット画面に表示
    overworld.runCommand(`say ${player.nameTag}: ${text}`);
  }
  overworld.runCommand(`say --------------`);
}

順位をつけて表示したい

得点を表示をするだけなら、上の処理だけでも構いません。 しかしゲームである以上、勝者や順位を適切に表示してあげるべきでしょう。

ここからの処理は少し難しくなります。 わからなくなったら、最終結果をコピペして次に進んでください。

最初に全員の得点を計算する

今回は単純に得点順にプレイヤーを並べ替えて、その並び順を、そのまま順位とします。

順位は厳密に判定しようとすると、同じ点数などを考えないといけません。 この処理はずっと複雑な処理になってしまうので、今回は簡単な処理で済ませます。

先に全体の処理を載せておきます。

scripts/main.ts(部分)
function getScore(max: number, min: number) {
  return max - min;
}

function getScoreText(max: number, min: number) {
  const score = getScore(max, min);
  return `${score}pt (${min} ~ ${max})`;
}

function getResult(playerIterator: mc.PlayerIterator) {
  // イテレーターをスプレッド構文を使って配列化する(次のように.map()と.sort()を使いたい)
  const players = [...playerIterator];
  // 結果をJSON構造で返す
  const result = players.map((player) => {
    const name = player.nameTag;
    const height = getHeightProperty(player);
    const score = getScore(height.max, height.min);
    const text = getScoreText(height.max, height.min);
    return { name, score, text };
  });

  // テスト用に結果情報を追加
  result.push({
    name: 'Steve',
    score: getScore(60, 57),
    text: getScoreText(60, 57),
  });
  result.push({
    name: 'Alex',
    score: getScore(80, 62),
    text: getScoreText(80, 62),
  });

  return result
    .sort((a, b) => {
      // スコアで並び替えて順位を決める
      return b.score - a.score;
    })
    .map((obj, index) => {
      // スコアが高い順に並び変えているので、indexを+1した値がそのまま順位になる(indexは0から)
      const ranking = index + 1;
      return { ranking, ...obj };
    });
}

function showResult() {
  const players = overworld.getPlayers();
  const result = getResult(players);
  try {
    overworld.runCommand(`say --- 結果表示 ---`);
    result.forEach((obj) => {
      overworld.runCommand(`say ${obj.ranking}. ${obj.name} ${obj.text}`);
    });
    overworld.runCommand(`say --------------`);
  } catch (error) {
    overworld.runCommand(`say ${error}`);
  }
}

最終的に、showResult()は次のように、大きく3つに分かれます。

  1. 計算対象のプレイヤーを決定する
  2. そのプレイヤーの結果を取得する
  3. 取得した結果を順番に表示する
scripts/main.ts(部分)
function showResult() {
  // 1. 計算対象のプレイヤーを決定する
  const players = overworld.getPlayers();
  // 2. そのプレイヤーの結果を取得する
  const result = getResult(players);
  // 3. 取得した結果を順番に表示する
  try {
    overworld.runCommand(`say --- 結果表示 ---`);
    result.forEach((obj) => {
      overworld.runCommand(`say ${obj.ranking}. ${obj.name} ${obj.text}`);
    });
    overworld.runCommand(`say --------------`);
  } catch (error) {
    overworld.runCommand(`say ${error}`);
  }
}

計算対象のプレイヤーを決定する

今回の計算対象はプレイヤー全員です。

今後開発が進むと、ゲームに参加している人だけに絞り込んだりする必要があります。 そのため、計算対象は次の結果計算関数から分離しておきます。

const players = overworld.getPlayers();

そのプレイヤーの結果を取得する

結果計算はgetResult()として専用の関数にまとめます。

scripts/main.ts(部分)
function getResult(playerIterator: mc.PlayerIterator) {
  // イテレーターをスプレッド構文を使って配列化する(次のように.map()と.sort()を使いたい)
  const players = [...playerIterator];
  // 結果をJSON構造で返す
  const result = players.map((player) => {
    const name = player.nameTag;
    const height = getHeightProperty(player);
    const score = getScore(height.max, height.min);
    const text = getScoreText(height.max, height.min);
    return { name, score, text };
  });

  return result
    .sort((a, b) => {
      // スコアで並び替えて順位を決める
      return b.score - a.score;
    })
    .map((obj, index) => {
      // スコアが高い順に並び変えているので、indexを+1した値がそのまま順位になる(indexは0から)
      const ranking = index + 1;
      return { ranking, ...obj };
    });
}

overworld.getPlayers()で取得されるデータはイテレーター という少し特殊な形式になっているので注意してください。

イテレーターは次のようにスプレッド構文 を使って、簡単に配列に変換できるので、今の段階では正確に理解する必要はありません。

const players = [...playerIterator];

配列に変換したらmap() を使って、順番に処理します。

map()は配列の内容ごとに繰り返し、それぞれの戻り値で新しい配列を作ります。 新しい配列には、後の結果表示処理するときの内容を格納しておきます。

const result = players.map((player) => {
  const name = player.nameTag;
  const height = getHeightProperty(player);
  const score = getScore(height.max, height.min);
  const text = getScoreText(height.max, height.min);
  return { name, score, text };
});

処理内容は、次のコードとまったく同じですが、この場合はmap()を使った方が少し綺麗に書けます。

// map()をfor文で書き直した場合
const result = [];
for (const player of players) {
  const name = player.nameTag;
  const height = getHeightProperty(player);
  const score = getScore(height.max, height.min);
  const text = getScoreText(height.max, height.min);
  result.push({ name, score, text });
};

map()は頻繁に出現する処理方法なので、少しずつ慣れていってください。

細かい違いを言うとmap()は「式」で、for文は「文」という違いがあります。 そのため、すべての場合で、相互に置き換えられるわけではありません。

また、最後の戻り値のオブジェクトは省略表記になっています。

// 本来のオブジェクトの記入の仕方
{
  name: name,
  score: score,
  text: text,
}

// キーと変数が同じ場合には、記述を省略できる
{
  name,
  score,
  text,
}

// 1行に直した場合
{ name, score, text }

テスト用のデータを追加する

開発環境では基本的に自分1人だけなので、順位として並べ替える対象が存在しません。 これではテストができないので、結果配列にデータをpush() で追加しておきます。

// テスト用に結果情報を追加
result.push({
  name: 'Steve',
  score: getScore(60, 57),
  text: getScoreText(60, 57),
});
result.push({
  name: 'Alex',
  score: getScore(80, 62),
  text: getScoreText(80, 62),
});

データを並べ替える

配列の並べ替えは、次にsort() を使います。

今回の配列内容はオブジェクトなので、どのように並べ替えるのかを明示するアロー関数を書きます。 この部分は少し難しいので、とりあえず次のように書いておいてください。 これで得点が高い方から順に並びます。(降順)

result.sort((a, b) => {
  // スコアで並び替えて順位を決める
  return b.score - a.score;
})

続けて、再びmap()を使い、並び替えた後の配列内オブジェクトに順位を追加します。

result.map((obj, index) => {
  // スコアが高い順に並び変えているので、indexを+1した値がそのまま順位になる(indexは0から)
  const ranking = index + 1;
  return { ranking, ...obj };
});

上の二つのメソッドを連続で実行し、returnで返すまでの処理を一気に書くと、次のようになります。

return result
  .sort((a, b) => {
    return b.score - a.score;
  })
  .map((obj, index) => {
    const ranking = index + 1;
    return { ranking, ...obj };
  });

ここは改行で少し伝わりづらいですが、result.sort().map()という形で、メソッドが繋がっています。 このようにメソッドを繋げる方法をメソッドチェーン (method chaining)と呼びます。 このメソッドチェーンを使う場合はmap()部分をfor文におきかえられません。

結果を表示する

showResult()に戻って、結果を表示する処理を行います。

表示する内容は、結果計算関数内で終えているので、ここでは単純な繰り返しから、その内容を呼び出すだけです。 予期しない値でエラーになった場合に備えて、try...catch文で囲んでおきます。

try {
  overworld.runCommand(`say --- 結果表示 ---`);
  result.forEach((obj) => {
    overworld.runCommand(`say ${obj.ranking}. ${obj.name} ${obj.text}`);
  });
  overworld.runCommand(`say --------------`);
} catch (error) {
  overworld.runCommand(`say ${error}`);
}

ここの処理はとても簡潔になっていますが、これを最初から書くことは難しいです。 実際は、どのように前処理を行なっておけば、ここでのコードが綺麗になるかを考えて、何度も修正を繰り返しています。

次のページへ
STEP07.タイマーで終了処理を自動化しよう