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

908 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 游戏核心逻辑详设文档
> **文档版本**: v1.0
> **撰写人**: 游戏逻辑架构师
> **创建日期**: 2024年9月11日
## 1. 游戏逻辑总览
### 1.1 核心游戏机制
基于经典"打飞机"游戏规则,实现回合制战略对战:
```typescript
// 游戏核心参数
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 飞机几何模型
```typescript
// 飞机形状定义
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 棋盘数据结构
```typescript
// 单元格状态
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 棋盘操作类
```typescript
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 游戏状态管理
```typescript
// 游戏状态
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 游戏状态机实现
```typescript
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 输入验证器
```typescript
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[]
}