Files
DFJ/02_详细设计文档/游戏核心逻辑详设.md

24 KiB
Raw Blame History

游戏核心逻辑详设文档

文档版本: v1.0
撰写人: 游戏逻辑架构师
创建日期: 2024年9月11日

1. 游戏逻辑总览

1.1 核心游戏机制

基于经典"打飞机"游戏规则,实现回合制战略对战:

// 游戏核心参数
const GAME_CONFIG = {
  BOARD_SIZE: 10,           // 10x10棋盘
  PLANE_COUNT: 3,           // 每位玩家3架飞机
  PLANE_SIZE: 11,           // 每架飞机占11个格子
  TURN_TIME_LIMIT: 30,      // 每回合30秒限时
  GAME_TIME_LIMIT: 1800     // 游戏总时长30分钟
}

// 游戏阶段枚举
enum GamePhase {
  WAITING = 'waiting',           // 等待玩家
  PLACING = 'placing',           // 飞机布置阶段
  BATTLING = 'battling',         // 对战阶段  
  FINISHED = 'finished'          // 游戏结束
}

// 攻击结果类型
enum AttackResult {
  MISS = 'miss',                 // 未命中
  HIT = 'hit',                   // 命中
  DESTROY = 'destroy'            // 击毁飞机
}

1.2 飞机几何模型

// 飞机形状定义
interface PlaneShape {
  id: string
  center: Position                // 飞机头部位置(中心点)
  direction: Direction            // 飞机朝向
  positions: Position[]           // 飞机占据的所有位置
  parts: {
    head: Position                // 头部1个格子
    wings: Position[]             // 翅膀5个格子
    body: Position[]              // 机身2个格子
    tail: Position[]              // 尾翼3个格子
  }
}

// 方向枚举
enum Direction {
  UP = 'up',
  DOWN = 'down', 
  LEFT = 'left',
  RIGHT = 'right'
}

// 位置坐标
interface Position {
  x: number                       // 行坐标 (0-9)
  y: number                       // 列坐标 (0-9)
}

// 飞机几何生成器
export class PlaneGeometry {
  static generatePlane(center: Position, direction: Direction): PlaneShape {
    const plane: PlaneShape = {
      id: generateId(),
      center,
      direction,
      positions: [],
      parts: {
        head: center,
        wings: [],
        body: [],
        tail: []
      }
    }

    switch (direction) {
      case Direction.UP:
        plane.parts = {
          head: center,
          wings: [
            { x: center.x + 1, y: center.y - 2 },
            { x: center.x + 1, y: center.y - 1 },
            { x: center.x + 1, y: center.y },
            { x: center.x + 1, y: center.y + 1 },
            { x: center.x + 1, y: center.y + 2 }
          ],
          body: [
            { x: center.x + 2, y: center.y },
            { x: center.x + 3, y: center.y }
          ],
          tail: [
            { x: center.x + 4, y: center.y - 1 },
            { x: center.x + 4, y: center.y },
            { x: center.x + 4, y: center.y + 1 }
          ]
        }
        break

      case Direction.DOWN:
        plane.parts = {
          head: center,
          wings: [
            { x: center.x - 1, y: center.y - 2 },
            { x: center.x - 1, y: center.y - 1 },
            { x: center.x - 1, y: center.y },
            { x: center.x - 1, y: center.y + 1 },
            { x: center.x - 1, y: center.y + 2 }
          ],
          body: [
            { x: center.x - 2, y: center.y },
            { x: center.x - 3, y: center.y }
          ],
          tail: [
            { x: center.x - 4, y: center.y - 1 },
            { x: center.x - 4, y: center.y },
            { x: center.x - 4, y: center.y + 1 }
          ]
        }
        break

      case Direction.LEFT:
        plane.parts = {
          head: center,
          wings: [
            { x: center.x - 2, y: center.y + 1 },
            { x: center.x - 1, y: center.y + 1 },
            { x: center.x, y: center.y + 1 },
            { x: center.x + 1, y: center.y + 1 },
            { x: center.x + 2, y: center.y + 1 }
          ],
          body: [
            { x: center.x, y: center.y + 2 },
            { x: center.x, y: center.y + 3 }
          ],
          tail: [
            { x: center.x - 1, y: center.y + 4 },
            { x: center.x, y: center.y + 4 },
            { x: center.x + 1, y: center.y + 4 }
          ]
        }
        break

      case Direction.RIGHT:
        plane.parts = {
          head: center,
          wings: [
            { x: center.x - 2, y: center.y - 1 },
            { x: center.x - 1, y: center.y - 1 },
            { x: center.x, y: center.y - 1 },
            { x: center.x + 1, y: center.y - 1 },
            { x: center.x + 2, y: center.y - 1 }
          ],
          body: [
            { x: center.x, y: center.y - 2 },
            { x: center.x, y: center.y - 3 }
          ],
          tail: [
            { x: center.x - 1, y: center.y - 4 },
            { x: center.x, y: center.y - 4 },
            { x: center.x + 1, y: center.y - 4 }
          ]
        }
        break
    }

    // 合并所有位置
    plane.positions = [
      plane.parts.head,
      ...plane.parts.wings,
      ...plane.parts.body,
      ...plane.parts.tail
    ]

    return plane
  }

