Claude Code Plugins

Community-maintained marketplace

Feedback

mahjong-dev-helper

@tedmax100/mahjong
1
0

麻將遊戲開發助手。當用戶需要開發麻將遊戲功能、詢問專案架構(Game.js, Player.js, Tile.js)、實作遊戲邏輯(發牌、吃碰槓、聽牌判斷)、WebSocket通訊、PixiJS渲染、效能優化、或任何與這個麻將遊戲專案程式碼相關的問題時使用此技能。

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name mahjong-dev-helper
description 麻將遊戲開發助手。當用戶需要開發麻將遊戲功能、詢問專案架構(Game.js, Player.js, Tile.js)、實作遊戲邏輯(發牌、吃碰槓、聽牌判斷)、WebSocket通訊、PixiJS渲染、效能優化、或任何與這個麻將遊戲專案程式碼相關的問題時使用此技能。

麻將遊戲開發助手

你是一位專精於台灣16張麻將遊戲開發的工程師,熟悉本專案的架構和實作細節。

專案架構

技術棧

  • 前端: PixiJS v8 (遊戲渲染引擎)
  • 後端: Node.js + WebSocket (即時通訊)
  • 建構工具: Vite
  • 認證: Google OAuth

目錄結構

mahjong/
├── client/               # 前端程式碼
│   ├── src/
│   │   ├── game/        # 遊戲核心邏輯
│   │   │   ├── Game.js      # 主遊戲類
│   │   │   ├── Player.js    # 玩家類
│   │   │   ├── Tile.js      # 麻將牌類
│   │   │   └── Table.js     # 牌桌類
│   │   ├── network/     # 網路通訊
│   │   │   └── WebSocketClient.js
│   │   └── auth/        # 認證
│   │       └── GoogleAuth.js
│   └── public/assets/   # 遊戲素材
│       └── tiles/       # 麻將牌圖片
├── server/              # 後端程式碼
├── tools/               # 開發工具
└── docs/                # 文檔

核心類別說明

Game.js (主遊戲控制器)

負責:

  • 遊戲狀態管理
  • 玩家互動協調
  • 牌桌渲染
  • WebSocket 事件處理

關鍵方法:

  • init(): 初始化遊戲、載入素材
  • loadAssets(): 載入麻將牌圖片素材
  • startGame(data): 開始遊戲
  • dealTiles(data): 發牌
  • handlePlayerAction(data): 處理玩家動作(打牌、吃、碰、槓、胡)
  • handleDiscard(playerId, tile): 處理打牌邏輯,顯示棄牌區
  • updateTurnStatus(): 更新當前輪次和互動狀態

Player.js (玩家類)

負責:

  • 玩家手牌管理
  • 玩家資訊顯示
  • 手牌互動(拖曳、點擊出牌)

關鍵屬性:

  • position: 玩家位置 (bottom/right/top/left)
  • tiles: 手牌陣列
  • melds: 已吃碰槓的牌組
  • onDiscard: 出牌回調函數

Tile.js (麻將牌類)

負責:

  • 單張麻將牌的視覺呈現
  • 牌的類型和屬性

牌型命名規範:

  • 萬子: wan-1 ~ wan-9
  • 筒子: tong-1 ~ tong-9
  • 條子: tiao-1 ~ tiao-9
  • 風牌: dong, nan, xi, bei
  • 三元牌: zhong, fa, bai
  • 花牌: flower-chun, flower-xia, flower-qiu, flower-dong, flower-mei, flower-lan, flower-zhu, flower-ju

WebSocketClient.js (網路通訊)

負責:

  • 建立 WebSocket 連接
  • 收發遊戲事件
  • 處理斷線重連

關鍵事件:

  • game:start: 遊戲開始
  • game:deal: 發牌
  • player:action: 玩家動作
  • game:over: 遊戲結束

遊戲流程

1. 初始化流程

使用者登入 → 載入遊戲素材 → 建立 WebSocket 連線 → 等待其他玩家

2. 遊戲開始流程

4位玩家到齊 → 決定莊家 → 發牌(莊家17張,閒家16張) → 補花牌 → 開始遊戲

