React × Framer Motion で作る一人麻雀ゲーム

React × Framer Motion で作る一人麻雀ゲーム

作成日:
更新日:

ブラウザで動く一人麻雀ゲームを作ってみました。対戦相手はおらず、配牌から自分のペースで手役を組み立てていく、パズル的なゲームです。

実際に遊んでみる

まずは実際に遊んでみてください。「ゲーム開始」ボタンを押すと配牌され、ツモと捨て牌を繰り返してあがりを目指します。

読み込み中...

遊び方

  1. ゲーム開始 - 13枚の牌が配られ、自動的に1枚ツモります
  2. 捨て牌 - 手牌または引いた牌を選択し、もう一度クリックで捨てます(捨てると自動で次の牌をツモります)
  3. リーチ - テンパイ時にリーチ宣言ができます
  4. ツモあがり - あがり形になったら「ツモ!」ボタンが表示されます

手牌はドラッグ&ドロップで並べ替えることができます(リーチ後は並べ替え不可)。

技術構成

このゲームは以下の技術で構成されています。

  • React 19 - UIフレームワーク
  • Framer Motion - アニメーションライブラリ
  • TypeScript - 型安全な開発

特別なゲームエンジンは使用せず、既存のWebフロントエンド技術だけで実装しています。

実装のポイント

牌のアニメーション

Framer Motion の motion.div を使って、牌のホバー・選択・移動アニメーションを実装しています。

Tile.tsx
<motion.div
  layout
  layoutId={tile.id}
  whileHover={{ y: -6 }}
  whileTap={{ scale: 0.95 }}
  animate={{
    y: isSelected ? -12 : 0,
    boxShadow: isSelected
      ? '0 8px 20px rgba(0,0,0,0.3)'
      : '0 2px 4px rgba(0,0,0,0.2)',
  }}
  transition={{ type: 'spring', stiffness: 400, damping: 25 }}
>
  {/* 牌の内容 */}
</motion.div>

layoutId を指定することで、牌が手牌から河に移動する際にスムーズなアニメーションが実現できます。

手牌の並べ替え

Framer Motion の Reorder コンポーネントを使って、ドラッグ&ドロップでの並べ替えを実装しています。

Hand.tsx
<Reorder.Group
  axis="x"
  values={tiles}
  onReorder={handleReorder}
>
  {tiles.map((tile) => (
    <Reorder.Item key={tile.id} value={tile}>
      <MahjongTile tile={tile} />
    </Reorder.Item>
  ))}
</Reorder.Group>

ゲーム状態管理

カスタムフック useMahjongGame でゲームの状態を管理しています。

useMahjongGame.ts
export type GamePhase =
  | 'waiting'    // ゲーム開始待ち
  | 'dealing'    // 配牌中
  | 'draw'       // ツモ待ち
  | 'discard'    // 捨て牌選択中
  | 'win'        // あがり
  | 'draw_game'; // 流局
 
export interface GameState {
  phase: GamePhase;
  wall: Tile[];         // 牌山
  hand: Tile[];         // 手牌
  river: Tile[];        // 河(捨て牌)
  drawnTile: Tile | null;
  doraIndicators: Tile[];
  isReach: boolean;
  // ...
}

役判定

あがり形の判定は、雀頭(対子)を抜いて残りが4面子(刻子または順子)で構成されるかを再帰的にチェックしています。

yaku.ts
// 雀頭候補を試す
for (const [key, group] of groups) {
  if (group.length >= 2) {
    const pair = group.slice(0, 2);
    const remaining = tiles.filter((t) => !pair.includes(t));
    
    const result = extractSets(remaining, []);
    if (result && result.sets.length === 4) {
      return { isWinning: true, sets: result.sets, pair };
    }
  }
}

特殊形として七対子と国士無双にも対応しています。

対応している役

Mリーグルールに準拠した役を判定できます。

1翻役

役名説明
リーチ門前でリーチ宣言
一発リーチ後1巡以内にあがり
門前清自摸和門前でツモあがり
平和全て順子で役牌以外の雀頭
断么九么九牌を含まない
一盃口同じ順子が2つ
役牌白・發・中の刻子
海底摸月最後のツモであがり

2翻役

役名説明
ダブルリーチ第一巡でリーチ
七対子7つの対子
一気通貫同じ色で123, 456, 789の順子
三色同順3色で同じ数字の順子
三色同刻3色で同じ数字の刻子
対々和全て刻子
三暗刻暗刻が3つ
小三元白發中のうち2刻子+1雀頭
混老頭么九牌のみ
チャンタ全ての面子と雀頭に么九牌

3翻役

役名説明
混一色一種類の数牌と字牌のみ
純チャン全ての面子と雀頭に1,9(字牌なし)
二盃口一盃口が2組

6翻役

役名説明
清一色一種類の数牌のみ

役満

役名説明
天和配牌であがり
国士無双13種の么九牌+1枚
四暗刻4つの暗刻
大三元白發中の刻子
小四喜東南西北のうち3刻子+1雀頭
大四喜東南西北の刻子
字一色字牌のみ
緑一色緑色の牌のみ(索子2,3,4,6,8と發)
清老頭1と9の数牌のみ
九蓮宝燈同色で1112345678999+1枚

まとめ

React と Framer Motion を使えば、特別なゲームエンジンなしでもブラウザゲームを作ることができます。

今回のポイント:

  • Framer Motion で手軽にアニメーションを実装
  • Reorder でドラッグ&ドロップの並べ替え
  • カスタムフック でゲームロジックを分離
  • TypeScript で型安全に牌や役を管理

麻雀のルールは複雑ですが、一人用にすることで鳴きや点数計算を省略でき、実装がシンプルになります。ぜひ遊んでみてください。