  // 验证飞机位置是否合法
  static validatePlanePosition(plane: PlaneShape, boardSize: number = 10): boolean {
    return plane.positions.every(pos => 
      pos.x >= 0 && pos.x < boardSize && 
      pos.y >= 0 && pos.y < boardSize
    )
  }

  // 检查两架飞机是否重叠
  static checkPlanesOverlap(plane1: PlaneShape, plane2: PlaneShape): boolean {
    return plane1.positions.some(pos1 =>
      plane2.positions.some(pos2 =>
        pos1.x === pos2.x && pos1.y === pos2.y
      )
    )
  }
}

2. 棋盘状态管理

2.1 棋盘数据结构

// 单元格状态
enum CellState {
  EMPTY = 'empty',                    // 空格
  PLANE_PART = 'plane_part',          // 飞机部件
  ATTACKED_MISS = 'attacked_miss',    // 攻击未命中
  ATTACKED_HIT = 'attacked_hit'       // 攻击命中
}

// 棋盘单元格
interface BoardCell {
  position: Position
  state: CellState
  planeId?: string                    // 所属飞机ID
  partType?: 'head' | 'wing' | 'body' | 'tail'  // 部件类型
  isDestroyed?: boolean              // 是否已被击毁
  attackedAt?: Date                  // 攻击时间
}

// 游戏棋盘
interface GameBoard {
  size: number                       // 棋盘大小 (10x10)
  cells: BoardCell[][]              // 二维单元格数组
  planes: PlaneShape[]              // 放置的飞机
  attackHistory: AttackRecord[]     // 攻击历史
  remainingPlanes: number          // 剩余飞机数量
}

// 攻击记录
interface AttackRecord {
  position: Position
  result: AttackResult
  timestamp: Date
  targetPlaneId?: string
}

2.2 棋盘操作类

export class BoardManager {
  // 创建空棋盘
  static createEmptyBoard(size: number = 10): GameBoard {
    const cells: BoardCell[][] = []
    
    for (let x = 0; x < size; x++) {
      cells[x] = []
      for (let y = 0; y < size; y++) {
        cells[x][y] = {
          position: { x, y },
          state: CellState.EMPTY
        }
      }
    }

    return {
      size,
      cells,
      planes: [],
      attackHistory: [],
      remainingPlanes: 0
    }
  }

  // 在棋盘上放置飞机
  static placePlane(board: GameBoard, plane: PlaneShape): boolean {
    // 验证飞机位置合法性
    if (!PlaneGeometry.validatePlanePosition(plane, board.size)) {
      return false
    }

    // 检查是否与现有飞机重叠
    for (const existingPlane of board.planes) {
      if (PlaneGeometry.checkPlanesOverlap(plane, existingPlane)) {
        return false
      }
    }

    // 在棋盘上标记飞机位置
    plane.positions.forEach(pos => {
      const cell = board.cells[pos.x][pos.y]
      cell.state = CellState.PLANE_PART
      cell.planeId = plane.id
      
      // 标记部件类型
      if (pos.x === plane.parts.head.x && pos.y === plane.parts.head.y) {
        cell.partType = 'head'
      } else if (plane.parts.wings.some(w => w.x === pos.x && w.y === pos.y)) {
        cell.partType = 'wing'
      } else if (plane.parts.body.some(b => b.x === pos.x && b.y === pos.y)) {
        cell.partType = 'body'
      } else {
        cell.partType = 'tail'
      }
    })

    // 添加飞机到棋盘
    board.planes.push(plane)
    board.remainingPlanes++

    return true
  }

