公開: 2026年10月25日 ・ 筆者: Toshi(開発者)
フレンド対戦の招待にはQRコードを使います。バズ走は「外部ライブラリを使わない」方針なので、QRコードの生成器も自分で書きました。仕様自体は公開されていますが、実装してみると誤り訂正まわりで2回、原因の分かりにくいバグに遭遇しました。この記事はその記録です。
| 項目 | 対応範囲 |
|---|---|
| モード | バイトモードのみ(URLを入れるだけなので数字・英数字モードは不要) |
| 誤り訂正レベル | L のみ(画面に表示して至近距離で読むので最低レベルで十分) |
| 型番 | 1〜10(招待URL程度の長さなら型3〜4に収まる) |
| マスク | パターン0に固定(画面に大きく表示して至近距離で読む用途なので、8種類を評価して選ぶ処理は省略) |
| サイズ | JavaScript 約7KB。Canvasに描画 |
対応範囲を絞ったことで、実装は数百行で済みました。汎用ライブラリの大部分は「自分が使わないモードや型番」のためのコードなので、用途が決まっているなら自前で書く方がむしろ小さくなります。
QRコードの誤り訂正は、リード・ソロモン符号という方式で、データの後ろに「訂正用の符号」を付け足します。この計算は通常の整数ではなく、256個の要素しかないガロア体という世界で行います。実装では、原始多項式 0x11d を使って指数表 EXP と対数表 LOG を先に作っておき、掛け算を「対数を足して指数表を引く」に置き換えます。
let x = 1;
for (let i = 0; i < 255; i++) {
EXP[i] = x; LOG[x] = i;
x <<= 1;
if (x & 0x100) x ^= 0x11d;
}
const mul = (a, b) => (a === 0 || b === 0) ? 0 : EXP[LOG[a] + LOG[b]];
指数表を512要素にして後半に前半をコピーしておくと、LOG[a] + LOG[b] が255を超えても剰余を取らずに済みます。小さな工夫ですが、1コードに何千回も呼ぶ関数なので効きます。
訂正符号を作るには、まず「生成多項式」を組み立てます。これは (x − α⁰)(x − α¹)…(x − αⁿ⁻¹) を展開したもので、1つずつ因子を掛けていきます。最初の実装では、掛け算のときに次数の高い項と低い項の扱いを逆にしていました。配列の添字0を「最高次」と見るか「最低次」と見るかの取り違えです。
厄介なのは、この状態でもQRコードは「それらしく」表示されることです。模様は出るし、サイズも正しい。読み取り機がエラー訂正に失敗して無言で読めないだけなので、どこが悪いのか画面を見ても分かりません。既知のテストベクタ(データ列と正しい訂正符号の組)を用意し、1バイトずつ比較してようやく気づきました。
QRコードには、誤り訂正レベルとマスク番号を記録した15ビットの「フォーマット情報」が、左上の位置検出パターンの周囲に縦と横の2か所に書き込まれます。片方は横方向に、もう片方は縦方向に、しかも途中で向きが折り返す配置です。
ここで、縦と横のビット順を取り違えました。データ部分は完全に正しいのに、読み取り機がフォーマット情報を読んだ時点で「どのマスクで、どの訂正レベルか」を誤認し、結果としてデータを復号できない。マスクを1種類に固定していても、この15ビットが正しくなければ読めません。これもやはり、画面上は正常に見えます。
2つのバグに共通するのは「目で見ても分からない」ことです。そこで、生成したQRコードを画像に書き出し、macOSの画像認識API(Vision)でデコードして元のURLと一致するかを自動で確認する仕組みを作りました。手順はこうです。
iPhoneのカメラとまったく同じ実装ではありませんが、Appleのデコーダで読めるならiPhoneでもまず読めます。修正のたびにこの検証を回すことで、「直したつもりで別の型番が壊れた」という手戻りを防げました。
招待URLの長さに合わせて最小の型番を選べるので、QRコードが常に大きなモジュールで描かれ、至近距離で素早く読めます。また依存ライブラリがゼロなので、ゲーム本体と同じく「HTMLが届いた瞬間に動く」性質を保てました。何より、リード・ソロモン符号を実際に手で書いたことで、誤り訂正が「なぜ多少汚れても読めるのか」を体で理解できました。
技術メモの一覧へ戻る:技術メモ
⚽ いますぐ走る