PRODUCT NOTE / NETWORK DESIGN
迷路をいちばん速く抜けるゲーム。オンラインでは 2〜4 人が別々の端末で同じ迷路を走る。
この文書は遊び方ではなく、端末どうしをどうつなぎ、どちらの向きに何を流すと決めたかを記録した開発ノートです。
| ステータス | β版(Web と Android の APK を配布中)。ストア版は未リリースで、リリース日は未定 |
|---|---|
| 現行バージョン | v0.6.0(2026-09-23 の Web ビルドと APK)。ストア版で v1.0.0 にする |
| 遊ぶ | nzw.jp/static/app/nukero/web/(インストール不要。部屋コードを渡せば別の端末から入れる) |
| 初版 | 2026-09-21(最初のコミット) |
| 前身 | 爆弾ゲーム(10 数年前) ・ Algo Derby(2025-12) ・ BOM MAN ARENA(2026-03) ・ グループチャット(2026-06)。下の「前身」を参照 |
| 提供形態 | Web と Android の APK 直配布(Flutter)。NUKERO.apk。iOS はビルド(IPA)まで用意したが、ストアには出していない |
| 通信 | WebSocket(wss://nzw.jp/v1/ws/maze/{部屋コード})・メッセージは JSON |
| サーバー | nzw.jp の API(FastAPI)に同居する中継 maze_ws.py(203 行)。部屋はメモリだけ。端末側の通信は 1,341 行 |
| 最終更新 | 2026-10-10 |
| 技術 | Flutter ・ 純 Dart の core/(決定的な進行)・ web_socket_channel ・ speech_to_text |
| 関連リンク | Synapse Link の開発ノート(固定ステップで踏んだ 2 倍速) ・ プロダクトの系譜 ・ 開発ノート一覧 |
オンラインでは、進行を部屋を作った人(ホスト)の端末が持ち、サーバーは部屋と席の管理と受け渡しだけを行う。
2026-09-23、Web 版をβとして公開した。通信の作りは手元の 2 台では確かめきれないので、インストールの要らない形を先に出して人の回線で試す。
計測 2026-09-23(β版のコード)。大きさは core/ をそのまま回して JSON にしたもの、中継は開発機から本番の wss://nzw.jp へホストとゲストをつないで 40 回ずつ。内訳は下の「実測」。
core/(Flutter を import しない純 Dart)に 1 本だけある。1 台で遊ぶときも同じコードが動く。| 遊び方 | 人数 | 通信 | 進行を持つ端末 |
|---|---|---|---|
| ひとりで | 1 人(銃ありのタイムアタック) | なし | 自分の端末 |
| みんなで(1 台) | 2〜4 人・CPU も入れられる | なし | その端末 |
| オンライン対戦 | 2 人。撃たれた相手は 2 秒しびれる | WebSocket | ホストの端末 |
| オンライン協力 | 2〜4 人。味方には当たらない | WebSocket | ホストの端末 |
このゲームは 2026-09-21 に始まって 3 日で今の形になっている。ゼロから考えたからではなく、四つの前身から部品を持ってきたからで、新しく書いたのはそれらを決定的につなぎ直す部分だった。前の 3 本はどれも素の JavaScript で、344 行・2,882 行・2,648 行ある。
ただしどれの後継でもない。BOM MAN ARENA は「爆弾を置いて敵を全滅させる」ゲームで、こちらは「迷路をいちばん速く抜ける」ゲーム。爆弾も炎もパワーアップも 1 つも入っていない。持ってきたのは遊びではなく、その下の仕組みのほうだった。
| 前身 | いつ | 持ってきたもの | 置いてきたもの |
|---|---|---|---|
| 爆弾ゲーム | 10 数年前 2023-09 公開 |
マス目の偶数・奇数で柱を立てる格子の壁。壊せる壁があるという考え方 | ランダムに歩くだけの敵、0.7 秒ごとの setInterval、座標が一致したら当たりという判定 |
| Algo Derby | 2025-12 | 迷路の掘り方(深さ優先の穴掘り法。掘った先だけでなく間のマスも抜く書き方まで同じ)、ゴールからの距離場を幅優先で作って順位に使うこと、そして迷路をレースにするという遊びそのもの | 右手の法則・集合知・量子トンネルといったアルゴリズムの見本市と、賭け。何よりシードを持たない乱数 |
| BOM MAN ARENA | 2026-03 | 敵の巡回の決め方(直進を優先し、分かれ道でだけ選び直し、行き止まりで反転)、見つける距離 8、追跡者の「巡回・追跡・見失い」の三つの状態、土管のワープ、入った部屋だけ見せる霧、効果音の作り方 | 爆弾・炎・キック・リモコン・パワーアップの全部。勝ち方も「敵を全滅させる」から「速く抜ける」へ変えた |
| グループ チャット |
2026-06 | 部屋ごとに接続を持って自分以外へ配る中継の骨格、4000 番台の close コードで断る理由を返す作法(「部屋が無い」の 4004 は同じ番号を使っている)、先に受けてから検査して閉じる順番 |
ログイン(JWT と許可リスト)、データベース、REST。部屋の作成も照会も、WebSocket の接続そのもので行っている |
いちばん大きく変えたのは「決定的にした」こと。Algo Derby は迷路も走りっぷりも実行のたびの乱数で決まるので、同じレースは二度と再現できない。NUKERO! は逆に、同じ設定・シード・入力なら必ず同じ結果になるよう core/ を縛った。迷路を送らずシードだけを配るオンライン(下の決定)は、この書き換えの上にしか建てられない。
BOM MAN ARENA から持ってきた部分も、そのままでは使えなかった。壊れ方が分かっていた 3 つは、コードの中に日付つきで直した記録が残っている。
DateTime を使わないこの 4 本がどう分かれてきたかは プロダクトの系譜に 1 枚の図で置いてある。爆弾ゲームから BOM MAN ARENA までが実線(同じ血)で、NUKERO! へ入る 3 本は破線(部品の受け渡し)になっている。
狭い画面では図を横にスクロールできる。
メッセージはすべて JSON で、種類を t に入れる。サーバーが見るのは t と送り手の席だけで、行き先を決めたら中身には触らない。
| 種類(t) | 向き | 中身 | 頻度 |
|---|---|---|---|
start | ホスト → ゲスト全員 | ステージ番号・シード・HP・人数・空き席 | レースごとに 1 回 |
snap | ホスト → ゲスト全員 | d の下に写し(人・敵・矢・弾・開いた部屋・その間の出来事) | 3 tick ごと(20 回/秒)と、決着の瞬間 |
back | ホスト → ゲスト全員 | 全員をロビーへ戻す | レースのあと |
in | ゲスト → ホスト | 押しっぱなしの向きと撃つ、1 回きりの合図、タップの行き先。サーバーが席番号 s を付ける | 変わったときと 1 回きりの合図。押しっぱなしの念押しに 1 秒ごと |
chat | 誰でも → 自分以外の全員 | 文字 m か定型文のコード q。サーバーが s を付ける | 1 人 1 秒に 1 通まで |
joined / lobby / left / closed | サーバー → 本人か全員 | 自分の席・埋まっている席・抜けた席・ホストが抜けて閉じた | 出入りのたび |
in の中は、押しっぱなし・1 回きり・行き先の 3 つに分けてある。ホストはこの 8 つの欄を読むだけで、相手が 4 つの操作のどれで遊んでいるのかを知らない。
| 欄 | 種類 | 中身 | いつ送るか |
|---|---|---|---|
h | 押しっぱなし | 押している向き(無ければ null) | 変わったとき・1 秒ごとの念押し |
f | 押しっぱなし | 撃つボタンを押しているか | 同上 |
tp | 1 回きり | 十字を 1 回押した向き | 押した 1 回だけ |
sw | 1 回きり | スワイプした向き | スワイプした 1 回だけ |
st | 1 回きり | タップで撃った | タップした 1 回だけ |
ta | 行き先 | タップしたマス [x, y] | タップした 1 回だけ |
tr | 行き先 | 全体マップでタップした部屋の番号(完全自動) | タップした 1 回だけ |
tw | 設定 | タップの歩き方(straight / full) | 行き先を送るときだけ。既定(そのまま)は省く |
接続を断るときは WebSocket の close コードで理由を返す。端末は理由ごとに文言を変える。
| コード | 理由 | 端末の扱い |
|---|---|---|
4009 | 作ろうとした部屋コードがもう使われている | ホストが別のコードで作り直す(5 回まで)。人には見せない |
4004 | その部屋コードの部屋が無い | 「見つからない」 |
4010 | もう始まっている | 途中からは入れない |
4011 | 満員(対戦 2 席・協力 4 席) | 「満員」 |
4503 | 部屋数が上限(200) | 区別していない。通信の失敗と同じ表示 |
4400 | 部屋コードか遊び方の形が不正 | アプリからは起きない |
日付はその判断が入ったコミットの日。新しいものが上。
一発アウトはそのままに、やられてよい回数を 3 回にした。3 回目でゲームオーバーになり、その面はやり直し。やられるたびに3 秒数えてから動き出す(その間は敵にも罠にも当たらない)。
これまでは何回やられても続けられたので、失敗に重みが無かった。部屋の入口に戻されるだけなら、運が悪ければ押し続けるだけで進めてしまう。
すぐ動き出す作りも問題だった。やられた瞬間は何が起きたのか見えないまま再開するので、同じ敵にもう一度突っ込むことがあった。3 秒あれば、敵の位置と自分の戻り先を見てから動ける。
上の帯に残り回数(ハート 3 つ)を出し、やられたときは「アウト! / あと n 回 / 3・2・1」を盤面の真ん中に出す。
CPU の走りを測るテストは、残機を 999 にして回すようにした。既定のままだと残機切れで止まり、道さがしの質(クリア率・強さの順)が測れなくなる——実際、最初は Lv4 のクリア率が 95% を割ってテストが落ちた。
10 階ぶんの面の札を全部並べるのをやめ、中を開くのは 1 階だけにした。ほかの階は薄い帯にして、進み具合の升目(10 個)だけ出す。左端に階段の線を通し、上に地面の線を引いてビルの断面に見せている。あわせて、操作の選択を世界地図より前に移した。
「階層が縦長すぎて、操作ボタンが下にあるのが気付けない」と言われた。測ったら 10 階ぶんで 5,000px を超えていて、その下に置いたものは存在しないのと同じだった。
階をたたんでも困らない。いま遊ぶのは 1 つの階で、ほかの階は「どこまで進んだか」が分かれば足りる。升目ならひと目で分かる。
地図の高さは 1/4 ほどになり、操作の選択が最初の画面に入るようになった。ゲーム中のポーズ画面でも変えられる(v0.5.2)ので、入口は 2 つになった。
作るときに 2 回落とし穴を踏んだ: 階段の線を `Row` の stretch で作ったら高さが無限になって落ち(地図は縦に伸びるリストの中にある)、升目 10 個は幅の狭い画面ではみ出した。線は左の border に、升目は縮む箱に入れた。
壊せない壁に弾が当たったとき、当たった面で光らせ、火花を来た方向へ跳ね返す(0.26 秒)。壁の中心ではなく手前の面で光らせる。
これまでは弾が黙って消えていた。壊せる壁はひびが入るので手ごたえがあるが、壊せない壁は「当たったのかどうか」も分からなかった。弾かれる絵があれば、撃っても無駄だと 1 発で伝わる。
壁の中心で光らせると、壁が壊れたように見えてしまう。だから手前の面で光らせる。
進行には関わらない(出来事を 1 つ増やして画面側で描くだけ)。偶然を待たずに確かめられるよう、スプライト一覧に 3 コマ描き出すようにした。
スワイプ操作を「なぞって押さえている間だけ、その向きへ歩く」に変えた。指を離したら止まる。これまでは、スワイプした向きへ分かれ道まで自動で走り続けていた。
あわせて、十字ボタンと撃つボタンの当たり判定を絵の外まで広げ(各 22px)、ボタン自体も少し大きくした。
「押してないのに勝手に移動することがある」と言われた。実際そのとおりで、指を離したあとも分かれ道まで走る作りだった。速く動けるが、止めたいところで止まらない。迷路は一発アウトなので、止まれないことの損のほうが大きい。
十字ボタンは「押せる範囲が狭い」。絵の大きさ=触れる大きさにしていたが、指はふちを外す。絵より外側にも効く余白を付けた(見た目は変えずに触れる範囲だけ広げる)。
自動走行のしくみ(runDir)は core に残っているが、画面からは送っていない。テストで「指を離したら、それ以上進まない」ことを見るようにした。
横向きの画面では、ボタンが大きくなったぶん定型文が 1 つ入りきらなくなった(テストが見つけた)。横向きだけボタンを少し小さくして戻した。
ベルトコンベアの逆走を、乗ること自体を止める形から乗れるが押し戻される形に変えた。下流から 1 マス目までは進めて、そこで流れに押し返される。ベルトの上で流れと逆を押し続けても、流れが勝つ。
「1 マス目までは進めて、そのあと押し戻されるように」と言われた。止めてしまうと見えない壁になり、なぜ動かないのかが分からない。押し戻されるなら、理由が目で分かる。
道さがし(CPU・タップの行き先・順位の距離)のほうは今までどおり「逆走は通れない」と数える。乗れても先へ進めないので、道としては変わらない。
ポーズ画面に操作の選択を置いた。閉じるとその場で切り替わる。
操作は 4 つあるが、どれが手に合うかは遊んでみないと分からない。始める前の画面まで戻らないと変えられないと、試す気になりにくい。
切り替えたときは押しっぱなしと行き先を捨てる(十字を押したまま変えて、歩き続けたままになるのを防ぐ)。ポーズ画面の札は地の色を敷いた——透かしたら、後ろの盤面が札の中に透けて読めなかった。
タップ操作の下にあった「タップの歩き方(おまかせ / そのまま)」という設定をやめ、タップは「そのまま」だけにした。歩き方はタップ(指した方へまっすぐ)とタップ完全自動(面全体で道を探す)の 2 つになり、設定の段が 1 つ消えた。
操作そのものは 4 つ(タップ・タップ完全自動・スワイプ・十字ボタン)のまま。
「おまかせ」は部屋の中で最短の道を探し、トゲと矢が通り過ぎるのを待ち、敵や落とし穴の手前で止まっていた。プレイヤーがやることが「どこへ行くか指す」だけになり、完全自動とほとんど区別がつかなかった。
同じことをする選択肢が 2 つ並んでいると、選ぶ手間だけが増える。「自分で操る」と「ぜんぶ任せる」の 2 つに割り切るほうが、何が違うか一言で言える。
設定画面から 1 段まるごと消え、歩き方を表す型(TapWalk)も 3 つから 2 つになった。「おまかせ」を前提に書いていたテスト 3 本は、まっすぐ歩く前提に書き直した。
オンラインでは歩き方を相手に送っているので、古い版が送ってくる「おまかせ」は「そのまま」に読み替える(知らない名前で落ちない)。
スワイプ操作のときに画面の下半分に出している案内を、薄めるのをやめて枠で囲った。中身(手の絵と「スワイプで進む」「タップで撃つ」)は変えていない。
「スワイプを選んだのに、スワイプのボタンが出てこない」と言われた。実際には案内は出ていたが、文字と絵を透明度 0.55 で描いていて、コントラスト比が 2.7 しかなかった(本文の下限は 4.5)。暗い地の上では、何も無い黒い帯に見えていた。
ボタンを足す案は採らなかった。スワイプは「画面のどこでも指 1 本」が売りで、押す場所を作ると意味が変わる。以前、4 方向の矢印と丸を描いたときも「押す場所に見える」と言われている。
枠なら「この中ぜんたいが操作の面」という意味になり、押す場所には見えない。
案内の色は薄めない決まりにして、テストで色と枠の有無を見るようにした(また薄めたら落ちる)。十字ボタンのほうは出ていることも動くことも確かめた。
nukero に揃え、旧 URL は捨てるApp Store に登録する Bundle ID と SKU を jp.nzw.nukero にした。それに合わせて Android の applicationId と、nzw.jp の配信フォルダも /static/app/nukero/ に移した。旧 /static/app/mazerush/ からのリダイレクトは置かず、消した。
Dart のパッケージ名(package:mazerush/…)だけは旧名のまま残した。
開発中の名前(Maze Rush)から NUKERO! に変えたあと、識別子だけが旧名のまま残っていた。Bundle ID はあとから変えられないので、ストアに出す前——つまり今——でなければ直せない。
旧 URL を残さなかったのは、β を公開したのが同じ日(2026-09-23)で、外に出回っている期間が数時間しかなかったため。リダイレクトを足すと nginx の設定が 1 つ増え、消す理由を覚えている人がいなくなる。
Dart のパッケージ名を変えなかったのは、全ファイルの import に響くのに、外からは一切見えないため。変える理由が「気持ち悪い」しかないものは変えない。
サイト側で旧パスを指していたのは 3 ファイル 5 か所だけだった(開発ノート・アプリ一覧・Web 版の <base href>)。Web 版は --base-href を変えて作り直した。
同じ日に Android の APK もβ配布に足した。署名は他のアプリと共通のリリース鍵で、apksigner で v3 署名と CN=nzw を確認してから置いた。鍵が無いときは release ビルドを止めるようにしてある——debug 鍵で配り始めると、あとで共通鍵に移る版が上書き更新できなくなり、利用者にアンインストールしてもらうしかなくなるため。
iOS は IPA(22.0MB)まで作れることを確かめた。App Store の Capabilities はひとつも有効にしていない——マイクと音声認識は Info.plist の説明文で足り、オンライン対戦は素の WebSocket なので、entitlement を要求するものが無い。
ストアを待たずに、Web 版だけを β として /static/app/nukero/web/ に置いた。
URL を開けば部屋が作れる。相手には 4 文字の部屋コードを渡すだけで、インストールもアカウントも要らない。
ストア版(iOS / Android)は 1 本も出していない。リリース日は決めずに、β で通信の壊れ方を拾ってから出す。
オンラインの作りは、手元の 2 台では確かめきれない。同じ家の回線・同じ機械に立てた 2 つのブラウザでは、片道 10 ms でパケットの落ちない世界しか見ていない。
携帯回線・別の国・NAT の内側・端末を裏に回したときは、人に遊んでもらわないと出てこない。インストールが要ると、その人数が集まらない。
ゲームの中身は、一発アウトと片手の見直しで形が決まった。残って未検証なのは通信だけなので、そこだけを試す出し方にする。
2026-09-23 に公開。本番の中継へ実際につないで、ping/pong の往復が中央値 10.5 ms、in が 11.9 ms、snap が 11.4 ms で通ることを確かめた(下の「実測」)。
分かっていて残した穴は 3 つ。ゲストの回線が一瞬切れると始まった部屋には戻れない(4010)、ホストの端末が裏に回ると部屋ごと止まる、API を再起動すると遊んでいる部屋が全部消える。
記録や順位を残す仕組みは入れない。β の間は、部屋を作って遊ぶところまで。
押しっぱなしの向き(h)と撃つ(f)は、変わったときだけ送る。
1 回きりの合図(十字の 1 押し tp、スワイプ sw、タップで撃つ st)は、それぞれ別の欄で、その 1 回だけ送る。
タップで歩く操作は、行き先のマス ta: [x, y] をそのまま送る。道を探すのはホストの仕事。
片手で遊ぶ形に見直して、操作がスワイプとタップになった。どちらも「1 回きりの合図」で、押しっぱなしとは性質が違う。
1 回きりの合図を f の 1 → 0 で表すと、2 通が同じフレームに届いたときにホストは「押して離した」を 1 tick の中で見ることになり、撃たずに終わる。欄を分ければ、同じフレームに 2 通届いても両方が 1 回ずつ効く。
行き先をそのまま送るのは、ゲスト側で道順を十字の連打に直すと、壁が壊れて迷路が変わった瞬間にホストと別の道を歩くため。ホストの GameState は崩れた壁を知っている。
1 通は 34 バイト(向きだけ)から 63 バイト(行き先と歩き方つき)。サーバーが席番号を足して 40〜69 バイト。
ゲストは操作のしかたを自由に選べる。2026-09-23 の時点で 4 つ(タップ・タップ完全自動・スワイプ・十字ボタン。既定はタップ)あるが、ホストはこの 8 つの欄を読むだけで、どれで遊んでいるのかを知らない。
操作を増やしても、ホストの進行のコードは 1 本のまま(core/)。1 台で遊ぶときと同じ入力の形に入る。
タップ完全自動では、全体マップの部屋をタップすると行き先になる。送るのは部屋の番号 1 つ(tr)で、そこまでの道はホストが探す。
タップの歩き方(そのまま/完全自動)はゲストごとの設定なので、行き先と一緒に tw で送る。既定の「そのまま」は欄ごと省く(v0.5.1 で「おまかせ」をやめた。下の意思決定ログ)。
行き先が部屋なら、道さがしは面全体に広がる。ゲスト側で解いて十字の連打に直すと通数も増えるし、壁が崩れて迷路が変わった瞬間にホストと別の道を歩く。
歩き方を写しや start に入れなかったのは、席ごとに違う設定だから。ホストは全員ぶんの進行を回すが、どう歩かせるかは各ゲストの手元の設定で決まる。入力に相乗りさせれば、途中で切り替えてもそのまま効く。
部屋をタップして 1 通 54 バイト(中継後 60 バイト)。部屋をまたぐ長い移動でも、送るのはこの 1 通だけ。
ホストが読む欄は 8 つになったが、操作のしかたを 4 つに増やしても写しの形は変わっていない。
写しに gl(いまのゴールのマスの番号)を足した。ゲストは生成した迷路の stage.goal ではなく、届いた gl を見る。
敵のしびれ(撃たれて止まっている残り)も足した。
9・10 面のゴールは、泥棒が星を持って逃げる。迷路はシードから各端末で作れるが、生成した時点のゴールは泥棒が最初にいた場所で、走り出したあとは合わない。
ゲストが stage.goal を見ていると、全体マップの矢印も順位の距離も、誰もいない場所を指す。
マスの番号は整数 1 つで、写し全体で 5〜9 バイト。差分にする理由がない。
泥棒の面(10、協力 4 人)の写しは中央値 606 バイト・最大 726 バイト。敵の多い面 8(649 / 782 バイト)より小さい。
「シードから作れるものは送らない」の例外は、いまのところゴールと壁の傷だけ。どちらも走っている間に変わる。
敵の巡回の決め方と見つける距離 8 は BOM MAN ARENA から、迷路の掘り方(深さ優先の穴掘り法)とゴールからの距離場は Algo Derby から持ってくる。
そのかわり乱数は作り直す。Dart の Random(seed) をやめ、xorshift32 を自分で書いた。DateTime も使わない。
敵の動きも迷路も、既に 2 度作って形が分かっている。3 度目に新しく考える理由が無い。
乱数だけは引き継げなかった。Random(seed) が Dart VM と dart2js で同じ列を返す保証は無く、同じ端末でしか同じ迷路にならない。掛け算を使う実装も、Web では桁が落ちる。
Algo Derby はそもそもシードを持たず、同じレースが二度と再現できない作りだった。1 人で遊ぶぶんにはそれで面白いが、迷路を送らずシードだけ配る形(下の決定)はその上に建てられない。
レースを進めるのはホストの端末だけ。入力を当て、敵・弾・当たり判定・CPU を動かす。
サーバーは部屋と席を管理し、メッセージの種類と送り手の席を見て行き先を決めるだけ。ホストのメッセージは受け取った文字列のまま配る。
ホストが抜けたら部屋を閉じる。
進行のコードは core/ に 1 本だけある。サーバーを正にすると、同じ進行を Python にもう 1 本書き、2 本を一致させ続けることになる。
ホストの端末なら、1 台で遊ぶときと同じコードがそのまま動く。
サーバーは既にある nzw.jp の API に同居させた。nginx は既存の /v1/ws/ の設定(Upgrade の中継と、待ち上限 3600 秒)のままで通った。
レース開始の start で送るのは、ステージ番号・シード・HP・人数・空き席だけ。迷路・敵の初期配置・仕掛けは、各端末が同じシードから作る。
そのために core/ では次を守る。
DateTime とシードなしの Random() を使わない迷路を送らなければ、写しには動くものだけが残る。
同じ設定・シード・入力なら同じ結果になる性質は、テストの再現にも要る(迷路の生成を 8 面 × 1000 シードで回している)。
1 フレーム = 1/60 秒にしないのは、Synapse Link がそうしていて、120Hz の端末で 2 倍速になっていたため。
写しは 1 回 195〜663 バイトに収まった。
その代わり、ゲストの端末も迷路全体(ゴールの位置を含む)を持っている。霧(誰かが入った部屋だけを見せる)は描画の側で隠しているだけ。
ホストは 3 tick ごと(20 回/秒)に、次のものを 1 通にして送る。前回との差分にはしない。
行はキー付きのオブジェクトではなく、数の並びにした。
ゲストは届いた写しで状態を丸ごと上書きする。何通か固まって届いたら、次の tick で順に全部を当てる。
毎回全部を送れば、1 通が遅れても次の 1 通で元に戻り、ずれが溜まらない。前の写しを受け取れていたかを気にしなくてよい。
固まって届いた写しを最後の 1 通だけにせず全部当てるのは、途中の写しに載っている出来事(効果音)を落とさないため。
t(tick の番号)が種類を上書きし、サーバーに全部捨てられた(下の「踏んだこと」)。ゲストは毎 tick 入力を見るが、送るのは次のときだけ。1 通は 34 バイトほど。
写しと写しの間は、移動の補間と罠の周期だけを手元で進める。最後の写しから 4 tick を過ぎたら、次が届くまで進めない。
自分の操作を先に動かして見せる予測はしていない。
毎 tick 送ると 1 人 1 秒 60 通になる。サーバーの上限は 1 人 1 秒 90 通で、超えた分は黙って捨てられる。
見た目を 4 tick で止めるのは、ホストより先に時計が進むと、トゲの出入りがホストの画面と食い違って見えるため。
ゲストが押してから自分の画面で動くまでは、開発機からの実測と tick の区切りから見積もって約 21〜104 ms(下の「実測」)。
携帯回線では測っていない。
部屋はホストの端末が決める 4 文字のコードで作る。使う文字は、見間違えやすい O/0・I/1・S/5・B/8・Z/2 を抜いた 26 文字(約 46 万通り)。
席はサーバーが割り当て、ゲストの in と全員の chat にはサーバーが席番号を付け直す。
上限は次のとおり。
4503・4011)4010)中継そのものは 3 か月前のグループチャットから持ってきた(部屋ごとに接続を持って自分以外へ配る骨格と、断る理由を close コードで返す作法)。ただしログインもデータベースも引き継がなかった。あちらは会話を残すために両方が要るが、こちらは残すものが無い。
アカウントが無いので、荒らしへの備えは数の上限で行う。
部屋コードは人が読んで打ち込むので、見間違えやすい文字を最初から使わない。グループチャットの部屋番号はサーバーが 6 文字で採番していたが、こちらは端末が 4 文字で作って、ぶつかったら引き直す形にした。
マイクのボタン(V キー)を押している間だけ聞き、離したら OS・ブラウザの音声認識で文字にして送る。
定型文 6 つはコード(q)だけを送り、受け取った側が自分の言語で出す。
1 通 40 文字・1 秒に 1 通の上限は、端末とサーバーの両方に同じ値で置いた。1 台で遊ぶときは出さない。
str[:40] と同じくコードポイントで数える(Dart の length は UTF-16 の単位なので使わない)。写しの大きさは、core/ を CPU(Lv4)で回し、実際に送る形({"t":"snap","d":写し} の JSON 全体)にして数えた。ステージごとに 5 シード、ゴールか 90 秒まで。
| ステージ | 敵の数 | 対戦 2 人(中央値 / 最大) | 協力 4 人(中央値 / 最大) |
|---|---|---|---|
| 1 | 0 | 204 / 244 B | 287 / 321 B |
| 4 | 3 | 335 / 396 B | 427 / 517 B |
| 8 | 8 | 555 / 653 B | 649 / 782 B |
| 9(泥棒) | 5 | 425 / 502 B | 518 / 642 B |
| 10(泥棒) | 7 | 514 / 606 B | 606 / 726 B |
2026-09-23 のβ版のコードで計測。大きさは人と敵の数でほぼ決まる(1 人・1 体あたり約 40〜45 バイト)。前日(9-22)の計測より面 8 で 67 バイト増えたが、足した欄(gl が 9 バイト、敵 1 体につきしびれの 1 つ)は 30 バイトほどで、残りは面ごとの敵の数を見直した分(面 8 は 7 体 → 8 体)。
ゲストが送る in は、欄を分けたあとも 70 バイトに収まる。
| 送るとき | 足す欄 | 大きさ(サーバーが席を足したあと) |
|---|---|---|
| 向きを押した・離した | なし | 34 B(40 B) |
| スワイプ 1 回 | "sw":"r" | 44 B(50 B) |
| タップで撃つ | "st":1 | 42 B(48 B) |
| タップで行き先を指す | "ta":[12,7] | 47 B(53 B) |
| タップで行き先を指す(そのまま) | "ta":[12,7]+"tw":"straight" | 63 B(69 B) |
| 全体マップの部屋をタップ(完全自動) | "tr":5+"tw":"full" | 54 B(60 B) |
遅れは、開発機から本番の中継(wss://nzw.jp)へホストとゲストを同じ機械からつなぎ、in と snap を 40 回ずつ通して測った。同じ機械の時計で測れるので、片道(端末→サーバー→端末)がそのまま出る。
| ゲストが押してから自分の画面で動くまで | 待つもの | 値 |
|---|---|---|
| 押してから送るまで | ゲストの次の tick | 0〜17 ms |
| ゲスト → サーバー → ホスト | in(34 バイト) | 中央値 11.9 ms(p95 61.1 ms) |
| ホストが写しに載せるまで | 次の tick で入力を当て、3 tick ごとの写しに載る | 0〜50 ms |
| ホスト → サーバー → ゲスト | snap(約 650 バイトで計測) | 中央値 11.4 ms(p95 60.4 ms) |
| 届いてから映すまで | ゲストの次の tick | 0〜17 ms |
| 合計 | 約 23〜107 ms。ホスト自身は 0 |
2026-09-23 の計測。ホストとサーバーの往復(ping / pong)は中央値 10.5 ms、p95 68.2 ms。chat(定型文をゲストからホストへ)は中央値 13.6 ms、p95 65.4 ms(1 秒に 1 通までなので 20 回)。携帯回線・海外からは測っていない。
中央値は前日とほとんど変わらないが、p95 は 11〜12 ms から 60 ms へ開いた(同じ開発機・同じ回線、時間帯だけが違う)。20 回に 1 回は写しの間隔(50 ms)より回線の揺れが大きい、ということになる。β で人の回線を見たいのはここ。
中央値で見るかぎり、幅がいちばん大きいのは回線ではなく写しの間隔(3 tick = 50 ms)の待ちだった。写しを 1 秒 60 回にすれば縮むが、ホストの上りとサーバーの複製も 3 倍になる。
ただし p95 まで見ると回線の方が大きくなる日がある。どちらを削るかは、β で人の回線の数字を見てから決める。
| 壊れ方 | 原因 | 今の形 |
|---|---|---|
| 写しがゲストに届かない | 写しを {"t":"snap", ...写し} と種類と同じ階層に広げた。写しにも t(tick の番号)があり、種類を上書きした。サーバーは「ホストが送る種類ではない」として捨てた | 写しは d の下に包む。「写しには t がある」ことをテストで固定した |
| release の APK でオンラインにつながらない | Android の雛形は INTERNET を debug と profile にしか書いておらず、使っているプラグインも足さない | src/main に INTERNET を書く。マイクの権限と音声認識の問い合わせ先も自分で書く |
| メッセージが 4 秒遅れて届くように見えた | Playwright で 1 つのブラウザに 2 ページを開くと、後ろのページが 1〜3fps に落ち、キー 1 回に数秒かかっていた。通信ではなく試し方の問題 | 2 人ぶんはブラウザを 2 つ起動して試す。Web は開始直後の数秒が重いので、10 秒ほど待ってから測る |
| 撃ったのに撃っていないことがある(作り直す前の形) | スワイプとタップの 1 回きりの合図を、押しっぱなしの f の 1 → 0 で表していた。2 通が同じフレームに届くと、ホストは「押して離した」を 1 tick の中で見て撃たない | 1 回きりの合図は別の欄(sw / st)で送る。欄が別なら同じフレームでも両方が 1 回ずつ効く |
| ゲストのゴールの矢印が、誰もいない場所を指す | ゴールを持って逃げる泥棒の面で、ゲストが生成した迷路の stage.goal(泥棒が最初にいた場所)を見ていた | いまのゴールを写しの gl で送る。進行・描画・順位の距離はどれも GameState.goal を見る |
| Web で 2 回目から話せない | 音声認識は stop() のあと終わりの知らせが届くまで、次の start() が InvalidStateError で失敗する。知らせが来ないこともある | 聞き始める前に abort して 300 ms 待つ(abort なら 50 ms ほどで知らせが来た)。始まらなくても 3 回続くまでは「使えない」扱いにしない |
ストア版のリリース日を未定にする(10 月の目標を外した)/β で通信の壊れ方を拾い切るまで、日付を置かない
iOS / Android のストア版(どちらも未リリース)
やられるのは 3 回まで(3 秒数えて再開・3 回目でゲームオーバー)/世界地図をビルの断面に/壊せない壁で弾かれる火花
スワイプは押さえている間だけ進む(離すと止まる)/十字ボタンの当たり判定を広げる/ポーズ画面で操作を変えられる/ベルトは 1 マス乗れて押し戻される
タップの「おまかせ」をやめて 2 種類に/スワイプの操作面を枠で囲って見えるように/Android の APK を v0.5.1 に
Web 版をβとして公開/Android の APK も配布(Bundle ID と配信先を nukero に統一)/地下 10 階 × 10 面の全 100 面と世界地図/入力を押しっぱなし・1 回きり・行き先の 3 つに分ける(sw・st・ta・tr・tw)/動くゴールを写しに載せる(gl)/操作を 4 つにして既定をタップに/見た目をファミコン風の 8bit へ
名前を NUKERO! に/オンラインで声と定型文のメッセージ/中継を本番の API に載せ、release の APK で INTERNET が抜けていたのを直す
4 つの遊び方/オンラインはホストが進め、nzw.jp が受け渡すだけ/core/ は純 Dart で決定的(テスト 58 本)
β で見たいのは、ほとんどが通信のこと。ゲームの中身より先に、これが分からないとストアに出せない。
in と snap がどれだけ遅れて届くか。開発機からの p95 が 60 ms まで開く日がある以上、写しの間隔(50 ms)を詰める前にこちらを知りたい。4010)。ホストが切れると部屋ごと終わる。β で実際に起きる頻度を見てから、入り直しを作るか決める。4503)も、いまは通信の失敗と同じ表示になる。未決事項:迷路とほかの人が入った部屋を全端末が持っているので、改造した端末ならゴールが見える。いまは記録や順位を残す仕組みが無い。