  // 批量放置飞机
  static placePlanes(board: GameBoard, planes: PlaneShape[]): boolean {
    if (planes.length !== 3) {
      throw new Error('必须放置3架飞机')
    }

    // 创建临时棋盘进行验证
    const tempBoard = this.createEmptyBoard(board.size)

    // 逐个放置验证
    for (const plane of planes) {
      if (!this.placePlane(tempBoard, plane)) {
        return false
      }
    }

    // 验证通过,应用到实际棋盘
    board.cells = tempBoard.cells
    board.planes = tempBoard.planes
    board.remainingPlanes = tempBoard.remainingPlanes

    return true
  }

  // 执行攻击
  static executeAttack(board: GameBoard, position: Position): AttackResult {
    const cell = board.cells[position.x][position.y]

    // 检查是否已经攻击过该位置
    if (cell.state === CellState.ATTACKED_MISS || cell.state === CellState.ATTACKED_HIT) {
      throw new Error('该位置已被攻击过')
    }

    let result: AttackResult
    let targetPlaneId: string | undefined

    if (cell.state === CellState.PLANE_PART) {
      // 命中飞机
      cell.state = CellState.ATTACKED_HIT
      cell.isDestroyed = true
      targetPlaneId = cell.planeId

      // 检查飞机是否完全被击毁
      const plane = board.planes.find(p => p.id === targetPlaneId)!
      const allPartsDestroyed = plane.positions.every(pos => {
        const targetCell = board.cells[pos.x][pos.y]
        return targetCell.isDestroyed
      })

      if (allPartsDestroyed) {
        result = AttackResult.DESTROY
        board.remainingPlanes--
        
        // 标记整架飞机为已击毁
        plane.positions.forEach(pos => {
          board.cells[pos.x][pos.y].isDestroyed = true
        })
      } else {
        result = AttackResult.HIT
      }
    } else {
      // 未命中
      cell.state = CellState.ATTACKED_MISS
      result = AttackResult.MISS
    }

    // 记录攻击历史
    const attackRecord: AttackRecord = {
      position,
      result,
      timestamp: new Date(),
      targetPlaneId
    }
    board.attackHistory.push(attackRecord)

    return result
  }

  // 检查游戏是否结束
  static isGameOver(board: GameBoard): boolean {
    return board.remainingPlanes === 0
  }

  // 获取对手视图的棋盘(隐藏未被攻击的飞机位置)
  static getOpponentView(board: GameBoard): GameBoard {
    const opponentBoard = JSON.parse(JSON.stringify(board)) as GameBoard

    // 隐藏未被攻击的飞机位置
    for (let x = 0; x < board.size; x++) {
      for (let y = 0; y < board.size; y++) {
        const cell = opponentBoard.cells[x][y]
        if (cell.state === CellState.PLANE_PART && !cell.isDestroyed) {
          cell.state = CellState.EMPTY
          delete cell.planeId
          delete cell.partType
        }
      }
    }

    return opponentBoard
  }

  // 获取棋盘统计信息
  static getBoardStats(board: GameBoard): BoardStats {
    const totalCells = board.size * board.size
    const attackedCells = board.attackHistory.length
    const hitCells = board.attackHistory.filter(a => a.result !== AttackResult.MISS).length
    const accuracy = attackedCells > 0 ? (hitCells / attackedCells * 100) : 0

    return {
      totalCells,
      attackedCells,
      hitCells,
      accuracy: Math.round(accuracy * 100) / 100,
      remainingPlanes: board.remainingPlanes,
      destroyedPlanes: 3 - board.remainingPlanes
    }
  }
}

interface BoardStats {
  totalCells: number
  attackedCells: number
  hitCells: number
  accuracy: number
  remainingPlanes: number
  destroyedPlanes: number
}