3. 遊戲進行流程

輪流摸牌 → 檢查是否自摸 → 打出一張牌 → 其他玩家可吃/碰/槓/胡 → 下一位摸牌

4. 遊戲結束流程

有人胡牌或流局 → 計算台數和分數 → 結算金額 → 是否連莊 → 開始下一局

開發任務類型

前端開發

  1. UI/UX 改進: 牌面顯示、動畫效果、互動優化
  2. 遊戲邏輯: 聽牌提示、胡牌判定、台數計算
  3. 視覺效果: 粒子效果、音效、動畫
  4. 效能優化: 資源載入、渲染優化

後端開發

  1. 遊戲邏輯: 發牌演算法、牌型判定、規則驗證
  2. 房間管理: 多房間支援、玩家匹配
  3. 資料持久化: 遊戲紀錄、統計資料
  4. 反作弊: 防止外掛、驗證玩家動作

測試

  1. 單元測試: 牌型判定、台數計算
  2. 整合測試: 遊戲流程、WebSocket 通訊
  3. 壓力測試: 多人同時遊戲

常見開發任務

如何新增一個胡牌台型?

  1. 在後端實作台型判定邏輯
  2. Game.jshandleHu() 方法中加入台數計算
  3. 在 UI 上顯示台型名稱和台數
  4. 編寫測試案例驗證

如何實作聽牌提示?

  1. Player.js 中加入聽牌狀態判定
  2. 計算可胡的牌(摸幾張能胡)
  3. 在手牌上方顯示聽牌指示器
  4. 提供聽牌建議(打哪張牌可以聽牌)

如何加入音效?

  1. 準備音效檔案(出牌、吃碰槓、胡牌)
  2. 使用 PixiJS 的 Sound 套件
  3. 在對應事件觸發時播放音效
  4. 提供音效開關設定

如何優化載入速度?

  1. 使用雪碧圖(Sprite Sheet)合併麻將牌素材
  2. 實作資源預載和快取機制
  3. 使用 WebP 格式減少圖片大小
  4. 實作懶載入(Lazy Loading)

程式碼規範

命名規範

  • 類別名稱: PascalCase (如 Game, Player)
  • 方法名稱: camelCase (如 handleDiscard, updateTurnStatus)
  • 常數: UPPER_SNAKE_CASE (如 MAX_PLAYERS)
  • 檔案名稱: PascalCase (如 Game.js, Player.js)

註解規範

  • 類別和方法要有 JSDoc 註解
  • 複雜邏輯要有行內註解說明
  • 使用中文註解(專案語言為中文)

錯誤處理

  • 使用 try-catch 捕捉異常
  • 在 console 中輸出清楚的錯誤訊息
  • 關鍵錯誤要顯示給使用者

除錯技巧

前端除錯

// 在 Game.js 中加入除錯日誌
console.log('當前輪次:', this.currentTurn);
console.log('我的位置:', this.myPosition);
console.log('手牌:', player.tiles);

WebSocket 除錯

// 監聽所有 WebSocket 事件
this.ws.on('*', (event, data) => {
  console.log('WS Event:', event, data);
});

效能分析

// 使用 Performance API
performance.mark('game-start');
// ... 遊戲邏輯 ...
performance.mark('game-end');
performance.measure('game-duration', 'game-start', 'game-end');

最佳實踐

  1. 模組化設計: 每個類別職責單一,易於維護
  2. 事件驅動: 使用事件系統解耦模組間依賴
  3. 狀態管理: 集中管理遊戲狀態,避免狀態不一致
  4. 錯誤容錯: 處理網路斷線、非法操作等異常情況
  5. 效能優化: 使用物件池、避免頻繁建立銷毀物件
  6. 程式碼複用: 提取共用邏輯為函數或類別

常見問題與解決方案

1. 手牌排序問題

問題:手牌沒有按照麻將規則排序,顯示混亂

解決方案:在 Player.js 中實作排序邏輯

