Skip to content

実装詳細

概要

このドキュメントでは、ぷよぷよゲームアプリケーションの具体的な実装内容について詳しく説明します。Clean Architectureに基づいた4層構造での実装方針と、各層における具体的な実装詳細を記載します。

実装アーキテクチャ

レイヤー構成と実装

uml diagram

各層の実装詳細

1. プレゼンテーション層(Presentation Layer)

App.tsx - ルートコンポーネント

// 依存性注入の初期化
useState(() => {
  DIConfiguration.initializeApplication()
  return null
})

// サービス解決
const [gameUseCase] = useState(() =>
  container.resolve<GameUseCase>(GAME_USE_CASE)
)

主要実装内容:

  • DIコンテナの初期化とサービス解決
  • ゲーム状態に応じたUI制御
  • キーボード・タッチイベントハンドリング
  • BGM・効果音の状態変化制御

GameBoard.tsx - ゲーム盤面

// Canvas APIによる描画
const canvasRef = useRef<HTMLCanvasElement>(null)
const drawField = useCallback((field: Field) => {
  const canvas = canvasRef.current
  const ctx = canvas?.getContext('2d')
  // ぷよ描画ロジック
}, [])

実装特徴:

  • Canvas APIを使用した高性能描画
  • React useRefによるCanvas制御
  • アニメーション状態管理
  • レスポンシブ対応

コンポーネント設計原則

  • 単一責任の原則に基づく細分化
  • プロパティによる疎結合
  • React Hooksによる状態管理
  • TypeScriptによる型安全性

2. アプリケーション層(Application Layer)

GameUseCase.ts - ユースケース統合

export class GameUseCase {
  private game: Game = new Game()

  public startNewGame(): void {
    this.game.start()
  }

  public moveLeft(): boolean {
    return this.game.moveCurrentPairLeft()
  }

  // プレゼンテーション層用データ変換
  public getGameStateData(): GameStateData {
    const fieldData: PuyoData[][] = []
    const field = this.game.field

    for (let y = 0; y < field.height; y++) {
      fieldData[y] = []
      for (let x = 0; x < field.width; x++) {
        const puyo = field.getPuyo(x, y)
        fieldData[y][x] = {
          color: puyo ? puyo.color : 'empty'
        }
      }
    }

    return {
      isPlaying: this.isPlaying(),
      isPaused: this.isPaused(), 
      isGameOver: this.isGameOver(),
      currentScore: this.game.score,
      chainCount: this.game.lastChainResult?.chainCount || 0,
      fieldData,
      nextPair: this.getNextPairData()
    }
  }
}

実装ポイント:

  • ドメインロジックとUI層の橋渡し
  • データ変換による層間疎結合
  • ビジネス操作の統一インターフェース

DIConfiguration.ts - 依存性注入設定

export class DIConfiguration {
  public static setupContainer(): void {
    // アプリケーション層サービス
    container.registerFactory(GAME_USE_CASE, () => new GameUseCase())

    // サービス層サービス(シングルトン)
    container.registerSingleton(SOUND_EFFECT_SERVICE, soundEffect)
    container.registerSingleton(BACKGROUND_MUSIC_SERVICE, backgroundMusic)
    container.registerSingleton(HIGH_SCORE_SERVICE, highScoreService)
    container.registerSingleton(GAME_SETTINGS_SERVICE, gameSettingsService)
  }
}

3. サービス層(Service Layer)

音響サービス実装

// SoundEffectService.tsx
class SoundEffectService {
  private audioElements: Map<SoundType, HTMLAudioElement> = new Map()

  public async play(type: SoundType): Promise<void> {
    try {
      const audio = this.getAudioElement(type)
      audio.currentTime = 0
      await audio.play()
    } catch (error) {
      console.error(`Failed to play sound ${type}:`, error)
    }
  }
}

PWAService実装

export class PWAService {
  public async registerSW(): Promise<void> {
    // 開発環境では Service Worker の登録をスキップ
    if (import.meta.env.DEV) {
      console.log('PWA: Service Worker registration skipped in development mode')
      return
    }

    if ('serviceWorker' in navigator) {
      this.wb = new Workbox('/sw.js')
      // イベントハンドラー設定...
    }
  }
}

4. ドメイン層(Domain Layer)

Game.ts - ゲームコア

export class Game {
  public state: GameState = GameState.READY
  public score: number = 0
  public field: Field
  public currentPair: PuyoPair | null = null
  public nextPair: PuyoPair | null = null
  public lastChainResult: ChainResult | null = null
  private chain: Chain

  public moveCurrentPairLeft(): boolean {
    if (!this.currentPair || this.state !== GameState.PLAYING) {
      return false
    }

    const newX = this.currentPair.x - 1
    if (this.field.canPlacePuyoPair(this.currentPair, newX, this.currentPair.y)) {
      this.currentPair.x = newX
      return true
    }
    return false
  }
}

Field.ts - フィールド管理

export class Field {
  public readonly height = 16 // 隠しライン2行 + 表示14行
  public readonly width = 6
  private grid: (Puyo | null)[][]

  public findConnectedPuyos(x: number, y: number): [number, number][] {
    const puyo = this.getPuyo(x, y)
    if (!puyo) return []

    const visited = new Set<string>()
    const result: [number, number][] = []
    this.dfs(x, y, puyo.color, visited, result)
    return result
  }

  public applyGravity(): boolean {
    let moved = false
    // 重力処理の実装...
    return moved
  }
}

Chain.ts - 連鎖システム

