企画書

リレーから CPU までを、できるだけ小さい面積で組み上げる 1 人用パズルゲームの計画。 本書はルール、ステージ構成、エンジン設計、UI、技術構成、開発フェーズ、決定事項を定める。

1. ゲームの概要

プレイヤーはマス目の盤面に素子と配線を置き、ステージごとに指定された論理回路を作る。 最初に使える素子は relay (default on)relay (default off) の 2 種類だけである。 ステージをクリアすると、作った回路を 部品 として登録でき、以降のステージでは 1 個の素子として置ける。 部品の大きさは、作ったときの盤面の大きさそのままである。 つまり小さく作った部品ほど、後のステージで場所を取らない。

ステージの並びは nandgame に倣い、論理ゲートから加算器、ALU、メモリ、CPU へ進む。 nandgame との違いは次の 3 点に集約される。

したがって目的は「動く回路を作る」ことではなく、「動く回路をできるだけ小さい面積で作る」ことである。 ステージごとの面積が記録され、最終的には CPU 全体の面積が総合スコアになる。

2. ルール

2.1 盤面

盤面は幅 W、高さ H のマス目である。 プレイヤーは W と H を自由に変えられる(上限はステージごとに定める。初期値は 64×64 程度)。 ステージの入力ピンと出力ピンは盤面の外周に置く。 外周の各辺の各マス位置に 1 本までピンを置け、プレイヤーはピンを辺に沿って動かせる。

ピンの位置を盤面の外に置く理由は、部品化したときにそのまま部品の端子配置になるからである。 どの辺にどの端子を出すかは、次のステージでの配線のしやすさを左右する。

2.2 マスに置けるもの

各マスには次のいずれか一つを置く。

素子と部品は 90 度回転と鏡像反転ができる(8 通りの向き)。 面積最適化では端子の向きが効くため、回転と反転は最初の実装から入れる。

2.3 リレー

リレーは 1×1 で、3 本の端子を持つ。

relay (default on) は c が 0 のとき接点が閉じ、out に in の値が出る。c が 1 のとき接点は開き、out は何も出力しない。 relay (default off) はその逆で、c が 1 のとき閉じる。

接点は物理的には双方向だが、本ゲームでは in から out への一方向とする。 双方向にすると回路の解釈が難しくなり、プレイヤーに読める挙動でなくなるからである。

定数 1 は 1×1 で、4 辺すべてに 1 を出力する。 定数 0 は用意しない。どこにもつながっていない配線は 0 と読むので(次節)、必要なら未接続のままにすればよい。

2.4 信号のモデル

配線でつながったマスと端子の集合を ネット と呼ぶ。 ネットに値を与えるものを ドライバ と呼び、ステージの入力ピン、定数 1、リレーの out、部品の出力端子がドライバである。 ドライバが出す値は 1、0、または Z(何も出さない)である。 リレーの接点が開いているときの out は Z である。

ネットの値は次の規則で決まる。

このモデルの帰結として、リレーの out 同士をつなぐと OR になる(ワイヤード OR)。 これはリレー計算機で実際に使われた技法であり、面積最適化の主要な手段になる。 一方、ステージの入力ピン同士を直結して OR を作ることはできない。入力ピンは 0 を強く出すので、片方が 1 で片方が 0 のとき短絡になる。

部品の出力端子は、内部のネットの値をそのまま外へ出す。内部が Z なら外でも Z である。 これにより、リレーだけで作った部品はワイヤード OR に使えるが、値を 0 として出す部品は使えない。 部品の作り方がその後の使い勝手を変えるので、これも最適化の対象になる。

2.5 接続の規則

配線マス同士は、双方がその方向への接続を持つときつながる。 素子や部品の端子は、隣のマスの配線がその端子の方を向いているときつながる。 端子と端子が直接隣り合っている場合もつながる(配線マスを消費しない)。 端子のない辺同士が隣り合っても、何も起きない。

2.6 面積とスコア

ステージのスコアは盤面の面積 W×H である。 盤面に空きマスがあっても面積に含む。空きを減らすには盤面を縮めるしかない。 外周のピンは面積に含まない。

ステージごとに、参照解の面積を パー として表示する。 参照解は開発側で作り、エンジンの回帰テストにも使う。

2.7 部品の登録と再利用

ステージのテストにすべて合格した回路は、部品として登録できる。 登録する部品は名前を持つ(既定は「NAND 3×2」のような自動命名で、変更できる)。 同じステージから何個でも登録でき、ライブラリにはステージごとにまとめて表示する。

登録した部品は不変のスナップショットとする。 内部を編集したければ、複製して新しい部品を作る。 既に他の部品や回路から使われている部品は削除できない。

同じステージの部品で端子配置と大きさが同じものは、置いた後で差し替えられる(後の機能)。