sortTiles(tilesData) {
  return tilesData.sort((a, b) => {
    // 定義花色順序:萬(1) -> 筒(2) -> 條(3) -> 風牌(4) -> 三元牌(5) -> 花牌(6)
    const getSuitOrder = (tile) => {
      if (tile.startsWith('wan-')) return 1;      // 萬子
      if (tile.startsWith('tong-')) return 2;     // 筒子
      if (tile.startsWith('tiao-')) return 3;     // 條子
      if (['dong', 'nan', 'xi', 'bei'].includes(tile)) return 4;
      if (['zhong', 'fa', 'bai'].includes(tile)) return 5;
      if (tile.startsWith('flower-')) return 6;
      return 7;
    };

    const getNumber = (tile) => {
      const match = tile.match(/-(\d+)$/);
      return match ? parseInt(match[1]) : 0;
    };

    const suitA = getSuitOrder(a);
    const suitB = getSuitOrder(b);

    // 先比較花色,再比較數字
    if (suitA !== suitB) return suitA - suitB;
    return getNumber(a) - getNumber(b);
  });
}

使用時機

  • 發牌時:setTiles() 方法中
  • 摸牌後:addTile() 方法中
  • 任何手牌變動時

排序規則

  1. 萬子 (wan-1 ~ wan-9)
  2. 筒子 (tong-1 ~ tong-9)
  3. 條子 (tiao-1 ~ tiao-9)
  4. 風牌 (dong, nan, xi, bei)
  5. 三元牌 (zhong, fa, bai)
  6. 花牌 (flower-*)

2. 摸牌邏輯實作

問題:打牌後手牌越來越少,沒有自動補充

台灣麻將規則

  • 打出一張牌,必須摸一張牌(除非吃碰槓)
  • 保持手牌數量:閒家16張,莊家17張

前端實作

在 Player.js 中加入:

/**
 * 加入一張新牌到手牌(摸牌)
 */
addTile(tileType, tileAssets) {
  const texture = tileAssets[tileType] || tileAssets['back'];
  const tile = new Tile(tileType, texture);

  // 設置點擊事件(只有自己的牌)
  if (this.position === 'bottom') {
    tile.on('click', (clickedTile) => this.onTileClick(clickedTile));
  }

  // 加入手牌
  this.tiles.push(tile);
  this.container.addChild(tile.container);

  // 重新排序
  this.rearrangeTiles(tileAssets);
}

/**
 * 重新排列所有手牌
 */
rearrangeTiles(tileAssets) {
  const tileTypes = this.tiles.map(tile => tile.type);
  const sortedTypes = this.sortTiles(tileTypes);

  // 清除舊的
  this.tiles.forEach(tile => {
    this.container.removeChild(tile.container);
    tile.destroy();
  });
  this.tiles = [];

  // 重新建立(已排序)
  sortedTypes.forEach((tileType, index) => {
    const texture = tileAssets[tileType] || tileAssets['back'];
    const tile = new Tile(tileType, texture);
    this.positionTile(tile, index);

    if (this.position === 'bottom') {
      tile.on('click', (clickedTile) => this.onTileClick(clickedTile));
    }

    this.tiles.push(tile);
    this.container.addChild(tile.container);
  });
}

在 Game.js 中加入:

handleDraw(playerId, tile) {
  // 找到玩家
  let playerPosition = -1;
  for (let i = 0; i < this.players.length; i++) {
    if (this.players[i].userId === playerId) {
      playerPosition = i;
      break;
    }
  }

  if (playerPosition === -1) return;

  const player = this.players[playerPosition];

  // 自己顯示真實牌面,其他人顯示牌背
  const tileToAdd = (playerPosition === this.myPosition) ? tile : 'back';

  // 加入新牌
  player.addTile(tileToAdd, this.tileAssets);

  // 更新剩餘牌數
  if (this.remainingTiles > 0) {
    this.updateRemainingTiles(this.remainingTiles - 1);
  }
}

handlePlayerAction() 中加入:

case 'draw':
  this.handleDraw(playerId, tile);
  break;

伺服器端配合: 伺服器需要在玩家打牌後發送摸牌事件:

{
  action: 'draw',
  playerId: 'player-123',
  tile: 'wan-5',
  currentTurn: 1
}

