ホーム › 開発ブログ › 日替わりパズルで「全員が同じ問題」になる仕組み:日付シードと疑似乱数の実装メモ
開発メモ

日替わりパズルで「全員が同じ問題」になる仕組み:日付シードと疑似乱数の実装メモ

POCKET ARCADEのデイリーゲーム(ブレイン・スキャン、Astra Tiles、COLOR CODE)は、同じ日に遊ぶ人全員に同じ問題を出しています。ところが、問題をサーバーから配っているわけではありません。問題はすべて、遊ぶ人のブラウザの中でその場で作られています。それでも全員が同じ問題になるのはなぜか——この記事では、その仕組みである「日付シード」と「疑似乱数」を、実際のコードとあわせて解説します。

Math.random() では同じ問題を作れない

JavaScriptでランダムな数を作るときによく使うMath.random()は、呼び出すたびに予測できない数を返します。同じページを2人が開いても、出てくる数はまったく違います。デイリーゲームの問題作りには、これでは困ります。

そこで使うのが疑似乱数です。疑似乱数は、最初に与える数(シード)が同じなら、何度やり直しても、どの端末で動かしても、まったく同じ数の並びを返す計算式です。見た目はランダムでも、中身は決まった計算なのです。

仕組みは3ステップ

  1. 今日の日付を文字列にする:例「2026-10-10」
  2. 日付の文字列から数(シード)を作る:文字列を「ハッシュ関数」に通して、32ビットの整数にする
  3. シードから疑似乱数を作り、問題を生成する:同じシードからは同じ乱数の並びが出るので、同じ問題になる

日付が変われば文字列が変わり、シードが変わり、問題も変わります。サーバーに問い合わせる必要がないので、通信できない環境でも同じ問題を遊べます。

実際のコード

ブレイン・スキャンでは、ハッシュ関数に「FNV-1a」、疑似乱数に「mulberry32」という、どちらも短くて高速なアルゴリズムを使っています。

// 文字列 → 32ビット整数(FNV-1a)
function hashString(str) {
  let h = 2166136261;
  for (let i = 0; i < str.length; i++) {
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}

// シード → 0以上1未満の疑似乱数を返す関数(mulberry32)
function mulberry32(a) {
  return function () {
    a |= 0; a = a + 0x6D2B79F5 | 0;
    let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}

const seed = hashString('brain-scan:' + '2026-10-10');
const rng = mulberry32(seed);
rng(); // 何度実行しても、同じ日なら同じ数の並びになる

シードの文字列に「brain-scan:」のようなゲーム名を付けているのは、同じ日付でもゲームごとに違う問題にするためです。COLOR CODEでは、同じ考え方で「xmur3」というハッシュ関数とmulberry32を組み合わせています。

つまずきやすい4つのポイント

1. 乱数を使う順番を変えてはいけない

疑似乱数は「何回目に呼んだか」で返す数が決まります。問題を作る途中で、ある人の環境だけ余分に乱数を呼んでしまうと、それ以降の問題がずれてしまいます。たとえば「画面が小さいときだけ乱数で位置を調整する」といった処理を問題生成の途中に入れると、端末によって問題が変わる原因になります。

ブレイン・スキャンでは、5つのテストそれぞれに別のシード(日付のシード+テスト番号ごとのずらし)を与えています。こうすると、1つ目のテストで乱数を使う回数がプレイヤーの操作によって変わっても、2つ目以降のテストの問題には影響しません。

2. 「今日」はタイムゾーンで変わる

日付を作るときに、端末の時刻(ローカル時刻)を使うか、日本時間に固定するかで、日付が切り替わる瞬間が変わります。海外から遊ぶ人に日本時間の0時で切り替えたい場合は、日本時間に変換してから日付の文字列を作る必要があります。Astra Tilesは日本時間で日付を決めています。どちらを選ぶかは、「誰に、いつ同じ問題を遊んでほしいか」で決めます。

3. 「1日1回」は端末に記録する

問題が日付で決まる以上、同じ日に何度でも遊び直せてしまいます。1日1回にするには、遊び終えた日付をブラウザ(localStorage)に記録し、同じ日に再び開いたときは結果だけを表示します。ブレイン・スキャンでは、テストを1つ終えるたびに途中経過も保存し、途中でページを閉じても続きから再開できるようにしています。

4. 問題の「質」は数を作ってから確かめる

乱数で問題を作ると、まれに簡単すぎる・難しすぎる問題が出ることがあります。開発中に何百日分もの問題をまとめて生成し、難しさの偏りがないかを確認しておくと安心です。

この仕組みの良いところ

  • サーバーがいらない:問題を配る仕組みを作らなくても、全員に同じ問題を出せる。
  • オフラインでも動く:一度ページを読み込めば、通信なしで問題を作れる。
  • 過去の問題を再現できる:日付さえわかれば、何日前の問題でも同じものを作り直せる。

一方で、問題の作り方(コード)を変えると、同じ日付でも問題が変わってしまいます。デイリーゲームを公開したあとに問題生成のコードを変更するときは、その日の途中で問題が変わらないよう、切り替えのタイミングに注意が必要です。

まとめ

  • 日付の文字列をハッシュ関数でシードにし、疑似乱数で問題を作ると、全員が同じ問題になる
  • 乱数を呼ぶ順番は固定する。テストごとにシードを分けると安全
  • 日付の切り替えをローカル時刻にするか日本時間にするかを決めておく
  • 1日1回の制限は、遊んだ日付をブラウザに記録して実現する
POCKET ARCADE 開発チーム
「サイバー&ロジック」をテーマに、インストール不要のオリジナルブラウザゲームを制作・運営しています。記事内の数値は、各ゲームの実際のプログラムの設定値にもとづいています。