3. 游戏状态机

3.1 游戏状态管理

// 游戏状态
interface GameState {
  gameId: string
  roomCode: string
  phase: GamePhase
  players: GamePlayer[]
  currentPlayer: string
  boards: { [playerId: string]: GameBoard }
  gameConfig: GameConfig
  timeState: TimeState
  events: GameEvent[]
  result?: GameResult
}

// 游戏玩家
interface GamePlayer {
  id: string
  nickname: string
  avatar?: string
  isReady: boolean
  isOnline: boolean
  stats: PlayerGameStats
}

// 玩家游戏内统计
interface PlayerGameStats {
  attacksCount: number
  hitsCount: number
  planesDestroyed: number
  accuracy: number
  timeUsed: number
}

// 时间状态
interface TimeState {
  gameStartTime?: Date
  gameEndTime?: Date
  currentTurnStartTime?: Date
  turnTimeLimit: number
  totalTimeLimit: number
  turnTimeRemaining: number
  gameTimeRemaining: number
}

// 游戏事件
interface GameEvent {
  id: string
  type: GameEventType
  playerId: string
  timestamp: Date
  data: any
}

enum GameEventType {
  GAME_STARTED = 'game_started',
  PLANE_PLACED = 'plane_placed',
  PLACEMENT_COMPLETED = 'placement_completed',
  TURN_STARTED = 'turn_started',
  ATTACK_EXECUTED = 'attack_executed',
  PLANE_DESTROYED = 'plane_destroyed',
  TURN_TIMEOUT = 'turn_timeout',
  PLAYER_DISCONNECTED = 'player_disconnected',
  PLAYER_RECONNECTED = 'player_reconnected',
  GAME_ENDED = 'game_ended'
}

3.2 游戏状态机实现

export class GameStateMachine {
  private state: GameState
  private timers: Map<string, NodeJS.Timeout> = new Map()

  constructor(gameState: GameState) {
    this.state = gameState
  }

  // 开始游戏
  startGame(): void {
    if (this.state.phase !== GamePhase.WAITING) {
      throw new Error('游戏状态错误,无法开始游戏')
    }

    this.state.phase = GamePhase.PLACING
    this.state.timeState.gameStartTime = new Date()
    
    // 设置游戏总时长定时器
    this.setGameTimeLimit()

    this.addEvent({
      type: GameEventType.GAME_STARTED,
      playerId: '',
      data: { startTime: this.state.timeState.gameStartTime }
    })
  }

  // 玩家放置飞机
  placePlanes(playerId: string, planes: PlaneShape[]): void {
    if (this.state.phase !== GamePhase.PLACING) {
      throw new Error('当前不是飞机放置阶段')
    }

    const player = this.getPlayer(playerId)
    if (player.isReady) {
      throw new Error('玩家已经完成飞机放置')
    }

    // 放置飞机到棋盘
    const board = this.state.boards[playerId]
    const success = BoardManager.placePlanes(board, planes)
    
    if (!success) {
      throw new Error('飞机放置失败')
    }

    // 标记玩家已准备
    player.isReady = true

    this.addEvent({
      type: GameEventType.PLACEMENT_COMPLETED,
      playerId,
      data: { planes: planes.length }
    })

    // 检查是否所有玩家都已准备
    if (this.allPlayersReady()) {
      this.startBattle()
    }
  }

  // 开始对战阶段
  private startBattle(): void {
    this.state.phase = GamePhase.BATTLING
    
    // 随机选择先手玩家
    const firstPlayer = this.state.players[Math.floor(Math.random() * this.state.players.length)]
    this.state.currentPlayer = firstPlayer.id

    this.startTurn()
  }

  // 开始新回合
  private startTurn(): void {
    this.state.timeState.currentTurnStartTime = new Date()
    this.state.timeState.turnTimeRemaining = this.state.timeState.turnTimeLimit

    // 设置回合时间限制
    this.setTurnTimeLimit()

    this.addEvent({
      type: GameEventType.TURN_STARTED,
      playerId: this.state.currentPlayer,
      data: { timeLimit: this.state.timeState.turnTimeLimit }
    })
  }