export class Chain {
  public detectAndExecute(): ChainResult | null {
    const chainDetails: ChainStep[] = []
    let totalScore = 0
    let totalErasedCount = 0
    let stepNumber = 1

    while (true) {
      const step = this.executeSingleStep(stepNumber)
      if (step.erasedPuyos.length === 0) break

      chainDetails.push(step)
      totalScore += step.stepScore
      totalErasedCount += step.erasedPuyos.length
      stepNumber++

      this.field.applyGravity()
    }

    return chainDetails.length > 0 ? {
      chainCount: chainDetails.length,
      totalErasedCount,
      score: totalScore,
      chainDetails
    } : null
  }
}

5. インフラストラクチャ層(Infrastructure Layer)

Container.ts - DIコンテナ

export class Container {
  private singletons: Map<string, unknown> = new Map()
  private factories: Map<string, () => unknown> = new Map()

  public registerSingleton<T>(token: string, instance: T): void {
    this.singletons.set(token, instance)
  }

  public registerFactory<T>(token: string, factory: () => T): void {
    this.factories.set(token, factory)
  }

  public resolve<T>(token: string): T {
    // シングルトン解決
    if (this.singletons.has(token)) {
      return this.singletons.get(token) as T
    }

    // ファクトリー解決
    if (this.factories.has(token)) {
      const factory = this.factories.get(token)!
      return factory() as T
    }

    throw new Error(`No registration found for token: ${token}`)
  }
}

テスト実装戦略

3層テスト構造

単体テスト(Unit Tests)

// ドメインロジックの例
describe('Chain', () => {
  it('基本的な4個消去が正しく動作する', () => {
    // 3A Pattern (Arrange-Act-Assert)
    const field = new Field()
    // ... フィールド設定

    const chain = new Chain(field)
    const result = chain.detectAndExecute()

    expect(result).not.toBeNull()
    expect(result!.chainCount).toBe(1)
    expect(result!.totalErasedCount).toBe(4)
  })
})

統合テスト(Integration Tests)

// コンポーネント統合テスト
describe('GameBoard Integration', () => {
  it('ゲーム開始から基本操作まで統合動作する', () => {
    render(<App />)

    const startButton = screen.getByTestId('start-button')
    fireEvent.click(startButton)

    const gameBoard = screen.getByTestId('game-board')
    expect(gameBoard).toBeInTheDocument()
  })
})

E2Eテスト(End-to-End Tests)

// Playwrightによるブラウザテスト
test('ゲーム開始から基本操作まで', async ({ page }) => {
  await page.goto('/')

  await page.click('[data-testid="start-button"]')
  await expect(page.locator('[data-testid="game-board"]')).toBeVisible()

  // キーボード操作テスト
  await page.keyboard.press('ArrowLeft')
  await page.keyboard.press('ArrowRight')
  await page.keyboard.press('ArrowUp') // 回転
})

パフォーマンス最適化

レンダリング最適化

// React.memo によるメモ化
export const PuyoCell = React.memo<PuyoCellProps>(({ puyo, x, y }) => {
  return (
    <div 
      className={`puyo-cell ${puyo?.color || 'empty'}`}
      style={{ gridColumn: x + 1, gridRow: y + 1 }}
    />
  )
})

// useCallback によるコールバック最適化
const handleMove = useCallback((direction: 'left' | 'right') => {
  const moved = direction === 'left' 
    ? gameUseCase.moveLeft() 
    : gameUseCase.moveRight()

  if (moved) {
    soundEffectService.play(SoundType.PUYO_MOVE)
    forceRender()
  }
}, [gameUseCase, soundEffectService, forceRender])

バンドル最適化

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
        },
      },
    },
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
  },
})

アーキテクチャ監視

dependency-cruiser設定

// .dependency-cruiser.cjs
module.exports = {
  forbidden: [
    {
      name: 'presentation-layer-boundaries',
      comment: 'プレゼンテーション層は上位層のみ参照可能',
      severity: 'error',
      from: { path: '^src/components' },
      to: {
        path: '^src/(?!application|services|utils|styles|hooks|types\\.ts)',
        pathNot: '^src/(application|services|utils|styles|hooks|components|types\\.ts)',
      },
    },
    // ... その他のルール
  ],
}

型安全性の実装

共通型定義

// types.ts
export interface PuyoData {
  color: string
}

export interface PuyoPairData {
  main: PuyoData
  sub: PuyoData
}

export interface GameStateData {
  isPlaying: boolean
  isPaused: boolean
  isGameOver: boolean
  currentScore: number
  chainCount: number
  fieldData: PuyoData[][]
  nextPair: PuyoPairData | null
}

厳格なTypeScript設定

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitReturns": true,
    "noImplicitAny": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

実装のベストプラクティス

1. SOLID原則の適用

  • Single Responsibility: 各クラスは単一の責任
  • Open/Closed: 拡張に開いて修正に閉じている
  • Liskov Substitution: 派生クラスは基底クラスと置換可能
  • Interface Segregation: 使用しないインターフェースに依存しない
  • Dependency Inversion: 具象ではなく抽象に依存

2. テスト駆動開発(TDD)

  • Red: 失敗するテストを書く
  • Green: テストを通す最小実装
  • Refactor: リファクタリングで品質向上

3. 継続的品質管理

  • ESLint による静的解析
  • Prettier による自動フォーマット
  • dependency-cruiser によるアーキテクチャ監視
  • Vitest による単体・統合テスト
  • Playwright による E2E テスト

この実装により、保守性・拡張性・テスタビリティを兼ね備えたクリーンなアーキテクチャを実現しています。