バックエンドなしで「毎日更新パズルゲーム」を作った話(450問自動生成・PWA対応・テスト2,600件)
作ったもの
定時退社タンゴ(https://teiji-tango.com)
👔と🍺を6×6のグリッドに並べる、毎日更新の論理パズルゲームです。
ルールはシンプルです。
- 各行・各列に👔と🍺がちょうど3個ずつ
- 同じ記号が3連続してはいけない
- セル間の「=」「×」制約を満たす
「定時退社できた!」という達成感をゲームにしたくて作りました。残業も飲み会も、3連続したらNG。
なぜ作ったか
LinkedInが公開している「Tango」というパズルゲームがあります。毎日1問、論理パズルを解くゲームで、Wordleと同じ「今日の問題」フォーマットです。
これが面白くて毎日やっていたんですが、ある日ふと思いました。
「👔と🍺に置き換えたら、完全に日本のサラリーマンゲームになるのでは」
3連残業も3連飲み会もNG。バランスよく定時退社する。ゲームのルールがそのままメッセージになる。
作るしかないと思いました。
技術構成
フロントエンド: 素のHTML + CSS + JavaScript(フレームワークなし)
バックエンド: なし
データベース: なし(localStorage のみ)
ホスティング: GitHub Pages(無料)
ビルドツール: なし(静的ファイルそのままデプロイ)
意図的にバックエンドを作りませんでした。
毎日更新のパズルは、日付から決定論的に問題を選べます。(日付のオフセット) mod 30 でプールから選ぶだけ。サーバーは不要です。ユーザーの進捗はlocalStorageに保存します。
Wordleも最初はサーバーなしの静的サイトでした。
一番大変だったこと:パズル自動生成
450問以上のパズルを作る必要がありました(5モード × 3難易度 × 30問)。手動では無理なので自動生成スクリプトを作りました。
ただし、パズルには厳しい条件があります。
- 唯一解であること(答えが1つだけ)
- 演繹のみで解けること(仮置きが不要)
- 難易度が正確であること(初級は初級の手筋だけで解ける)
この3つを満たすパズルを自動生成するのが、思ったより難しかったです。
ソルバーの実装
パズルが「演繹のみで解けるか」を判定するために、9種類の解法パターンを実装しました。
// ① バランス完了:行/列に3個揃ったら残りは全て逆
// [👔 🍺 _ _ 👔 👔] → 👔が3個 → [👔 🍺 🍺 🍺 👔 👔]
// ② ダブルブロック:2連続の前後は逆
// [👔 👔 _ ...] → [👔 👔 🍺 ...]
// ③ サンドイッチ:同じ記号に挟まれた中間は逆
// [👔 _ 👔] → [👔 🍺 👔]
// ④ 制約直接:= や × の片側が確定したらもう片方も確定
// ⑤ 端点バランス:端の2連続から反対端を確定
// [👔 👔 _ _ _ _] → 反対端は必ず🍺(背理法で証明)
これらのパターンを繰り返し適用して、変化がなくなったときに全マスが埋まっていれば「演繹のみで解けるパズル」と判定します。
「端点バランス(⑤)」は特に面白いパターンです。
[👔 👔 _ _ _ _]
「もし反対端が👔なら:👔は3個(位置0,1,5)」
「バランスより残り3セル(位置2,3,4)は全て🍺」
「位置2はダブルブロックで既に🍺確定」
「→ 位置2,3,4が🍺🍺🍺 = 3連続違反」
「矛盾 → 反対端は🍺に確定」
単純なパターンマッチングではなく、2段階の背理法推論です。これを知らないと「論理的に解けない」と感じてしまうパズルが、実は演繹で解けます。
生成アルゴリズム
1. ランダムな完成盤面を生成(バックトラッキング)
2. マスを1つずつ消していく
3. 消したあとにソルバーを走らせる
4. まだ唯一解かつ演繹で解けるなら、次のマスを消す
5. 解けなくなったら1つ前の状態が最終パズル
6. 使った手筋のセットで難易度を自動判定
このプロセスを1パズルあたり数十〜数百回試行して、条件を満たすものだけを採用しています。
5種類のゲームモード
通常モード以外に4種類追加しました。
壁ありモード 🧱
グリッドに壁を設置し、壁をまたいで3連続は無効になります。空間の分断がパズルの複雑さを増します。
エリアモード 🗂️
6×6グリッドを6つのエリアに分割。各エリアにも3個ずつのルールが加わります。
Xタンゴモード ✕
両対角線にも同じルールが適用されます。9方向(行×6、列×6、対角線×2)全てを同時に満たす必要があります。
キラータンゴモード 🔪
Killer Sudokuのように、セルがケージ(グループ)に分割されます。各ケージ内の🍺の数が表示され、それを満たす必要があります。初期配置(ヒント)がゼロで、全マスが空白からスタートします。
シェア機能:Wordleと同じ仕組み
クリア後に絵文字グリッドをシェアできます。
定時退社タンゴ Day 4(通常・初級)
⏱ 1:47 | ヒント0回
👔🍺👔🍺👔🍺
🍺👔🍺👔🍺👔
👔🍺👔🍺👔🍺
🍺👔🍺👔🍺👔
👔🍺👔🍺👔🍺
🍺👔🍺👔🍺👔
#定時退社タンゴ
https://teiji-tango.com
Wordleが爆発的に広がったのは、この絵文字グリッドがタイムラインに流れて「なんだこれ?」と気になって踏むからです。同じ仕組みを最初から実装しました。
PWA対応
manifest.json と Service Worker を実装し、ホーム画面に追加するとネイティブアプリのように動きます。オフラインでもプレイ可能です。
// sw.js: キャッシュ戦略
const CACHE_NAME = 'teiji-tango-v19';
const ASSETS = ['/', '/index.html', '/style.css', '/js/app.js', ...];
self.addEventListener('install', e => {
e.waitUntil(caches.open(CACHE_NAME).then(c => c.addAll(ASSETS)));
});
テストについて
ゲームロジックが複雑なため、テストを重視しました。
tests/
├── unit/
│ ├── game.test.js # TangoGame ユニットテスト
│ ├── solver.test.js # TangoSolver ユニットテスト
│ └── puzzles.test.js # パズルデータ整合性テスト(全450問)
└── integration/
└── puzzle-flow.test.js # 実績・ゲームフロー統合テスト
パズルデータの整合性テスト(唯一解チェック)が全450問に対して走るので、テスト数は2,600件を超えています。
$ npm test
PASS tests/unit/game.test.js
PASS tests/unit/solver.test.js
PASS tests/unit/puzzles.test.js
PASS tests/integration/puzzle-flow.test.js
Test Suites: 4 passed
Tests: 2603 passed
パズルデータを変更するたびにCIで整合性が保証されます。
やってみてわかったこと
「バックエンドなし」は制約ではなく武器でした。
- デプロイが静的ファイルのアップロードだけ
- サーバー費用ゼロ
- レスポンスが速い(CDNから直配信)
- メンテナンスコストがほぼゼロ
毎日更新のコンテンツを作るなら、日付ベースの決定論的な問題選択で十分です。ユーザーごとの状態はlocalStorageで管理できます。「サーバーが必要かどうか」を最初に疑うことで、シンプルな構成になりました。
遊んでみてください
インストール不要、無料、毎日新しい問題が更新されます。
上級のキラータンゴモードはかなり手強いです。ノーヒントでクリアできたらぜひXで教えてください。
#定時退社タンゴ
もし実装について気になることがあればコメントください。ソルバーのロジックや自動生成アルゴリズムの詳細も書けます。
