24 KiB
24 KiB
游戏核心逻辑详设文档
文档版本: 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[]
}