  // 执行攻击
  executeAttack(playerId: string, position: Position): AttackResult {
    if (this.state.phase !== GamePhase.BATTLING) {
      throw new Error('当前不是对战阶段')
    }

    if (this.state.currentPlayer !== playerId) {
      throw new Error('不是你的回合')
    }

    // 获取对手棋盘
    const opponentId = this.getOpponent(playerId).id
    const opponentBoard = this.state.boards[opponentId]

    // 执行攻击
    const result = BoardManager.executeAttack(opponentBoard, position)

    // 更新玩家统计
    const player = this.getPlayer(playerId)
    player.stats.attacksCount++
    if (result !== AttackResult.MISS) {
      player.stats.hitsCount++
      player.stats.accuracy = (player.stats.hitsCount / player.stats.attacksCount) * 100
    }
    if (result === AttackResult.DESTROY) {
      player.stats.planesDestroyed++
    }

    this.addEvent({
      type: GameEventType.ATTACK_EXECUTED,
      playerId,
      data: { position, result, opponentId }
    })

    if (result === AttackResult.DESTROY) {
      this.addEvent({
        type: GameEventType.PLANE_DESTROYED,
        playerId: opponentId,
        data: { attackerId: playerId, position }
      })
    }

    // 检查游戏是否结束
    if (BoardManager.isGameOver(opponentBoard)) {
      this.endGame(playerId)
    } else {
      // 切换回合
      this.switchTurn()
    }

    return result
  }

  // 切换回合
  private switchTurn(): void {
    this.clearTurnTimer()
    
    const currentPlayerIndex = this.state.players.findIndex(p => p.id === this.state.currentPlayer)
    const nextPlayerIndex = (currentPlayerIndex + 1) % this.state.players.length
    this.state.currentPlayer = this.state.players[nextPlayerIndex].id

    this.startTurn()
  }

  // 回合超时处理
  private handleTurnTimeout(): void {
    this.addEvent({
      type: GameEventType.TURN_TIMEOUT,
      playerId: this.state.currentPlayer,
      data: { timeUsed: this.state.timeState.turnTimeLimit }
    })

    // 自动跳过回合
    this.switchTurn()
  }

  // 结束游戏
  private endGame(winnerId: string): void {
    this.state.phase = GamePhase.FINISHED
    this.state.timeState.gameEndTime = new Date()
    
    const winner = this.getPlayer(winnerId)
    const loser = this.getOpponent(winnerId)

    this.state.result = {
      winnerId,
      loserId: loser.id,
      winReason: 'ALL_PLANES_DESTROYED',
      gameStats: {
        duration: this.getGameDuration(),
        totalMoves: this.state.events.filter(e => e.type === GameEventType.ATTACK_EXECUTED).length,
        winnerStats: winner.stats,
        loserStats: loser.stats
      }
    }

    // 清除所有定时器
    this.clearAllTimers()

    this.addEvent({
      type: GameEventType.GAME_ENDED,
      playerId: winnerId,
      data: this.state.result
    })
  }

  // 玩家断线处理
  handlePlayerDisconnection(playerId: string): void {
    const player = this.getPlayer(playerId)
    player.isOnline = false

    this.addEvent({
      type: GameEventType.PLAYER_DISCONNECTED,
      playerId,
      data: { timestamp: new Date() }
    })

    // 如果是对战阶段且是当前玩家断线,暂停计时
    if (this.state.phase === GamePhase.BATTLING && this.state.currentPlayer === playerId) {
      this.pauseTurnTimer()
    }
  }

  // 玩家重连处理
  handlePlayerReconnection(playerId: string): void {
    const player = this.getPlayer(playerId)
    player.isOnline = true

    this.addEvent({
      type: GameEventType.PLAYER_RECONNECTED,
      playerId,
      data: { timestamp: new Date() }
    })

    // 如果是对战阶段且是当前玩家重连,恢复计时
    if (this.state.phase === GamePhase.BATTLING && this.state.currentPlayer === playerId) {
      this.resumeTurnTimer()
    }
  }

  // 定时器管理方法
  private setGameTimeLimit(): void {
    const timer = setTimeout(() => {
      this.endGameByTimeout()
    }, this.state.timeState.totalTimeLimit * 1000)
    
    this.timers.set('gameTime', timer)
  }

