FRONTEND2026-04-09📖 2分

Next.jsで本格ブラウザ麻雀ゲームを開発した技術スタック全解説

Next.js 16とTypeScriptで開発したブラウザ麻雀ゲーム「雀天」の技術構成を解説。ゲームロジックのnpmパッケージ分離、純関数ベースの状態管理、モバイル対応の設計判断まで、実際のコードを交えて紹介します。

髙木 晃宏

代表 / エンジニア

👨‍💼

なぜNext.jsでブラウザゲームを作ったのか

筆者らaduceでは、ブラウザで遊べる本格4人打ち麻雀ゲーム 雀天(janten.net) を開発・運営しています。ゲーム開発というとUnityやUnreal Engineを想像するかもしれませんが、雀天ではあえてNext.jsを採用しました。

理由はシンプルです。麻雀はターン制のゲームであり、60fpsのリアルタイム描画は不要です。一方で、SEO対応のブログ記事や手牌画像を返すAPIなど、Webアプリケーションとしての機能が多く求められます。Next.jsであれば、ゲームUI・コンテンツページ・APIエンドポイントを一つのプロジェクトで統合的に管理できます。

雀天の技術スタック一覧

カテゴリ技術バージョン
フレームワークNext.js16.2
UIReact19.2
言語TypeScript5.9
ゲームロジックmahjong-core(自作パッケージ)1.0
ビルド(コア)tsup8.x
テストVitest4.1
コンテンツMDX + remark-gfm + rehype-slug-

フロントエンドはReact 19のClient Componentsで構築し、ゲームロジックはローカルnpmパッケージとして完全に分離しています。

ゲームロジックをnpmパッケージとして分離する設計

雀天の設計で最も重要な判断は、麻雀のルール処理を mahjong-core という独立パッケージに切り出したことです。

// package.json { "dependencies": { "mahjong-core": "file:./mahjong-core" } }

mahjong-coretsup でビルドし、CJS/ESM両対応の型付きパッケージとして出力します。これにより、Next.jsのフロントエンドからもAPI Routeからも同じロジックを利用できます。UIの都合でゲームルールが汚染されることがなく、テストも純粋なロジックテストとして高速に実行できます。

純関数ベースのゲーム状態管理

ゲームの状態遷移は純関数で設計しています。例えばポン(他家の捨て牌を取る動作)の処理は次のようになります。

export function executePon( player: number, tile: Tile, discarder: number, hands: Tile[][], melds: Meld[][], discards: Tile[][], ): PonResult { const nH = hands.map(h => [...h]); const nM = melds.map(m => [...m]); const nD = discards.map(d => [...d]); nD[discarder].pop(); // ... 手牌から同じ牌を2枚取り出して副露に追加 nM[player].push({ type: "pon", tiles: ponT, closed: false }); return { hands: nH, melds: nM, discards: nD, ippatsu: nIp }; }

入力の配列をスプレッド構文でコピーし、元の状態を変更せずに新しい状態を返します。この設計には大きなメリットがあります。副作用がないため、どんな状態からでもテストが書けること。そしてUI側のカスタムフック useMahjongGame は、これらの純関数を呼び出して useState に結果をセットするだけで済むことです。

モバイル対応:TileSizeContextによるレスポンシブ設計

麻雀ゲームのUIはモバイル対応が難しいジャンルです。136枚の牌、4人の手牌と捨て牌を限られた画面に収める必要があります。雀天では TileSizeContext というReact Contextで画面サイズに応じた牌の寸法を一元管理しています。

const mob = vw < 500; const nar = vw < 680; const landscape = vh <= 500 && vw > vh; const lsScale = landscape ? Math.min(1.2, Math.max(0.7, vh / 430)) : 1; const T_H = mob ? 30 : nar ? 38 : 46; const TS_W = mob ? 16 : nar ? 20 : 24; const TS_H = mob ? 22 : nar ? 27 : 32;

通常のレスポンシブ(モバイル/ナロー/デスクトップ)に加え、スマートフォンの横持ちにも対応しています。lsScale はiPhone 14 Pro Maxの横向き高さ430pxを基準にスケール係数を計算し、CSS変数として各コンポーネントに注入します。メディアクエリだけでは対応しきれないゲーム特有のレイアウト問題を、Context経由の動的な値で解決しています。

SVG手牌画像APIの実装

雀天にはNext.jsのAPI Routeで実装した手牌画像生成APIがあります。URLパラメータで牌の記法を渡すと、SVG画像を返します。

GET /api/mahjong/tehai?tiles=123m456p789s11z&shanten=true&waits=true
export async function GET(request: NextRequest) { const tilesParam = searchParams.get("tiles"); const tiles = parseTileNotation(tilesParam); // 向聴数・待ち牌もmahjong-coreで計算 const shantenNum = calcShanten(allHandTiles as Tile[]); const waits = getWaits(tiles as Tile[]); const svg = renderSvg({ tiles, tsumo, doraKeys, shantenNum, waits, ... }); return new NextResponse(svg, { headers: { "Content-Type": "image/svg+xml" }, }); }

ゲームロジックを mahjong-core に分離していたことで、API Routeからも向聴数計算や待ち牌判定をそのまま呼び出せています。この画像APIはブログ記事への埋め込みやSNSでのシェアに活用しており、ゲームとコンテンツが同じコードベースにあるNext.jsの強みを活かした機能です。

まとめ

Next.jsでのブラウザゲーム開発は、ターン制ゲームとWebコンテンツの統合という観点で非常に合理的な選択です。雀天 の開発を通じて得た知見をまとめます。

  • ゲームロジックは独立パッケージに分離し、フロントエンドとAPIの両方から利用する
  • 状態遷移は純関数で設計し、テスタビリティと保守性を確保する
  • React Contextで画面サイズに応じた牌寸法を一元管理し、モバイル・横持ちに対応する
  • API Routeとゲームロジックの共有で、画像生成などの付加価値機能を低コストで実装する

aduceでは、このような技術選定から設計・実装まで一貫したIT開発支援を行っています。Webアプリケーションやゲーム開発に関するご相談は、お気軽に お問い合わせ ください。