開発日記⑧ 難易度別にオーラス条件をランダム生成する

前回までの進捗

前回は、入力した点数や目標順位などをlocalStorageへ保存し、ページを再読み込みしても内容が残るようにした。
これで、一度入力した条件をあとから続けて確認できるようになった。
ただ、毎回自分で4人分の点数を考えて入力するのは少し面倒。
オーラス条件を練習するためのアプリなので、ボタンを押すだけで問題となる条件を作れるようにしたい。
そこで今回は、難易度を選んでオーラス条件をランダム生成する機能を追加した。

生成する内容

ランダム生成するのは、次の項目。

  • 東家・南家・西家・北家の持ち点
  • 自分の席
  • 目標順位
  • 本場
  • 供託リーチ棒
  • 仮の放銃者

画面ではEasy・Normal・Hardのいずれかを選び、「条件をランダム生成」ボタンを押す。

<select
  value={difficulty}
  onChange={(event) =>
    onChangeDifficulty(
      event.target.value as Difficulty,
    )
  }
>
  <option value="easy">Easy</option>
  <option value="normal">Normal</option>
  <option value="hard">Hard</option>
</select>

<button
  type="button"
  onClick={onRandomize}
>
  条件をランダム生成
</button>

ボタンを押すと、選択した難易度に合う条件へ入力内容が置き換わる。

難易度の基準

今回の難易度は、CPUの強さなどを表しているわけではない。
目標順位を達成するために必要な最低和了点を基準にしている。

const difficultyPointRanges = {
  easy: {
    min: 0,
    max: 3900,
  },
  normal: {
    min: 4000,
    max: 8000,
  },
  hard: {
    min: 8100,
    max: 16000,
  },
};

現在の基準は次のとおり。

  • Easy:3,900点以下
  • Normal:4,000点から8,000点
  • Hard:8,100点から16,000点

たとえば、目標達成に必要な最低条件が3,900点ならEasyになる。
満貫が必要ならNormal、跳満以上が必要ならHardという大まかな分け方。
細かな難易度調整は、実際に何度か使ってから見直す予定。

まず点数をランダムに分ける

4人分の点数を作るときは、合計点が正しくなるようにする必要がある。
基本となる合計は100,000点。
ただし、供託リーチ棒がある場合、その点棒は誰かの持ち点から場へ出ている。
そのため、プレイヤー4人の合計点は次のようになる。

const totalScore =
  baseTotalScore - kyotaku * 1000;

供託が2本なら、4人の合計点は98,000点になる。
今回は各プレイヤーが最低5,000点を持つようにし、残りの点数を100点単位でランダムに分配した。

const pointUnit = 100;
const minimumScore = 5000;

生成後に点数をシャッフルし、東家・南家・西家・北家へ割り当てる。
単純に4人それぞれの点数をランダム生成すると、合計が100,000点にならない。
そのため、先に合計点を決めてから分配する形にした。

自分の席と目標もランダムにする

点数を生成したあと、自分の席を4つの席から選ぶ。

const selfSeat =
  seats[
    getRandomInt(
      0,
      seats.length - 1,
    )
  ];

目標も次の3種類からランダムに選択する。

  • トップ
  • 2着以上
  • ラス回避

本場と供託は、どちらも0から2の範囲で生成している。
これでオーラスの条件らしいデータは一通りそろう。

作った条件が難易度に合うか確認する

点数をランダムに作っただけでは、選択した難易度に合うとは限らない。
Easyを選んだのに、目標達成へ跳満が必要な条件が作られることもある。
そこで、いったん条件をランダム生成し、その条件で必要な最低和了点を計算する。

if (
  !matchesDifficulty(
    scores,
    selfSeat,
    goal,
    difficulty,
    honba,
    kyotaku,
  )
) {
  continue;
}

難易度に合わなければ、その条件は使わずにもう一度生成する。
条件を直接計算して作るのではなく、

  1. 条件をランダムに作る
  2. 必要な和了点を計算する
  3. 選択した難易度に合うか確認する
  4. 合わなければ作り直す

という流れにした。
すでに作った必要点数計算を、そのまま難易度判定にも利用できた。

すでに目標を達成している条件は除外する

ランダム生成すると、開始時点ですでに目標順位を達成している場合がある。
たとえば、現在1着なのに目標もトップになっている状態。
これではアガリ条件を考える必要がないので、問題としてはあまり意味がない。
(逃げ切り、守りのシミュレーションとして現在1着をアリにしてもいいかもしれない)

if (selfRank <= goalRankLimit) {
  return false;
}

そのため、開始時点ですでに目標を達成している条件は除外している。
必要な和了点を計算できない条件も、同じように採用しない。

最大1,000回まで生成する

条件によっては、選択した難易度に合う組み合わせがなかなか見つからない可能性もある。
無限に処理を繰り返さないように、生成回数には上限を設けた。

const maxAttempts = 1000;

for (
  let attempt = 0;
  attempt < maxAttempts;
  attempt += 1
) {
  // 条件を生成して難易度を確認
}

1,000回試しても条件が見つからなかった場合は、nullを返す。
画面側ではアラートを表示し、もう一度試してもらう。

if (generatedCondition === null) {
  alert(
    "指定した難易度の条件を生成できませんでした。もう一度試してください。",
  );

  return;
}

今のところ、大半はすぐに生成できる想定。
ただ、難易度の範囲や生成条件を変更したときに見つからなくなる可能性もあるので、上限は必要そう。

生成した条件を画面へ反映する

条件が見つかったら、点数や自分の席などをReactの状態へ反映する。

setScores(generatedCondition.scores);
setSelfSeat(generatedCondition.selfSeat);
setGoal(generatedCondition.goal);
setHonba(generatedCondition.honba);
setKyotaku(generatedCondition.kyotaku);

仮の終局結果については、自分を和了者として設定する。
放銃者は、自分以外の3人からランダムに選んでいる。
ロン点やツモ支払い、テンパイ者などは初期値へ戻す。
前回追加した自動保存も動くため、生成された条件はそのままブラウザへ保存される。

今回できたこと

今回の進捗は以下のとおり。

  • Easy・Normal・Hardを選択できるようにした
  • 4人分の持ち点をランダム生成した
  • 供託を考慮して持ち点の合計を調整した
  • 自分の席と目標順位をランダムに選んだ
  • 本場と供託をランダム生成した
  • 必要な最低和了点から難易度を判定した
  • すでに目標達成済みの条件を除外した
  • 難易度に合わない場合は条件を作り直した
  • 生成した条件を画面と保存データへ反映した

これで、点数を一つずつ入力しなくても、ボタンを押してオーラス条件を試せるようになった。
計算結果を確認するだけのツールから、少しずつ練習問題のように使える形へ近づいてきたと思う。
次は、ここまで作った点数計算や条件生成が正しく動いているか確認するため、テストコードを追加していく。

難易度Normalでランダム生成した4人の点数と目標条件の画面
今回作ったランダム条件生成ボタン
条件ランダム生成ボタンを押して点数と目標が再生成された画面
生成ボタンを押すと、点数などの条件が再生成される。

牛一。

コメント

タイトルとURLをコピーしました