企画書
リレーから 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 マスに置けるもの
各マスには次のいずれか一つを置く。
- 配線:上下左右 4 方向のうち接続する方向を持つ。3 方向以上つなげば分岐になる。
- 交差:縦の配線と横の配線が互いにつながらずに交差する。1 マスを消費する。
- 素子:relay (default on)、relay (default off)、定数 1 の 3 種類。いずれも 1×1。多ビットのステージからは Bus split(1×8)も使える。
- バス配線:8 本の配線を 1 マスにまとめたもの。1 本の配線とは直接つながらず、Bus split で 8 本にばらす。
- 部品:登録済みの部品。登録時の W×H を占める。
素子と部品は 90 度回転と鏡像反転ができる(8 通りの向き)。 面積最適化では端子の向きが効くため、回転と反転は最初の実装から入れる。
2.3 リレー
リレーは 1×1 で、3 本の端子を持つ。
- c(制御、北側):コイルへの入力。
- in(西側):接点の入力。
- out(東側):接点の出力。
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 である。
ネットの値は次の規則で決まる。
- 1 を出すドライバと 0 を出すドライバが同居していれば 短絡 で、回路はエラーになる。
- 1 を出すドライバがあれば 1。
- 0 を出すドライバがあれば 0。
- すべて Z なら 0。
このモデルの帰結として、リレーの out 同士をつなぐと OR になる(ワイヤード OR)。 これはリレー計算機で実際に使われた技法であり、面積最適化の主要な手段になる。 一方、ステージの入力ピン同士を直結して OR を作ることはできない。入力ピンは 0 を強く出すので、片方が 1 で片方が 0 のとき短絡になる。
部品の出力端子は、内部のネットの値をそのまま外へ出す。内部が Z なら外でも Z である。 これにより、リレーだけで作った部品はワイヤード OR に使えるが、値を 0 として出す部品は使えない。 部品の作り方がその後の使い勝手を変えるので、これも最適化の対象になる。
2.5 接続の規則
配線マス同士は、双方がその方向への接続を持つときつながる。 素子や部品の端子は、隣のマスの配線がその端子の方を向いているときつながる。 端子と端子が直接隣り合っている場合もつながる(配線マスを消費しない)。 端子のない辺同士が隣り合っても、何も起きない。
2.6 面積とスコア
ステージのスコアは盤面の面積 W×H である。 盤面に空きマスがあっても面積に含む。空きを減らすには盤面を縮めるしかない。 外周のピンは面積に含まない。
ステージごとに、参照解の面積を パー として表示する。 参照解は開発側で作り、エンジンの回帰テストにも使う。
2.7 部品の登録と再利用
ステージのテストにすべて合格した回路は、部品として登録できる。 登録する部品は名前を持つ(既定は「NAND 3×2」のような自動命名で、変更できる)。 同じステージから何個でも登録でき、ライブラリにはステージごとにまとめて表示する。
登録した部品は不変のスナップショットとする。 内部を編集したければ、複製して新しい部品を作る。 既に他の部品や回路から使われている部品は削除できない。
同じステージの部品で端子配置と大きさが同じものは、置いた後で差し替えられる(後の機能)。
3. ステージ構成
nandgame の並びを、リレーが原始素子である前提に合わせて組み替える。 括弧内はパー(開発側の参照解の面積)で、参照解のあるステージにだけ付く。参照解はエンジンのテストでもある。
論理ゲート
- NOT(2):定数 1 と relay (default on) を横に並べる。
- NAND(6):relay (default on) 2 個を並列にし、out をつなぐ。
- AND(3):relay (default off) 2 個を直列にする。
- OR(6):relay (default off) 2 個を並列にする。
- XOR(12):a を制御にして b を通すリレーと、b を制御にして a を通すリレーを並列にする。定数は要らず、リレーは 2 個で済むが、各入力を 2 か所へ配る配線と交差が面積を取る。
nandgame ではすべてを NAND から作るが、本ゲームでは各ゲートをリレーから直接作る方が小さい。 これは意図した違いで、「どの部品を何から作るか」自体を最適化の対象にする。
算術と切り替え
- セレクタ(12):2 入力 1 出力のマルチプレクサ。relay (default on) と relay (default off) を s で制御し、2 リレーで作れる。
- スイッチ:1 入力 2 出力のデマルチプレクサ。
- 半加算器(30):登録した XOR と AND を置いて作った参照解の面積。リレーから直接作れば小さくなる余地がある。
- 全加算器
- 多ビット加算器
- インクリメント
- 減算
- ゼロ判定
- 負数判定
ALU
- 論理ユニット:op1 op0 で AND、OR、XOR、NOT x を選ぶ。
- 算術ユニット:op1 op0 で x + y、x + 1、x - y、x - 1 を選ぶ。
- ALU:u で算術と論理を選び、zx で x を 0 に、sw で x と y を入れ替える。nandgame と同じ制御線で、語長だけ 8 ビット。
- 条件判定:x の符号とゼロを lt、eq、gt と照らす。
メモリ
- SR ラッチ(12):出力を自分の入力へ戻して保持する。
- D ラッチ(12):st が 1 の間だけ d を通し、0 の間は配線の輪で自分の値を保つ。
- D フリップフロップ(60):登録した D ラッチ 2 個と NOT で作った参照解の面積。clk の立ち上がりで取り込む。
- 1 ビットレジスタ:フリップフロップの前にセレクタを置き、st が 1 のときだけ取り込む。
- レジスタ
- カウンタ
- RAM(4 語):レジスタ 4 個とセレクタで作る。
CPU
- 制御ユニット:命令から書き込み先と分岐、停止の制御線を作る。
- CPU:命令とメモリの値を受け取り、pc と番地、メモリ書き込みを出す。nandgame と同じくメモリはステージ側にあり、3 本のプログラムを通せば合格。
ステージ 10 以降は語長(何ビットか)とバス配線の扱いに依存する。 この 2 点の決定は第 10 章にあり、実装は単ビットのステージが遊べるようになってから行う。
4. シミュレーションエンジン
エンジンは DOM に依存しない純粋な TypeScript モジュールとして書き、Deno のテストで検証する。
4.1 ネットリストの抽出
盤面の配線、交差、端子から union-find でネットを求める。 部品は再帰的に展開し、最終的にリレーと定数 1 だけからなる平坦なネットリストにする。 CPU 規模でもリレー数は数千程度と見込まれ、平坦化して問題ない。
4.2 評価
評価は固定点反復で行う。
- 各ネットの値をドライバから求める(2.4 節の規則)。
- 各リレーの out を、c と in の値から求める。
- ネットの値が変わらなくなるまで 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 つに分ける。
- ステージ一覧(
/):ステージの並び、クリア状況、最小面積とパーを表示する。 - エディタ(
/play?stage=<id>):盤面、パレット、テストパネル。 - ライブラリ(
/library):登録済み部品の一覧、名前の変更、削除、エクスポート。
エディタの操作は次のとおり。
- 配線はドラッグで引く。既存の配線をドラッグで消す。
- パレットから素子や部品をクリックして選び、盤面をクリックして置く。
- R で回転、F で反転、Delete で削除。
- 盤面の端をドラッグして W と H を変える。縮めて収まらないものがあれば拒否する。
- 外周のピンをドラッグして辺に沿って動かす。
- 入力ピンをクリックすると値が反転し、盤面の配線が値に応じて光る(常時シミュレーション)。
- テストパネルは全テストの合否を表示し、失敗したステップを選ぶとその入力が盤面に反映される。
- 全合格で「部品として登録」ボタンが有効になる。
盤面は 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 で check、test、build を回す。
本番はカスタムドメイン cpu.kbn.one で、BASE_URL はドメインのルートになる。
PR プレビューはサブパスに出るので、テンプレートの base の扱いをそのまま残す。
9. 開発フェーズ
各フェーズの終わりに、動くものが手元にある状態を保つ。
フェーズ 0:土台
- テンプレートのデモを削除する。
- 第 5 章の型と、第 3 章のステージ定義(NOT から XOR まで)を書く。
deno task testと CI を整える。
フェーズ 1:エンジン
- ネットリスト抽出、評価器、テスト実行を実装する。
- NOT から XOR までの参照解を手書きの
Designとして用意し、テストで検証する。 - 短絡と発振の検出をテストする。
フェーズ 2:エディタ
- 盤面の描画、配線のドラッグ、素子の配置、回転と反転、盤面のリサイズ、ピンの移動。
- 常時シミュレーションと配線の点灯。
- localStorage への自動保存。
フェーズ 3:ステージ進行と部品化
- ステージ一覧、テストパネル、部品登録、ライブラリ。
- パレットに登録部品を出し、配置した部品を平坦化して評価する。
- エクスポートとインポート。
ここまでで NOT から全加算器までが遊べる。 このフェーズの終わりに一度遊んでみて、面積のスケール(リレー 1×1 でよいか)とパーの妥当性を確かめる。
フェーズ 4:順序回路
- ラッチ、フリップフロップ、レジスタ、カウンタのステージと順序テスト。
- クロックの手動操作と自動ステップ実行。
フェーズ 5:多ビットと CPU
- 第 10 章の決定に従い、バス配線と分岐、結合の素子を実装する。
- 多ビット加算器から 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. 決定事項
計画時点で判断を要した点と、その決定を並べる。
- 語長:8 ビットとする。nandgame は 16 ビットだが、面積ゲームでは 16 ビットのレジスタや ALU は盤面が巨大になる。4 ビットは命令の表現力が足りず、CPU ステージが窮屈になる。
- バス配線:多ビットのステージから、1 マス幅のバス配線と、分岐と結合の素子を導入する。面積の物理的な意味は薄れるが、8 本の配線を手で引く作業を避けることを優先する。
- リレーの大きさ:1×1 を基準にする。フェーズ 3 の終わりに遊んで、面積のスケールとパーの妥当性を確かめてから再検討する。
- 交差:交差マスを最初から許可する。禁止すると平面回路の制約が強すぎて、XOR の時点で難しくなりすぎる。
- UI の言語:日本語 UI に、ステージ名と部品名は英語(NAND、ALU)を混ぜる。英語 UI は後で足せる構成にする。
- 既製部品:大容量 RAM のように、ステージ側が面積つきで提供する既製部品を認める。nandgame に倣う。