ドパ麻雀
手番のないリアルタイム麻雀(仕様書 v0.5)。4人が同時並行でツモ・打牌し、鳴き・和了は早押しで決まります。
構成はワードジャマー/17ポーカーと同じ Next.js 14 + Firebase Firestore(Vercel無料枠で公開可、課金不要)です。
shared/ 麻雀ロジック(役・符・点数・対局進行・CPU)とテスト
web/ Next.js アプリ(ロビー・待機室・対局・観戦・ランキング・管理画面)
動かすまでの手順
1. 依存パッケージのインストール
npm install
2. Firebase の準備
ワードジャマー・17ポーカーと 同じFirebaseプロジェクトを使ってOK です(コレクション名は dopa_* で分けています)。
- Firebaseコンソールの Firestore Database →「ルール」タブに、
web/firestore.rules の中身を貼り付けて「公開」。
- このファイルにはワードジャマー・17ポーカーのルールも含めてあるので、そのまま置き換えて大丈夫です。
- プロジェクトの設定 → 全般 → マイアプリ(ウェブ) の
firebaseConfig の6項目を控える(既存アプリと同じ値)。
- 管理画面を使う場合のみ:プロジェクトの設定 →「サービスアカウント」→「新しい秘密鍵を生成」で JSON をダウンロード。
3. 環境変数
web/.env.local.example を web/.env.local にコピーして値を入れます。
| 変数 | 内容 |
|---|
NEXT_PUBLIC_FIREBASE_* | 手順2-2の6項目 |
ADMIN_PASSWORD | 管理画面のパスワード(任意の文字列) |
FIREBASE_SERVICE_ACCOUNT_JSON | 手順2-3のJSONの中身(1行にして貼る。base64でも可) |
ADMIN_PASSWORD と FIREBASE_SERVICE_ACCOUNT_JSON はサーバー側だけで使います。NEXT_PUBLIC_ を付けないでください。
4. 起動
npm run dev # http://localhost:3000
npm test # 麻雀ロジックのテスト
5. 公開(Vercel)
- Vercelで「Add New Project」→ このリポジトリをインポート。
- Root Directory に
web を指定。
- Environment Variables に手順3の値を1組ずつ(Key と Value に分けて)設定して Deploy。
FIREBASE_SERVICE_ACCOUNT_JSON は複数行のJSONをそのまま貼ってOK。「starts and ends with whitespace and has return characters」という警告が出ますが問題ありません。
- 本番(Production)として公開されるのは
main ブランチです。作業ブランチからのデプロイはプレビュー扱いになります。
本番では、Vercelが自動で作るアカウント名入りのURL(*.vercel.app)で開かれると、https://dopa-mahjong.vercel.app に自動で切り替わります(web/src/middleware.ts)。別のURLに変える場合は、環境変数 CANONICAL_HOST にそのホスト名(例:dopa-mj.vercel.app)を設定してください。
管理画面は /admin です(ロビーからはリンクしていません)。
仕組みと、知っておいてほしい制約
サーバー(常駐プロセス)を持たない構成のため、仕様8章「判定はすべてサーバー側」を次の形で実現しています。
- 判定:各ブラウザが Firestore のトランザクションの中で同じ対局ロジック(
shared/)を実行し、結果を書き込みます。トランザクションは「先にコミットした方が勝ち」なので、早押しは Firestore に届いた順 で決まります。
- 時間切れ・CPU:対局中の人間のうち1人(席番号が最も小さい接続中の人)が「進行役」として、10秒の自動ツモ切り・CPUの打牌・次局への移行を実行します。進行役が落ちると他の人が数秒遅れで代わります。CPU戦であなたがページを閉じると対局は止まり、戻ると再開します。
- 時刻:端末の時計のずれは Firestore のサーバー時刻との差を測って補正しています。
制約(正直に書いておきます):
- 不正は防げません。対局データ(全員の手牌・山を含む)はブラウザが読める1つのドキュメントにあるため、開発者ツールを使えば他人の手牌が見えます。身内で遊ぶ前提の設計です(ワードジャマーのREADMEと同じ扱い)。
- 無料枠の目安:Firestore無料枠は1日あたり書き込み2万回・読み取り5万回(ワードジャマー・17ポーカーと共有)。書き込みは1局あたり70〜100回程度、東風戦1回で500〜1000回程度と見込んでいます(実測ではなく推定)。4人対戦は参加者全員が更新を受信するので読み取りが増え、4人対戦なら1日数時間程度 が目安です。超えると翌日まで使えなくなります(課金はされません)。
- 通信遅延の補正はしません(仕様どおり)。通信が遅い端末は早押しで不利です。
管理ログについて
- 管理画面でのログイン(失敗含む)と、すべての変更操作(強制終了・削除・除外・修正・リセット・統合、失敗した操作も含む)を
dopa_admin_logs に記録します。閲覧操作は記録しません。
- アプリにはログを削除・変更する機能がありません。Firestoreのルールでブラウザからの読み書きもすべて禁止しており、書き込めるのは管理画面のサーバー処理だけです。
- 各ログは直前のログのハッシュを含む「ハッシュチェーン」になっていて、管理画面の「改ざんチェック」で欠落・書き換えを検出できます。
- ただし Firebaseプロジェクトのオーナーは、Firebaseコンソールから直接ログを消せます(Firebaseの仕組み上、これを完全に防ぐ方法はありません)。消された場合は改ざんチェックで検出されます。
仕様書で決まっていなかった点の扱い
確認済みのもの:
- 鳴いたとき、手元の未使用のツモ牌は 山の先頭に戻す。
- 山切れ後は鳴き不可。まだ打牌していない人は最後の打牌を行い(ツモ和了可)、全員の打牌が終わって3秒後に流局。河底撈魚はその局の最後の打牌へのロン。
- 立直後の自動ツモ切りは ツモから1秒後(「ツモ切り」ボタンは廃止)。
こちらで決めたもの(変更したい場合は言ってください):
- 一発は「立直宣言後、次にツモした牌を捨てるまで」に和了すれば成立(仕様7章の解釈)。
- 立直棒は宣言時に払い、宣言牌でロンされたら返却(=供託が発生しない)。
- 見逃しフリテンは、当たり牌を捨てた人が次の牌を捨てた時点(ロンできる期間が終わった時点)で確定。見逃したかは「待ちに含まれていたか」で判定(役の有無は問わない)。
- 立直後に和了牌をツモって和了しなかった場合も、その局の終わりまでフリテン。
- オーラス以降の終了判定の「原点」は 30000点(和了止め・聴牌止めは親がトップかつ30000点以上)。
- 和了・流局画面は最大12秒表示(接続中の人間が全員OKを押せば先に進む)。
- 対局開始時の席順(起家)はランダム。
- 流し満貫では積み棒の支払いなし(本場は増える)。
- 同じルーム内に同じ名前の人は入れない。対局中に同じ名前で入ると、その席に再入場。
- 自動和了は最優先とし、接続が切れている扱いの間も働く(仕様書6-1「接続が切れている間、自動和了は機能しない」から変更。接続判定の誤りで和了を見逃さないため)。
- 放置されたルーム(最後の更新から10分以上たち、参加者の誰も10分以上接続していない待機中・対局中のルーム)は、ロビーを開いた人のブラウザが見つけた時点で終了扱いにする。途中で終わった対局は戦績に記録しない。
- 牌譜(各局の終局時の盤面)は
dopa_matches に kind: "kifu" として保存する(JSON文字列。Firestoreが配列の中の配列を保存できないため)。
- CPUの操作・時間切れの処理は、Firestoreへの書き込みを減らすため最低1秒間隔でまとめて書き込む(CPUの操作が最大1秒ほど遅れることがある)。
- 戦績の集計対象は人間のみ(CPUは記録しない)。
- ランキングは東風戦・東南戦・一荘戦を名前ごとに合計して表示する(記録は種類ごとに別々に持つので、管理画面のプレイヤー一覧は種類別)。どれかで除外された名前は、合計のランキングからも除外する。
- 一荘戦(東南西北)は北4局で終了し、延長戦はない(オーラスの和了止め・聴牌止め、飛び終了は他と同じ)。
- 三人麻雀は天鳳の段位戦3人打ちに合わせる:二萬〜八萬抜きの108枚、一萬↔九萬のドラ、チーなし、北抜きドラ(ポン直後は不可、抜きは鳴き扱い、抜いた北へのロンあり、北の面子はオタ風)、35000点持ち40000点返し・ウマ+20/0/-20、1場3局(東風戦は南3局・東南戦は西3局まで延長)、戦績・ランキングは四人と別集計。
- 三人麻雀で天鳳の説明に記載がなく、こちらで決めたもの:ツモ損あり(いない1人の分は払わない)、積み棒はロン300点・ツモは2人が各100点、ノーテン罰符は場に2000点、抜いた北へのロンに槍槓は付かない、立直中はツモった北だけ抜ける、四風連打・四家立直の途中流局なし。
- 少牌マイティ(三人麻雀のオプション、東風戦・東南戦・一荘戦を選べる):手牌12枚+架空の13枚目をオールマイティ(★)として扱う。★は和了時に最も高い点数になる牌(役満含む)として評価し、表ドラ・裏ドラにはなるが赤ドラにはならない。自分の手牌・副露・抜いた北で5枚目になる牌と二萬〜八萬にはなれない。待ち・フリテン・立直後の暗槓の判定は★込み、ポン・カンは実際の牌のみ、九種九牌は12枚+ツモの13枚で判定。CPUは★込みの向聴数を「通常の向聴数−1」で近似する(処理を軽くするため)。戦績・ランキングは別集計(mode = "mighty_tonpu" など。対局の長さを記録していなかった頃の対局は "mighty")。
- ドパなし(
rules.turnBased、今は少牌マイティのときだけ選べる):順番にツモ・打牌する普通の進行。1手番10秒(立直後のツモ切りは1秒)。打牌・加槓・北抜きのあと、ロン・ポン・カンできる人が答えるまで(最大10秒、スキップ可)次へ進まない。ロンはポン・カンより優先(ロンできる人が答え終わってから、宣言済みのポン・カンが成立)。ロンが複数人できるときは先に宣言した人の和了(自動和了の人どうしはダブロン)。見逃し・スキップは同巡内フリテン。鳴き・暗槓が入ると全員の第1巡は終わり。戦績はドパありと同じ集計。CPUの反応時間は強さに関係なく0.3〜0.6秒(進行役の書き込みが1秒間隔なので、実際は1手あたり約1秒)。
- 接続記録(利用状況の把握・不正チェック用。利用者の同意は運営側で取得済み):ロビー・部屋を開いたときに
/api/visit(サーバー)が接続元IP・Vercelが付けるIPからの推定地域・ブラウザ/端末情報・名前・ブラウザIDを dopa_access_logs に保存する(同じ端末・同じ名前では1日1回まで。その日に名前を変えるともう1回)。GPSなどブラウザの許可画面が出る情報は取らない。このコレクションはFirestoreのルールで許可していないため、ブラウザから直接は読み書きできず、管理画面の「接続記録」タブからのみ見られる。
- 効果音は Web Audio API で合成(音源ファイルなし)。右下の🔊で切替。
ローカルで Firestore エミュレータを使う(任意)
web/.env.local に以下を加えると、本番のFirestoreを使わずに試せます(Java と firebase-tools が必要)。
NEXT_PUBLIC_FIRESTORE_EMULATOR_HOST=127.0.0.1:8080
FIRESTORE_EMULATOR_HOST=127.0.0.1:8080