囮の音を聞くかどうかは、壁を避けて歩ける距離で判定します。 Codexで作った「薄明の回廊」は、灯機の巡回を読み、記録を回収して出口へ戻る手番制ステルスパズルです。投げた囮の音で通路を開きますが、帰りには灯機も元の巡回へ戻ってきます。
この記事では、ゲーム本体のC#を使って床距離による聴覚、調査からの復帰、予告と一手戻す処理を再現します。画面を含めた全制作手順ではありません。Unityを起動せずに実行できるサンプルを用意しました。

C#サンプルを実行する
C#サンプルZIPを展開し、.NET 10 SDKのある環境で次を実行します。追加のNuGetパッケージはありません。
dotnet run --project HakumeiSample.csproj
PASS wall detour: direct distance 4 but floor distance 8 PASS sound reaches by floor path and moves guard PASS preview does not mutate source PASS snapshot is a deep copy PASS round trip needs record before exit PASS exhaustive no-decoy search cannot pass the occupied choke
| ファイル | 確認する処理 |
|---|---|
| Corridor.cs | 盤面・視界・床距離・行動・灯機の状態・6面の定義 |
| Program.cs | 16検査、代表面の33手、囮なし探索、予告の照合 |
| HakumeiSample.csproj | .NET 10で動く実行プロジェクト |
実行後の verification/core-report.json には検査数、入力列、探索数が残ります。今回の実行は3,000状態・12,220行動で予告と実行が一致しました。これは同じルールを二重に実行した整合性検査です。ルール自体の正しさは、壁越し・視界の向き・衝突などの独立した期待値で別に確かめています。
音の再生と「敵が聞いた」は別に決める
Unityの AudioSource.spatialBlend は、音声を2D/3Dの空間化へどれだけ寄せるかの設定です。これだけで本作の灯機へ調査命令を出す構成にはしていません。Unity公式APIの用途と区別しました。
| 判定 | 本作の条件 | 理由 |
|---|---|---|
| 囮を投げられる | マンハッタン距離4以下、壁を横切らない | 壁の向こうへ直接投げない |
| 灯機が聞く | 床の最短距離6以下 | 曲がる通路に音が回り込む |
| 灯機が見る | 前方90度、マンハッタン距離3以下、壁で遮断 | 背後と壁の陰を使える |
投げる距離と、音が届く距離は同じではありません。 効果音は操作への反応として再生し、音を消しても灯機のルールは変えません。これは現実の音響シミュレーションではなく、盤面を読めるように定めたゲームのルールです。
壁を回り込む距離を幅優先探索で求める
囮のマスを0として、上下左右の床へ1ずつ距離を広げます。壁と未到達を除き、一度到達したマスは再登録しません。床の移動費用がすべて1なので、幅優先探索で最短の床距離になります。
public int[] Distances(int destination)
{
var d = new int[Walls.Length];
for (int i = 0; i < d.Length; i++) d[i] = -1;
if (!Floor(destination)) return d;
var q = new Queue<int>();
d[destination] = 0;
q.Enqueue(destination);
while (q.Count > 0) {
int p = q.Dequeue();
for (int k = 0; k < 4; k++) {
int n = Neighbor(p, k);
if (Floor(n) && d[n] < 0) {
d[n] = d[p] + 1;
q.Enqueue(n);
}
}
}
return d;
}
第4面の (1,5) と (5,5) は、座標の距離なら4ですが、間に壁があります。通路を回る床距離は8。聴覚の上限6を超えるので音は届きません。サンプルではこの直距離4/床距離8を固定した期待値として検査しました。
見通し判定を聴覚へ流用しない
- 症状
- 原因
- 対処
音にも視界と同じ直線判定を使うと、曲がった通路から誘えなくなります。一方で直距離だけなら壁を挟んだ近い灯機まで反応します。この作品では床距離を選び、壁の厚さ・材質・音量は扱いません。
音を投げた手で、各灯機の現在地の距離を読みます。未到達の -1 を先に除外するのは、負の距離が「6以下」に入るのを防ぐためです。
Decoys--;
var d = Level.Distances(action.Target);
foreach (var g in Guards)
if (d[g.Position] >= 0 && d[g.Position] <= 6) {
g.Target = action.Target;
g.Listen = 2;
g.Returning = false;
}
調査・2手の待機・巡回復帰を状態として持つ
灯機は囮へ瞬間移動しません。各行動の確定時に、最短の床経路を一マス進みます。同距離の分岐は北・東・南・西の順で選び、予告と本番の分岐が変わらないようにしています。
| 状態 | 次の一手 |
|---|---|
| 通常巡回 | 定義された次の巡回点へ進む |
| 調査中 | 音のマスへ進む。新しい音を聞けば調査先を更新 |
| 調査先に到着 | その場で2手待つ |
| 復帰中 | 巡回の先頭へ戻り、向きと巡回位置を復元 |
実装で持つ値は Position、Facing、Cursor、Target、Listen、Returning です。位置だけでは、同じ場所にいる灯機の次の行動を決められません。 調査中なのか、帰る途中なのか、待機が何手残るかで変わります。
代表面の最初の往復経路は、22手目に灯機のいるマスへ進んで失敗しました。音を聞かせた後に安全だと思い込み、帰路の位置を確認し足りなかったためです。
Witness failed at turn 22 Move:24 灯機1に発見。灯機のいるマスへ進みました。
帰りの合流前へ待機を一手入れ、33手で回収・帰還できました。後の探索では囮の位置を変えた28手の経路も見つかりました。33手は修正した経路の証拠で、最短手数ではありません。 灯機の現在地へ移動した場合は、灯機がその後に動くとしても衝突扱いにしています。
予告とUndoには灯機の状態までコピーする
選択時には本体を変更せず、コピーに同じ Apply を実行します。移動、音、灯機の更新、発見、回収、帰還の順序を別実装にしません。
public Run Preview(TurnAction action) {
var r = Copy();
r.Apply(action);
return r;
}
public Run Copy() {
var r = (Run)MemberwiseClone();
r.Guards = new GuardState[Guards.Length];
for (int i = 0; i < Guards.Length; i++)
r.Guards[i] = Guards[i].Copy();
return r;
}
MemberwiseClone だけでは参照型の中身を共有します。Microsoft公式の説明を確認し、配列と各灯機を別にしました。GuardState のフィールドは整数と真偽値なので、その段階の浅いコピーで値を独立させられます。変更しない盤面定義は共有しています。
Undoは行動直前のコピーをスタックへ置き、プレイヤーだけでなく、灯機の待機残数・調査先・囮残数・回収状態も戻します。UIで発見後の「一手戻す」を押し、危険な行動を選ぶ前の状態へ戻ることもブラウザーで確認しました。
6面で「同じ操作だけ」を避ける
以前の作品には、ユーザーから「簡単すぎる」「ステージの差がたいしてない」という指摘がありました。今回は囮を増やすだけの展開を避け、最初は囮なしの待機、次は奥へ進む一方通行、後半は往復と複数の灯機を組み合わせました。
| 面 | 新しく考えること | 確認したクリア経路 | 安全ランダムの成功 |
|---|---|---|---|
| 巡回する中庭 | 壁の陰で待って巡回を横切る | 15手 | 13 / 1,000 |
| 音で開く記録室 | 一つの囮で奥へ進む | 17手 | 3 / 1,000 |
| 灯機を誘う往復路 | 帰りのために囮を残す | 28手 | 0 / 1,000 |
| 待ち伏せを迂回する | 調査先へ追いつかず脇へ隠れる | 29手 | 0 / 1,000 |
| 二つの灯機 | 聞く灯機と巡回のタイミングを分ける | 32手 | 0 / 1,000 |
| 夜明け前の三つの記録 | 三つの囮と二重の巡回を配分する | 33手 | 0 / 1,000 |
安全ランダムは、予告で即発見になる行動を除いて選び、80手で打ち切った比較です。各面1,000回、固定した乱数で実行しました。無作為に危険へ飛び込む方式より強い基準ですが、人の難易度、面白さ、最適解の証明ではありません。 全6面には固定の解を置き、PCと375pxのタッチ操作でも帰還まで照合しています。実機スマホは未検証です。

Unityへ組み込むときの順序と、この方式の限界
Corridor.csをUnityのスクリプトへ置き、new Run(Levels.All[0])で開始する。- タップしたマスから
TurnActionを作り、Legalで選択できるか確認する。 Previewの結果で灯機と視界の予告を描く。選択時には本体へApplyしない。- 確定時に本体をコピーして保存し、
Applyを一度だけ呼ぶ。 - 結果の
PhaseとEventを画面へ表示し、Undoで保存した状態を復元する。
本作は最大9×9の固定盤面です。現在の NextToward は呼び出すたびに距離を計算するため、大きな盤面や多数の敵へそのまま拡張する場合には、距離マップの共有や経路更新の条件を見直します。動く壁、音量の減衰、連続時間の追跡はサンプルの対象外です。
音で通路を開いても、帰り道まで安全になるわけではない。
まず代表面の固定経路で調査からの復帰を追い、それから囮の位置を変えて予告を比べると、状態を持つ理由が見えます。画面を作る工程はタイトル画面の作り方、日本語文字の容量はWebGLフォントのサブセットで扱っています。