マガジン

ピクロス・お絵かきロジックの作り方:問題生成と解答判定

遊びをつくる

執筆にClaudeを使用しています。

しおり・ハート・時計の目印が付いた棚から、小さなゲーム世界の本を取り出すイラスト
記事のテーマをもとに制作したAI生成イラスト。

ピクロス(お絵かきロジック、ノノグラム)をブラウザゲームとして作りたいとき、最初に迷うのは「どうやって問題を自動生成するか」と「プレイヤーの回答をどう判定するか」の2点です。見た目はシンプルなパズルですが、ヒント数列の計算ロジックと解答検証の実装は、知らないと一から考えるのに時間がかかります。 この記事では、ピクロスの仕組みを整理したうえで、問題生成(ヒント数列の導出)と解答判定(一意解の確認・プレイヤー入力の照合)を具体的なコードの考え方とともに説明します。「絵を描いてパズルにする」流れと「ランダムに問題を量産する」流れの両方を扱います。

この記事の目次
  1. ピクロスの構造を整理する
  2. ヒント数列を導く関数を作る
  3. 問題を用意する2つの方針
  4. 解答判定を実装する
  5. よくある実装の落とし穴
  6. 次の行動
  7. 参考資料

ピクロスの構造を整理する

ピクロスは N×M のグリッドを使い、各行・各列に「塗りつぶしが何マス続くか」をヒントとして示すパズルです。たとえば行のヒントが [3, 1] なら、その行には「3マス連続の塗りつぶし」と「1マスの塗りつぶし」が左から順に並び、それぞれの間には少なくとも1マスの空白が入ります。

実装上は、グリッドを 0(空白)と 1(塗りつぶし)の二次元配列として持てば十分です。行も列もインデックスで扱えるので、grid[row][col] の形が最も扱いやすいでしょう。問題生成のゴールは「この二次元配列からヒント数列を導く関数」を作ることで、解答判定のゴールは「プレイヤーが入力した二次元配列とヒントが整合しているか確認する関数」を作ることです。

ヒント数列を導く関数を作る

一行分の配列(例:[1,1,0,1,1,1,0,1])からヒント数列([2,3,1])を導くには、連続する 1 の塊を数えるだけです。

function calcHints(line) {
  const hints = [];
  let count = 0;
  for (const cell of line) {
    if (cell === 1) {
      count++;
    } else if (count > 0) {
      hints.push(count);
      count = 0;
    }
  }
  if (count > 0) hints.push(count);
  return hints.length > 0 ? hints : [0];
}

全行・全列に対してこの関数を呼び出すだけで、グリッド全体のヒントセットが完成します。列ヒントを取り出すには、二次元配列を転置(行と列を入れ替えた配列を作る)してから同じ関数を使うのが素直です。

ヒントが [0] になる行や列は「すべて空白」を意味します。これを画面にどう表示するかはデザインの判断ですが、慣例として空白のまま表示するか「0」と表示するかのどちらかが多いです。

問題を用意する2つの方針

絵からパズルを作る

ドット絵や二値化した画像をグリッドに変換し、そこからヒントを生成する方法です。プレイヤーが「完成図を知っている」クリエイターモードや、キャラクターをモチーフにしたパズルに向いています。

手順はシンプルで、「画像を読み込む → ピクセルを閾値で白黒に変換する → グリッドの二次元配列に変換する → ヒント生成関数を呼ぶ」の4ステップです。ブラウザ環境では <canvas> と getImageData() を使ってピクセルデータを取り出せます。ただし著作権のある素材を使って問題を配布する場合は、権利関係を事前に確認する必要があります。

ランダムにグリッドを生成する

乱数でグリッドを埋めてヒントを導く方法は実装が最も速いですが、そのままでは「解が一意でないパズル」が大量に生まれます。ピクロスは論理的に唯一の答えが導ける(一意解を持つ)ことが品質の基準なので、ランダム生成後に一意解チェックを挟む工程が必要です。