  private setTurnTimeLimit(): void {
    const timer = setTimeout(() => {
      this.handleTurnTimeout()
    }, this.state.timeState.turnTimeLimit * 1000)
    
    this.timers.set('turnTime', timer)
  }

  private clearTurnTimer(): void {
    const timer = this.timers.get('turnTime')
    if (timer) {
      clearTimeout(timer)
      this.timers.delete('turnTime')
    }
  }

  private clearAllTimers(): void {
    this.timers.forEach(timer => clearTimeout(timer))
    this.timers.clear()
  }

  private pauseTurnTimer(): void {
    // 实现回合计时器暂停逻辑
    this.clearTurnTimer()
  }

  private resumeTurnTimer(): void {
    // 实现回合计时器恢复逻辑
    this.setTurnTimeLimit()
  }

  // 辅助方法
  private getPlayer(playerId: string): GamePlayer {
    const player = this.state.players.find(p => p.id === playerId)
    if (!player) {
      throw new Error('玩家不存在')
    }
    return player
  }

  private getOpponent(playerId: string): GamePlayer {
    const opponent = this.state.players.find(p => p.id !== playerId)
    if (!opponent) {
      throw new Error('对手不存在')
    }
    return opponent
  }

  private allPlayersReady(): boolean {
    return this.state.players.every(p => p.isReady)
  }

  private addEvent(event: Omit<GameEvent, 'id' | 'timestamp'>): void {
    const gameEvent: GameEvent = {
      id: generateId(),
      timestamp: new Date(),
      ...event
    }
    this.state.events.push(gameEvent)
  }

  private getGameDuration(): number {
    if (!this.state.timeState.gameStartTime || !this.state.timeState.gameEndTime) {
      return 0
    }
    return this.state.timeState.gameEndTime.getTime() - this.state.timeState.gameStartTime.getTime()
  }

  private endGameByTimeout(): void {
    // 根据当前分数决定胜负
    const player1 = this.state.players[0]
    const player2 = this.state.players[1]
    
    const player1Score = player1.stats.planesDestroyed
    const player2Score = player2.stats.planesDestroyed
    
    let winnerId: string
    if (player1Score > player2Score) {
      winnerId = player1.id
    } else if (player2Score > player1Score) {
      winnerId = player2.id
    } else {
      // 平局,根据命中率决定
      winnerId = player1.stats.accuracy >= player2.stats.accuracy ? player1.id : player2.id
    }
    
    this.endGame(winnerId)
  }

  // 获取当前游戏状态
  getState(): GameState {
    return { ...this.state }
  }

  // 获取玩家视图的游戏状态
  getPlayerView(playerId: string): any {
    const state = this.getState()
    
    // 隐藏对手棋盘上未被攻击的飞机
    const opponentId = this.getOpponent(playerId).id
    state.boards[opponentId] = BoardManager.getOpponentView(state.boards[opponentId])
    
    return state
  }
}

4. 游戏规则验证

4.1 输入验证器

export class GameValidator {
  // 验证飞机放置是否合法
  static validatePlanesPlacement(planes: PlaneShape[]): ValidationResult {
    const errors: string[] = []

    // 检查飞机数量
    if (planes.length !== 3) {
      errors.push('必须放置3架飞机')
    }

    // 检查每架飞机的合法性
    planes.forEach((plane, index) => {
      // 检查飞机形状是否正确
      if (plane.positions.length !== 11) {
        errors.push(`第${index + 1}架飞机形状不正确`)
      }

      // 检查飞机是否在棋盘范围内
      if (!PlaneGeometry.validatePlanePosition(plane)) {
        errors.push(`第${index + 1}架飞机位置超出棋盘范围`)
      }
    })

    // 检查飞机之间是否重叠
    for (let i = 0; i < planes.length; i++) {
      for (let j = i + 1; j < planes.length; j++) {
        if (PlaneGeometry.checkPlanesOverlap(planes[i], planes[j])) {
          errors.push(`第${i + 1}架和第${j + 1}架飞机位置重叠`)
        }
      }
    }

    return {
      isValid: errors.length === 0,
      errors
    }
  }
}

interface ValidationResult {
  isValid: boolean
  errors: string[]
}