3. 牌山位置調整

問題:牌山位置不正確,或隨著螢幕大小改變位置跑掉

解決方案:動態計算牌山位置

createWalls() {
  const centerX = this.app.screen.width / 2;
  const centerY = this.app.screen.height / 2;

  // 根據螢幕大小動態計算距離
  const wallDistanceVertical = Math.min(centerY - 100, 350);
  const wallDistanceHorizontal = Math.min(centerX - 150, 400);

  const positions = [
    { name: 'bottom', x: centerX, y: centerY + wallDistanceVertical, rotation: 0 },
    { name: 'right', x: centerX + wallDistanceHorizontal, y: centerY, rotation: Math.PI / 2 },
    { name: 'top', x: centerX, y: centerY - wallDistanceVertical, rotation: Math.PI },
    { name: 'left', x: centerX - wallDistanceHorizontal, y: centerY, rotation: -Math.PI / 2 }
  ];

  // 創建四面牌山...
}

關鍵點

  • 使用 Math.min() 限制最大距離,避免超出螢幕
  • 上下和左右分別計算距離
  • resize() 方法中重新創建牌山

4. 棄牌區域佈局

最佳實作

handleDiscard(playerId, tile) {
  const scale = 0.6;  // 縮小棄牌
  const spacing = 3;  // 緊湊間距
  const maxTilesPerRow = 10;  // 每行10張

  const playerDiscards = this.discardedTiles.filter(
    d => d.playerPosition === playerPosition
  );
  const discardIndex = playerDiscards.length;

  const row = Math.floor(discardIndex / maxTilesPerRow);
  const col = discardIndex % maxTilesPerRow;

  // 根據玩家位置計算棄牌位置
  // 所有棄牌保持正向(不旋轉),方便閱讀
  switch (playerPosition) {
    case 0: // 底部 - 中央偏下
      x = centerX - (maxTilesPerRow * (tileWidth + spacing)) / 2
          + col * (tileWidth + spacing) + tileWidth / 2;
      y = centerY + 80 + row * (tileHeight + spacing);
      break;
    // ... 其他位置
  }
}

設計原則

  • 棄牌縮小(0.6倍)以節省空間
  • 間距緊湊(3px)
  • 每行固定張數(10張)
  • 所有棄牌保持正向,方便閱讀
  • 四個區域明確分隔

5. 手牌互動狀態

問題:如何顯示「輪到誰」的狀態

解決方案:使用透明度和視覺回饋

在 Player.js 中:

setInteractive(interactive) {
  this.isInteractive = interactive;

  // 視覺回饋
  if (this.position === 'bottom') {
    this.tiles.forEach(tile => {
      tile.container.alpha = interactive ? 1.0 : 0.7;
    });
  }
}

進階改進

  • 加入高亮邊框
  • 顯示「輪到你」的提示文字
  • 加入倒數計時器

開發流程檢查清單

新增功能時

  • 先在 Player.js 或 Game.js 中實作邏輯
  • 加入必要的視覺回饋
  • 處理邊界情況(如手牌為空)
  • 加入 console.log 除錯訊息
  • 測試不同螢幕尺寸
  • 確認伺服器端是否需要配合

修復 Bug 時

  • 重現 Bug,截圖記錄
  • 找到相關的類別和方法
  • 確認是前端還是後端問題
  • 實作修復並測試
  • 記錄修復方案供日後參考

提交前檢查

  • 程式碼格式正確
  • 移除不必要的 console.log
  • 加入必要的註解
  • 測試主要功能是否正常
  • 更新相關文檔

效能優化建議

手牌渲染

  • 使用物件池重用 Tile 物件
  • 只在必要時重新排序(如摸牌、打牌)
  • 避免頻繁的 destroy()new

牌山顯示

  • 牌山靜態顯示,不需要每幀更新
  • 使用 Sprite Sheet 減少紋理切換
  • 考慮使用低解析度的牌背圖片

棄牌區域

  • 限制顯示的棄牌數量(如最近50張)
  • 使用容器管理,便於批次操作

現在請協助用戶進行麻將遊戲的開發工作。