3. ステージ構成

nandgame の並びを、リレーが原始素子である前提に合わせて組み替える。 括弧内はパー(開発側の参照解の面積)で、参照解のあるステージにだけ付く。参照解はエンジンのテストでもある。

論理ゲート

  1. NOT(2):定数 1 と relay (default on) を横に並べる。
  2. NAND(6):relay (default on) 2 個を並列にし、out をつなぐ。
  3. AND(3):relay (default off) 2 個を直列にする。
  4. OR(6):relay (default off) 2 個を並列にする。
  5. XOR(12):a を制御にして b を通すリレーと、b を制御にして a を通すリレーを並列にする。定数は要らず、リレーは 2 個で済むが、各入力を 2 か所へ配る配線と交差が面積を取る。

nandgame ではすべてを NAND から作るが、本ゲームでは各ゲートをリレーから直接作る方が小さい。 これは意図した違いで、「どの部品を何から作るか」自体を最適化の対象にする。

算術と切り替え

  1. セレクタ(12):2 入力 1 出力のマルチプレクサ。relay (default on) と relay (default off) を s で制御し、2 リレーで作れる。
  2. スイッチ:1 入力 2 出力のデマルチプレクサ。
  3. 半加算器(30):登録した XOR と AND を置いて作った参照解の面積。リレーから直接作れば小さくなる余地がある。
  4. 全加算器
  5. 多ビット加算器
  6. インクリメント
  7. 減算
  8. ゼロ判定
  9. 負数判定

ALU

  1. 論理ユニット:op1 op0 で AND、OR、XOR、NOT x を選ぶ。
  2. 算術ユニット:op1 op0 で x + y、x + 1、x - y、x - 1 を選ぶ。
  3. ALU:u で算術と論理を選び、zx で x を 0 に、sw で x と y を入れ替える。nandgame と同じ制御線で、語長だけ 8 ビット。
  4. 条件判定:x の符号とゼロを lt、eq、gt と照らす。

メモリ

  1. SR ラッチ(12):出力を自分の入力へ戻して保持する。
  2. D ラッチ(12):st が 1 の間だけ d を通し、0 の間は配線の輪で自分の値を保つ。
  3. D フリップフロップ(60):登録した D ラッチ 2 個と NOT で作った参照解の面積。clk の立ち上がりで取り込む。
  4. 1 ビットレジスタ:フリップフロップの前にセレクタを置き、st が 1 のときだけ取り込む。
  5. レジスタ
  6. カウンタ
  7. RAM(4 語):レジスタ 4 個とセレクタで作る。

CPU

  1. 制御ユニット:命令から書き込み先と分岐、停止の制御線を作る。
  2. CPU:命令とメモリの値を受け取り、pc と番地、メモリ書き込みを出す。nandgame と同じくメモリはステージ側にあり、3 本のプログラムを通せば合格。

ステージ 10 以降は語長(何ビットか)とバス配線の扱いに依存する。 この 2 点の決定は第 10 章にあり、実装は単ビットのステージが遊べるようになってから行う。

4. シミュレーションエンジン

エンジンは DOM に依存しない純粋な TypeScript モジュールとして書き、Deno のテストで検証する。

4.1 ネットリストの抽出

盤面の配線、交差、端子から union-find でネットを求める。 部品は再帰的に展開し、最終的にリレーと定数 1 だけからなる平坦なネットリストにする。 CPU 規模でもリレー数は数千程度と見込まれ、平坦化して問題ない。

4.2 評価

評価は固定点反復で行う。

  1. 各ネットの値をドライバから求める(2.4 節の規則)。
  2. 各リレーの out を、c と in の値から求める。
  3. ネットの値が変わらなくなるまで 1 と 2 を繰り返す。

反復回数に上限を設け、超えたら 発振 としてエラーにする(NOT の出力を自分の c に戻した場合など)。 短絡が起きた場合もエラーとして、該当ネットを盤面上で示す。

ラッチのような帰還回路は、前回の状態から反復を始めることで自然に状態を保持する。 リレーの状態を別に持つ必要はなく、ネットの値がそのまま状態になる。

4.3 テストの記述

ステージのテストは、入力の組と期待する出力の列で書く。

interface Step {
  set: Record<string, 0 | 1>; // 変更する入力
  expect: Record<string, 0 | 1>; // 評価後に確認する出力
}

組み合わせ回路は全入力パターンを列挙して生成する。 順序回路は手書きの列で、クロックの立ち上がりと立ち下がりを別ステップとして書く。 クロックは特別扱いせず、名前が clk の通常の入力ピンとして扱う。

5. データモデル

type Side = "n" | "e" | "s" | "w";

interface BorderPin {
  name: string;
  dir: "in" | "out";
  side: Side;
  index: number; // その辺の何マス目か
}