一意解チェックのアプローチは2種類あります。

  • ソルバーで解いて確認する:ヒントだけを入力として受け取り、論理的に解くソルバーを実装し、ソルバーが解ける問題だけを採用します。解けなかった問題は捨てるか、グリッドを微調整して再試行します。
  • 解の数を数える:バックトラッキングで解の総数を数え、1以外なら却下します。小さいグリッド(10×10 以下)なら許容範囲の計算量ですが、グリッドが大きくなるほど時間がかかります。

量産目的なら、まずソルバーベースのアプローチで「論理的に解けるか」を確認する方が実用的です。すべての問題に完全な一意解保証を課すと生成コストが大きくなるため、難易度設定の段階でどこまで保証するかを決めておくと作業がスムーズになります。

解答判定を実装する

プレイヤーの入力を判定する方法は「ヒント照合」と「グリッド照合」の2種類があります。

ヒント照合は、プレイヤーが入力したグリッドに対してヒント生成関数を実行し、元のヒントと一致するかを比べます。絵の元データを保持しなくてよいため、ヒントだけ配信したい場合に向いています。一方で、元データと異なるグリッドでも偶然ヒントが一致する場合(一意解でない問題)に誤判定を許してしまうリスクがあります。

グリッド照合は、プレイヤーの入力と正解グリッドをセルごとに比較します。一意解が保証されているならヒント照合と結果は同じですが、正解データをクライアントに持たせるとゲームファイルを調べられた際にネタバレになります。これを嫌う場合は、正解ハッシュをサーバー側で検証するか、難読化とトレードオフを承知のうえでクライアント完結にするかを選ぶことになります。ブラウザゲームとしてカジュアルに公開する用途なら、クライアント完結のヒント照合で十分なことが多いです。

判定のタイミングも設計の一部です。全マス入力後に一括判定する方式と、行・列単位でリアルタイムにヒントを色付けして「この行は確定」と伝える方式では、ゲームの手触りが大きく変わります。後者はプレイヤーにフィードバックを与えながら解けるため、初心者向けの難易度設計に使いやすいです。

よくある実装の落とし穴

ヒントの [0] 処理を忘れる:塗りつぶしが0マスの行・列は hints = [] になりがちです。後の照合時に空配列と [0] を同一視するか、統一して [0] を使うかを最初に決めておかないと、判定が通らないバグに悩まされます。

グリッドの転置を使い回さない:列ヒントを計算するたびに転置処理を走らせるとコードが読みにくくなります。初期化時に行配列と列配列を両方生成して保持しておくと、ヒント計算・表示・判定のすべてを同じ関数で扱えます。

大きいグリッドで一意解チェックに時間がかかる:15×15 以上のグリッドでバックトラッキングによる全解数え上げを行うと、問題によってはブラウザが固まることがあります。setTimeout や Web Workers を使って非同期化するか、生成処理をビルド時にオフラインで済ませてデータだけをゲームに埋め込む方法が現実的です。

ランダム生成の密度が偏る:塗りつぶし率が高すぎると「ほぼ全部塗る」問題になり、論理的に解く余地が少なくなります。経験的には塗りつぶし率40〜60%あたりが論理パズルとして成立しやすいとされています。生成時に密度を制御するパラメータを持たせておくと、難易度チューニングがしやすくなります。

次の行動

ヒント生成関数と解答判定をHTMLとJavaScriptで一つのファイルに実装し、まず5×5の小さいグリッドで動作を確認するのが最速の出発点です。動いたら10×10に広げ、ランダム生成と一意解チェックを追加していくと、段階的に完成度を上げられます。完成したゲームはZIPファイルにまとめてアソバースに投稿できますので、パズルとして公開して他のプレイヤーに解いてもらいながら改善を続けてみてください。

参考資料

  • Nonogram(ノノグラム)に関するアルゴリズム解説(Wikipedia "Nonogram" 英語版)
  • MDN Web Docs「CanvasRenderingContext2D.getImageData()」
  • MDN Web Docs「Web Workers API」