type Cell =
  | { kind: "wire"; n: boolean; e: boolean; s: boolean; w: boolean }
  | { kind: "cross" };

interface Placement {
  componentId: string; // "relay-on" | "relay-off" | "one" | 登録部品の id
  x: number;
  y: number;
  rotation: 0 | 1 | 2 | 3;
  mirror: boolean;
}

interface Design {
  width: number;
  height: number;
  cells: Record<string, Cell>; // "x,y" をキーにする
  placements: Placement[];
  pins: BorderPin[];
}

interface ComponentDef {
  id: string;
  name: string;
  stageId: string;
  width: number;
  height: number;
  pins: BorderPin[];
  design?: Design; // 原始素子とステージ提供の既製部品は持たない
  createdAt: string;
}

interface SaveData {
  version: 1;
  components: ComponentDef[];
  drafts: Record<string, Design>; // ステージごとの作業中の盤面
  best: Record<string, number>; // ステージごとの最小面積
}

6. UI

画面は 3 つに分ける。

エディタの操作は次のとおり。

盤面は SVG で描く。 64×64 程度までは SVG で十分だが、CPU ステージで盤面が大きくなった場合は Canvas への切り替えを検討する。

7. 保存

サーバーは持たない。 保存先はブラウザの localStorage で、SaveData を JSON として 1 キーに置く。 JSON ファイルのエクスポートとインポートを用意し、ブラウザをまたいだ持ち運びと共有に使う。 version を持たせ、スキーマ変更時に移行処理を書けるようにする。

8. 技術構成

このリポジトリのテンプレート(Remix v3 + @kuboon/remix-ssg + GitHub Pages)をそのまま土台にする。 ゲームは静的サイトとして GitHub Pages に置き、状態はすべてブラウザ側に持つ。

pages/
  lib/game/
    model.ts        # 第 5 章の型と、回転と反転の座標変換
    netlist.ts      # 盤面からネットリストへ
    sim.ts          # 固定点反復の評価器
    verify.ts       # テスト列の実行
    stages/         # ステージ定義(1 ファイル 1 ステージ)
    storage.ts      # localStorage とエクスポート
    *_test.ts       # 参照解を使った回帰テスト
  islands/game/
    editor.tsx      # エディタ本体
    stage-list.tsx
    library.tsx
    store.ts        # 保存データを島間で共有する
  pages/
    index.tsx
    play.tsx
    library.tsx

テンプレートのデモ(showcase、blog、counter)は最初のフェーズで削除する。 deno task test を追加し、CI で checktestbuild を回す。

本番はカスタムドメイン cpu.kbn.one で、BASE_URL はドメインのルートになる。 PR プレビューはサブパスに出るので、テンプレートの base の扱いをそのまま残す。

9. 開発フェーズ

各フェーズの終わりに、動くものが手元にある状態を保つ。

フェーズ 0:土台

フェーズ 1:エンジン

フェーズ 2:エディタ

フェーズ 3:ステージ進行と部品化

ここまでで NOT から全加算器までが遊べる。 このフェーズの終わりに一度遊んでみて、面積のスケール(リレー 1×1 でよいか)とパーの妥当性を確かめる。

フェーズ 4:順序回路

フェーズ 5:多ビットと CPU

10. 命令セット

CPU ステージの語長は 8 ビットで、命令も 8 ビットである。 レジスタは A(アキュムレータ兼アドレス)、D(データ)、PC(プログラムカウンタ)の 3 本。 M は番地 A のメモリの値を指す。

ビット 7〜0 意味
0 vvvvvvv A ← v(0〜127)
1 0 s1 s0 u o1 o0 d 計算。s1 s0 でオペランドを選び(00: x=A, y=D、01: x=A, y=M、10: x=D, y=A、11: x=0, y=M)、u o1 o0 で ALU の演算を選ぶ。d が 0 なら A に、1 なら D に書く
1 1 0 lt eq gt c _ 分岐。c が 0 なら D を、1 なら M を条件判定に通し、成り立てば PC ← A
1 1 1 0 ____ 記憶。M[A] ← D
1 1 1 1 ____ 停止。PC を進めない

即値が 7 ビットなのは nandgame と同じ理由で、上位ビットを命令の種別に使うからである。 128 以上の値が要るときは、即値を反転(NOT)するか、加算で作る。

CPU ステージは nandgame と同じくテストベンチの形をとる。 ステージは PC の命令 i と番地 A のメモリの値 m を入力に与え、pc、addr(A の値)、メモリ書き込み w とその値 data を確かめる。 メモリ自体はステージ側にあり、参照インタプリタが 3 本のプログラムを実行して期待値を作る。

11. 決定事項

計画時点で判断を要した点と、その決定を並べる。