88 KiB
打飞机小程序完整需求说明书
文档信息
| 项目 | 详情 |
|---|---|
| 项目名称 | 打飞机对战小程序 |
| 文档版本 | v1.0 |
| 创建日期 | 2025年9月 |
| 目标平台 | 微信小程序 / 原生APP |
| 开发语言 | TypeScript + JavaScript |
此文档已经过期失效 参考价值可以忽略
此文档已经过期失效 参考价值可以忽略
1. 项目概述
1.1 项目背景
打飞机是一款经典的双人对战策略游戏,玩家需要在棋盘上布置飞机并猜测对手飞机位置。本项目旨在开发一个现代化的小程序版本,支持人机对战和在线对战功能。
1.2 项目目标
- 提供流畅的游戏体验和直观的用户界面
- 实现智能AI对战系统
- 支持在线实时对战功能
- 建立完整的用户系统和数据统计
- 确保跨平台兼容性和高性能表现
1.3 目标用户群体
- 主要用户: 8-60岁喜欢益智游戏的用户
- 使用场景: 休闲娱乐、朋友对战、碎片时间游戏
- 用户特征: 追求简单易上手但有一定策略性的游戏
2. 功能需求规格
2.1 核心功能模块
2.1.1 用户系统
interface User {
userId: string
nickname: string
avatar: string
level: number
experience: number
winRate: number
totalGames: number
ranking: number
achievements: Achievement[]
createdAt: Date
lastLoginAt: Date
}
interface Achievement {
id: string
name: string
description: string
icon: string
unlockedAt: Date
progress: number
maxProgress: number
}
功能列表:
- 微信登录/游客登录
- 用户资料管理(头像、昵称)
- 成就系统(解锁条件和奖励)
- 好友系统(添加、删除、邀请对战)
- 排行榜(全服排名、好友排名)
2.1.2 游戏核心系统
A. 棋盘系统
interface GameBoard {
size: { width: number, height: number } // 10x10
cells: Cell[][]
coordinateSystem: 'LETTER_NUMBER' // A1-J10
}
interface Cell {
position: Position
state: CellState
isRevealed: boolean
hasPlane: boolean
planePartType?: PlanePartType
attackResult?: AttackResult
}
enum CellState {
EMPTY = 0,
PLANE_PART = 1,
ATTACKED_MISS = 2,
ATTACKED_HIT = 3,
ATTACKED_DESTROYED = 4
}
B. 飞机系统
interface Plane {
id: string
center: Position
direction: Direction
positions: Position[] // 11个位置
isDestroyed: boolean
headPosition: Position
wingPositions: Position[]
bodyPositions: Position[]
tailPositions: Position[]
}
enum Direction {
UP = 'UP',
DOWN = 'DOWN',
LEFT = 'LEFT',
RIGHT = 'RIGHT'
}
interface Position {
x: number // 1-10
y: number // 1-10
coordinate: string // "A_1" 格式
}
C. 游戏状态管理
interface GameState {
gameId: string
gameType: GameType
currentPhase: GamePhase
players: Player[]
currentPlayer: string
gameBoard: {
player1: GameBoard
player2: GameBoard
}
moveHistory: Move[]
startTime: Date
endTime?: Date
winner?: string
gameSettings: GameSettings
}
enum GameType {
AI_BATTLE = 'AI_BATTLE',
ONLINE_BATTLE = 'ONLINE_BATTLE',
LOCAL_BATTLE = 'LOCAL_BATTLE'
}
enum GamePhase {
WAITING = 'WAITING',
PLACING_PLANES = 'PLACING_PLANES',
BATTLING = 'BATTLING',
GAME_OVER = 'GAME_OVER'
}
2.1.3 AI对战系统
A. AI难度等级
interface AIConfig {
level: AILevel
reactionTime: number // ms
mistakeProbability: number // 0-1
strategicDepth: number // 1-5
personalityType: AIPersonality
}
enum AILevel {
BEGINNER = 'BEGINNER', // 初级
INTERMEDIATE = 'INTERMEDIATE', // 中级
ADVANCED = 'ADVANCED', // 高级
EXPERT = 'EXPERT', // 专家
MASTER = 'MASTER' // 大师
}
enum AIPersonality {
AGGRESSIVE = 'AGGRESSIVE', // 激进型
DEFENSIVE = 'DEFENSIVE', // 防守型
ANALYTICAL = 'ANALYTICAL', // 分析型
UNPREDICTABLE = 'UNPREDICTABLE' // 随机型
}
B. AI决策算法接口
interface AIDecisionEngine {
calculatePlacementStrategy(board: GameBoard): PlacementStrategy
selectAttackPosition(gameState: GameState): Position
analyzeOpponentPattern(attackHistory: Move[]): OpponentAnalysis
adaptStrategy(gameResult: GameResult): void
}
interface PlacementStrategy {
planes: PlaneConfiguration[]
confidence: number
reasoning: string[]
}
interface OpponentAnalysis {
predictedPattern: string
riskAreas: Position[]
nextMovePredict: Position[]
confidence: number
}
2.1.4 在线对战系统
A. 房间系统
interface GameRoom {
roomId: string
roomCode: string // 6位数字邀请码
hostUserId: string
guestUserId?: string
gameState: GameState
roomSettings: RoomSettings
createdAt: Date
status: RoomStatus
}
enum RoomStatus {
WAITING = 'WAITING',
IN_GAME = 'IN_GAME',
FINISHED = 'FINISHED',
ABANDONED = 'ABANDONED'
}
interface RoomSettings {
isPrivate: boolean
allowSpectators: boolean
timeLimit: number // 每步时间限制(秒)
gameMode: GameMode
}
B. 实时通信协议
interface GameMessage {
type: MessageType
from: string
to?: string
data: any
timestamp: number
gameId: string
}
enum MessageType {
// 房间管理
JOIN_ROOM = 'JOIN_ROOM',
LEAVE_ROOM = 'LEAVE_ROOM',
ROOM_STATUS_UPDATE = 'ROOM_STATUS_UPDATE',
// 游戏操作
PLACE_PLANE = 'PLACE_PLANE',
PREPARE_ATTACK = 'PREPARE_ATTACK', // 玩家选择但未确认打击的位置
ATTACK_POSITION = 'ATTACK_POSITION',
GAME_STATE_SYNC = 'GAME_STATE_SYNC',
// 系统消息
PLAYER_RECONNECT = 'PLAYER_RECONNECT',
PLAYER_TIMEOUT = 'PLAYER_TIMEOUT',
GAME_END = 'GAME_END'
}
2.2 用户界面需求
2.2.1 界面结构设计
主界面
├── 头部导航
│ ├── 用户头像+昵称
│ └── 设置按钮
├── 游戏模式选择
│ ├── AI对战
│ ├── 在线对战
│ └── 本地对战
├── 功能区域
│ ├── 排行榜
│ ├── 成就系统
│ ├── 游戏记录
│ └── 教程帮助
└── 底部导航
├── 首页
├── 对战
├── 排行
└── 我的
2.2.2 游戏界面布局
interface GameUILayout {
// 棋盘区域 - 占屏幕60%
gameBoard: {
playerBoard: BoardComponent // 己方棋盘
opponentBoard: BoardComponent // 对方棋盘
switchButton: SwitchComponent // 切换视角
}
// 信息面板 - 占屏幕25%
infoPanel: {
playerInfo: PlayerInfoComponent
gameStatus: GameStatusComponent
timer: TimerComponent
moveCounter: MoveCounterComponent
}
// 控制面板 - 占屏幕15%
controlPanel: {
actionButtons: ActionButtonComponent[]
chatBox?: ChatComponent
settingsMenu: SettingsComponent
}
}
2.2.3 交互设计规范
A. 飞机布置交互
- 拖拽模式: 从侧边栏拖拽飞机到棋盘
- 点击模式: 点击棋盘位置,选择飞机方向
- 辅助功能:
- 半透明预览显示
- 红色提示无效位置
- 绿色确认有效位置
- 自动布置功能
B. 攻击操作交互
- 点击攻击: 直接点击对方棋盘位置
- 确认机制: 重要攻击需二次确认
- 视觉反馈:
- 实时瞄准提示: 在对手回合,棋盘上应实时显示对手正在瞄准(已选择但未确认)的格子。
- 攻击动画效果
- 结果显示动画
- 音效配合
2.2.4 响应式设计要求
/* 屏幕适配断点 */
@media (max-width: 375px) {
/* 小屏手机 */
}
@media (min-width: 376px) and (max-width: 414px) {
/* 中屏手机 */
}
@media (min-width: 415px) {
/* 大屏手机/平板 */
}
2.3 性能需求
2.3.1 响应时间要求
| 功能模块 | 响应时间要求 | 备注 |
|---|---|---|
| 小程序启动 | < 3秒 | 首次启动 |
| 页面切换 | < 300ms | 页面路由 |
| AI决策 | < 1秒 | 普通难度 |
| AI决策 | < 3秒 | 最高难度 |
| 网络同步 | < 500ms | 局域网环境 |
| 动画渲染 | 60fps | 流畅动画 |
2.3.2 内存占用限制
- 小程序运行内存: < 10MB
- 图片资源缓存: < 5MB
- 游戏数据缓存: < 2MB
- 总内存使用: < 20MB
2.3.3 网络要求
- 支持弱网络环境(2G/3G)
- 断线重连机制
- 离线模式支持
- 数据压缩传输
3. 技术选型方案
3.1 前端技术栈
3.1.1 框架选择
主框架: Taro 3.x + React 18
// 原因:
// 1. 一套代码多端运行(小程序+H5+APP)
// 2. React生态成熟,组件丰富
// 3. TypeScript支持完善
// 4. 性能优化方案成熟
// 项目结构
src/
├── components/ # 通用组件
├── pages/ # 页面组件
├── hooks/ # 自定义Hooks
├── store/ # 状态管理
├── services/ # API服务
├── utils/ # 工具函数
├── types/ # TypeScript类型
├── assets/ # 静态资源
└── constants/ # 常量配置
3.1.2 状态管理
选择: Zustand + Immer
// 游戏状态Store
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
interface GameStore {
gameState: GameState | null
setGameState: (state: GameState) => void
updatePlayerBoard: (playerId: string, board: GameBoard) => void
addMove: (move: Move) => void
resetGame: () => void
}
export const useGameStore = create<GameStore>()(
immer((set) => ({
gameState: null,
setGameState: (state) => set((draft) => {
draft.gameState = state
}),
updatePlayerBoard: (playerId, board) => set((draft) => {
if (draft.gameState) {
draft.gameState.gameBoard[playerId as keyof typeof draft.gameState.gameBoard] = board
}
}),
addMove: (move) => set((draft) => {
draft.gameState?.moveHistory.push(move)
}),
resetGame: () => set((draft) => {
draft.gameState = null
})
}))
)
3.1.3 UI组件库
选择: Taro UI + 自定义组件
// 自定义游戏棋盘组件
import React from 'react'
import { View } from '@tarojs/components'
import './GameBoard.scss'
interface GameBoardProps {
board: GameBoard
isInteractive: boolean
onCellClick?: (position: Position) => void
showPlanes?: boolean
}
export const GameBoard: React.FC<GameBoardProps> = ({
board,
isInteractive,
onCellClick,
showPlanes = false
}) => {
const renderCell = (cell: Cell) => (
<View
key={`${cell.position.x}-${cell.position.y}`}
className={`cell ${cell.state}`}
onClick={() => isInteractive && onCellClick?.(cell.position)}
>
{renderCellContent(cell)}
</View>
)
return (
<View className="game-board">
<View className="board-grid">
{board.cells.flat().map(renderCell)}
</View>
</View>
)
}
3.2 后端技术架构
3.2.1 服务端框架
选择: Node.js + Express + TypeScript
// 服务器架构
src/
├── controllers/ # 控制器层
├── services/ # 业务逻辑层
├── models/ # 数据模型
├── middleware/ # 中间件
├── routes/ # 路由配置
├── websocket/ # WebSocket处理
├── utils/ # 工具函数
├── config/ # 配置文件
└── types/ # TypeScript类型
// 游戏服务示例
import { GameEngine } from '../services/GameEngine'
import { GameRoom } from '../models/GameRoom'
export class GameController {
private gameEngine: GameEngine
constructor() {
this.gameEngine = new GameEngine()
}
async createRoom(req: Request, res: Response) {
try {
const { userId, settings } = req.body
const room = await this.gameEngine.createRoom(userId, settings)
res.json({
success: true,
data: room
})
} catch (error) {
res.status(500).json({
success: false,
error: error.message
})
}
}
async joinRoom(req: Request, res: Response) {
// 房间加入逻辑
}
async makeMove(req: Request, res: Response) {
// 游戏操作逻辑
}
}
3.2.2 实时通信
选择: Socket.IO
import { Server as SocketServer } from 'socket.io'
import { GameRoomManager } from '../services/GameRoomManager'
export class GameSocketHandler {
private roomManager: GameRoomManager
constructor(io: SocketServer) {
this.roomManager = new GameRoomManager()
this.setupSocketHandlers(io)
}
private setupSocketHandlers(io: SocketServer) {
io.on('connection', (socket) => {
console.log('Client connected:', socket.id)
// 加入房间
socket.on('join-room', async (data) => {
const { roomId, userId } = data
try {
await this.roomManager.joinRoom(roomId, userId)
socket.join(roomId)
// 通知房间其他玩家
socket.to(roomId).emit('player-joined', { userId })
} catch (error) {
socket.emit('error', { message: error.message })
}
})
// 游戏操作
socket.on('game-move', async (data) => {
const { roomId, move } = data
try {
const result = await this.roomManager.processMove(roomId, move)
// 广播游戏状态更新
io.to(roomId).emit('game-state-update', result)
} catch (error) {
socket.emit('error', { message: error.message })
}
})
// 断线处理
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id)
this.roomManager.handlePlayerDisconnect(socket.id)
})
})
}
}
3.2.3 数据库设计
选择: MongoDB + Redis
// MongoDB 数据模型
import mongoose from 'mongoose'
// 用户模型
const UserSchema = new mongoose.Schema({
userId: { type: String, required: true, unique: true },
nickname: { type: String, required: true },
avatar: String,
level: { type: Number, default: 1 },
experience: { type: Number, default: 0 },
statistics: {
totalGames: { type: Number, default: 0 },
wins: { type: Number, default: 0 },
winRate: { type: Number, default: 0 }
},
achievements: [{
achievementId: String,
unlockedAt: Date
}],
createdAt: { type: Date, default: Date.now },
lastLoginAt: { type: Date, default: Date.now }
})
// 游戏记录模型
const GameRecordSchema = new mongoose.Schema({
gameId: { type: String, required: true, unique: true },
gameType: { type: String, enum: ['AI', 'ONLINE', 'LOCAL'] },
players: [{
userId: String,
nickname: String,
result: { type: String, enum: ['WIN', 'LOSE', 'DRAW'] }
}],
gameData: {
moves: [{}],
duration: Number,
placements: {}
},
createdAt: { type: Date, default: Date.now }
})
// Redis 缓存结构
interface RedisGameSession {
gameId: string
roomId: string
gameState: GameState
players: string[]
lastActivity: number
ttl: number // 生存时间
}
3.3 AI算法技术方案
3.3.1 核心算法框架
// AI决策引擎架构
export class AIDecisionEngine {
private difficultyConfig: AIConfig
private probabilityMap: ProbabilityHeatmap
private patternAnalyzer: PatternAnalyzer
private strategySelector: StrategySelector
constructor(difficulty: AILevel) {
this.difficultyConfig = this.loadDifficultyConfig(difficulty)
this.probabilityMap = new ProbabilityHeatmap()
this.patternAnalyzer = new PatternAnalyzer()
this.strategySelector = new StrategySelector()
}
// 飞机布置策略
async generatePlacementStrategy(): Promise<PlacementStrategy> {
const strategies = [
this.generateScatteredPlacement(),
this.generateClusteredPlacement(),
this.generateEdgeAvoidingPlacement(),
this.generateCornerFocusedPlacement()
]
const selectedStrategy = this.strategySelector.selectBestStrategy(
strategies,
this.difficultyConfig
)
return selectedStrategy
}
// 攻击位置选择
async selectAttackPosition(gameState: GameState): Promise<Position> {
// 更新概率地图
this.updateProbabilityMap(gameState)
// 生成候选位置
const candidates = this.generateCandidatePositions(gameState)
// 评估每个位置的价值
const evaluatedPositions = candidates.map(pos => ({
position: pos,
score: this.evaluatePosition(pos, gameState)
}))
// 根据难度选择最终位置
return this.selectFinalPosition(evaluatedPositions)
}
private updateProbabilityMap(gameState: GameState): void {
// 基于贝叶斯推理更新概率分布
for (const move of gameState.moveHistory) {
this.probabilityMap.updateAfterAttack(move.position, move.result)
}
// 模式识别更新
const patterns = this.patternAnalyzer.analyzePatterns(gameState.moveHistory)
this.probabilityMap.applyPatternAdjustments(patterns)
}
}
3.3.2 概率计算算法
// 概率热力图实现
export class ProbabilityHeatmap {
private probabilities: number[][]
private readonly BOARD_SIZE = 10
constructor() {
this.initializeHeatmap()
}
private initializeHeatmap(): void {
this.probabilities = Array(this.BOARD_SIZE + 1).fill(null)
.map(() => Array(this.BOARD_SIZE + 1).fill(0))
// 计算初始概率分布
for (let x = 1; x <= this.BOARD_SIZE; x++) {
for (let y = 1; y <= this.BOARD_SIZE; y++) {
this.probabilities[x][y] = this.calculateInitialProbability(x, y)
}
}
}
private calculateInitialProbability(x: number, y: number): number {
let totalPlacements = 0
let validPlacements = 0
// 计算该位置可能的飞机布置数量
const directions: Direction[] = ['UP', 'DOWN', 'LEFT', 'RIGHT']
const parts: PlanePartType[] = ['HEAD', 'WING', 'BODY', 'TAIL']
for (const direction of directions) {
for (const partType of parts) {
// 计算以当前位置为特定部件的飞机中心位置
const centerPositions = this.getPossibleCenters(x, y, direction, partType)
for (const center of centerPositions) {
totalPlacements++
if (this.isValidPlanePosition(center, direction)) {
validPlacements++
}
}
}
}
return totalPlacements > 0 ? validPlacements / totalPlacements : 0
}
updateAfterAttack(position: Position, result: AttackResult): void {
if (result.type === 'MISS') {
// 该位置及相关联的飞机配置概率置零
this.probabilities[position.x][position.y] = 0
this.propagateMissInformation(position)
} else if (result.type === 'HIT') {
// 提升相邻位置的概率
this.boostAdjacentProbabilities(position)
// 排除不可能的飞机配置
this.eliminateInvalidConfigurations(position, result)
} else if (result.type === 'DESTROYED') {
// 移除已摧毁飞机的所有位置
this.removeDestroyedPlane(position, result.destroyedPlane)
}
// 重新归一化概率分布
this.normalizeProbabilities()
}
getBestAttackPositions(count: number = 5): Position[] {
const candidates: { position: Position, probability: number }[] = []
for (let x = 1; x <= this.BOARD_SIZE; x++) {
for (let y = 1; y <= this.BOARD_SIZE; y++) {
if (this.probabilities[x][y] > 0) {
candidates.push({
position: { x, y, coordinate: `${String.fromCharCode(64 + x)}_${y}` },
probability: this.probabilities[x][y]
})
}
}
}
return candidates
.sort((a, b) => b.probability - a.probability)
.slice(0, count)
.map(c => c.position)
}
}
3.3.3 模式识别算法
// 模式分析器
export class PatternAnalyzer {
private knownPatterns: Map<string, PatternSignature> = new Map()
constructor() {
this.initializeKnownPatterns()
}
analyzePlayerBehavior(gameHistory: GameRecord[]): PlayerBehaviorProfile {
const profile: PlayerBehaviorProfile = {
preferredPlacements: this.analyzePlacementPatterns(gameHistory),
attackStrategies: this.analyzeAttackPatterns(gameHistory),
timingPatterns: this.analyzeTimingPatterns(gameHistory),
riskPreference: this.calculateRiskPreference(gameHistory)
}
return profile
}
private analyzePlacementPatterns(games: GameRecord[]): PlacementPattern[] {
const patterns: PlacementPattern[] = []
for (const game of games) {
const placements = game.gameData.placements
// 分析飞机分布
const distribution = this.calculateDistribution(placements)
// 分析边缘使用
const edgeUsage = this.calculateEdgeUsage(placements)
// 分析对称性
const symmetry = this.calculateSymmetry(placements)
patterns.push({
gameId: game.gameId,
distribution,
edgeUsage,
symmetry,
difficulty: this.classifyDifficulty(distribution, edgeUsage)
})
}
return patterns
}
predictNextMove(moveHistory: Move[], playerProfile: PlayerBehaviorProfile): Position[] {
const predictions: Position[] = []
// 基于历史模式预测
if (playerProfile.attackStrategies.includes('SYSTEMATIC_GRID')) {
predictions.push(...this.predictGridSearchNext(moveHistory))
}
if (playerProfile.attackStrategies.includes('HUNT_AND_TARGET')) {
predictions.push(...this.predictHuntTargetNext(moveHistory))
}
if (playerProfile.attackStrategies.includes('RANDOM_SEARCH')) {
predictions.push(...this.predictRandomNext(moveHistory))
}
return predictions.slice(0, 3) // 返回前3个预测
}
}
4. 核心算法实现详解
4.1 游戏核心逻辑算法
4.1.1 飞机位置生成算法
// 飞机几何模型定义
const PLANE_GEOMETRY: Record<Direction, number[][]> = {
UP: [
[0, -2], // 机头
[-2, -1], [-1, -1], [0, -1], [1, -1], [2, -1], // 机翼
[0, 0], [0, 1], // 机身
[-1, 2], [0, 2], [1, 2] // 机尾
],
DOWN: [
[0, 2], // 机头
[-2, 1], [-1, 1], [0, 1], [1, 1], [2, 1], // 机翼
[0, 0], [0, -1], // 机身
[-1, -2], [0, -2], [1, -2] // 机尾
],
LEFT: [
[-2, 0], // 机头
[-1, -2], [-1, -1], [-1, 0], [-1, 1], [-1, 2], // 机翼
[0, 0], [1, 0], // 机身
[2, -1], [2, 0], [2, 1] // 机尾
],
RIGHT: [
[2, 0], // 机头
[1, -2], [1, -1], [1, 0], [1, 1], [1, 2], // 机翼
[0, 0], [-1, 0], // 机身
[-2, -1], [-2, 0], [-2, 1] // 机尾
]
}
export class PlaneGeometry {
static generatePlanePositions(center: Position, direction: Direction): Position[] {
const offsets =
PLANE_GEOMETRY[direction]
return offsets.map(([dx, dy]) => ({
x: center.x + dx,
y: center.y + dy,
coordinate: `${String.fromCharCode(64 + center.x + dx)}_${center.y + dy}`
}))
}
static validatePlanePosition(center: Position, direction: Direction, boardSize: number): boolean {
const positions = this.generatePlanePositions(center, direction)
// 边界检查
return positions.every(pos =>
pos.x >= 1 && pos.x <= boardSize &&
pos.y >= 1 && pos.y <= boardSize
)
}
static getPlanePartType(position: Position, center: Position, direction: Direction): PlanePartType {
const positions = this.generatePlanePositions(center, direction)
const offsets = PLANE_GEOMETRY[direction]
const index = positions.findIndex(pos =>
pos.x === position.x && pos.y === position.y
)
if (index === 0) return 'HEAD'
if (index >= 1 && index <= 5) return 'WING'
if (index >= 6 && index <= 7) return 'BODY'
if (index >= 8 && index <= 10) return 'TAIL'
throw new Error('Position not part of plane')
}
}
4.1.2 碰撞检测算法
export class CollisionDetector {
private occupancyMap: Set<string> = new Set()
addPlane(plane: Plane): void {
for (const position of plane.positions) {
this.occupancyMap.add(this.positionToKey(position))
}
}
removePlane(plane: Plane): void {
for (const position of plane.positions) {
this.occupancyMap.delete(this.positionToKey(position))
}
}
checkCollision(newPlane: Plane): boolean {
return newPlane.positions.some(pos =>
this.occupancyMap.has(this.positionToKey(pos))
)
}
isPositionOccupied(position: Position): boolean {
return this.occupancyMap.has(this.positionToKey(position))
}
private positionToKey(position: Position): string {
return `${position.x},${position.y}`
}
// 优化的批量检测
checkMultipleCollisions(planes: Plane[]): boolean {
const tempMap = new Set(this.occupancyMap)
for (const plane of planes) {
for (const position of plane.positions) {
const key = this.positionToKey(position)
if (tempMap.has(key)) {
return true // 发现碰撞
}
tempMap.add(key)
}
}
return false
}
}
4.1.3 自动布置算法
export class AutoPlacementEngine {
private collisionDetector: CollisionDetector
private readonly MAX_ATTEMPTS = 1000
constructor() {
this.collisionDetector = new CollisionDetector()
}
generateRandomPlacement(boardSize: number): Plane[] {
const planes: Plane[] = []
this.collisionDetector = new CollisionDetector()
for (let i = 0; i < 3; i++) {
const plane = this.placeSinglePlane(boardSize, planes)
if (plane) {
planes.push(plane)
this.collisionDetector.addPlane(plane)
} else {
throw new Error(`无法布置第${i + 1}架飞机`)
}
}
return planes
}
private placeSinglePlane(boardSize: number, existingPlanes: Plane[]): Plane | null {
const directions: Direction[] = ['UP', 'DOWN', 'LEFT', 'RIGHT']
for (let attempt = 0; attempt < this.MAX_ATTEMPTS; attempt++) {
const center: Position = {
x: Math.floor(Math.random() * boardSize) + 1,
y: Math.floor(Math.random() * boardSize) + 1,
coordinate: ''
}
center.coordinate = `${String.fromCharCode(64 + center.x)}_${center.y}`
const direction = directions[Math.floor(Math.random() * directions.length)]
// 验证位置有效性
if (PlaneGeometry.validatePlanePosition(center, direction, boardSize)) {
const plane = this.createPlane(center, direction)
if (!this.collisionDetector.checkCollision(plane)) {
return plane
}
}
}
return null
}
// 智能布置策略
generateSmartPlacement(boardSize: number, strategy: PlacementStrategy = 'BALANCED'): Plane[] {
switch (strategy) {
case 'DEFENSIVE':
return this.generateDefensivePlacement(boardSize)
case 'AGGRESSIVE':
return this.generateAggressivePlacement(boardSize)
case 'SCATTERED':
return this.generateScatteredPlacement(boardSize)
default:
return this.generateBalancedPlacement(boardSize)
}
}
private generateDefensivePlacement(boardSize: number): Plane[] {
const planes: Plane[] = []
const preferredPositions = this.getDefensivePositions(boardSize)
// 优先选择边角和边缘位置
for (const position of preferredPositions) {
const directions: Direction[] = ['UP', 'DOWN', 'LEFT', 'RIGHT']
for (const direction of directions) {
if (PlaneGeometry.validatePlanePosition(position, direction, boardSize)) {
const plane = this.createPlane(position, direction)
if (!this.collisionDetector.checkCollision(plane)) {
planes.push(plane)
this.collisionDetector.addPlane(plane)
break
}
}
}
if (planes.length === 3) break
}
return planes
}
private getDefensivePositions(boardSize: number): Position[] {
const positions: Position[] = []
// 边角位置 (优先级最高)
const corners = [
{ x: 3, y: 3 }, { x: 8, y: 3 },
{ x: 3, y: 8 }, { x: 8, y: 8 }
]
// 边缘位置
for (let i = 3; i <= 8; i++) {
positions.push(
{ x: i, y: 3, coordinate: '' }, // 上边缘
{ x: i, y: 8, coordinate: '' }, // 下边缘
{ x: 3, y: i, coordinate: '' }, // 左边缘
{ x: 8, y: i, coordinate: '' } // 右边缘
)
}
return [...corners, ...positions].map(pos => ({
...pos,
coordinate: `${String.fromCharCode(64 + pos.x)}_${pos.y}`
}))
}
private createPlane(center: Position, direction: Direction): Plane {
const positions = PlaneGeometry.generatePlanePositions(center, direction)
const headPosition = positions[0] // 机头始终是第一个位置
return {
id: this.generatePlaneId(),
center,
direction,
positions,
isDestroyed: false,
headPosition,
wingPositions: positions.slice(1, 6),
bodyPositions: positions.slice(6, 8),
tailPositions: positions.slice(8, 11)
}
}
private generatePlaneId(): string {
return `plane_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
}
4.2 AI决策算法
4.2.1 蒙特卡洛树搜索(MCTS)
export class MCTSEngine {
private readonly EXPLORATION_CONSTANT = Math.sqrt(2)
private readonly MAX_ITERATIONS = 1000
private readonly MAX_SIMULATION_DEPTH = 50
selectBestMove(gameState: GameState): Position {
const rootNode = new MCTSNode(gameState, null, null)
for (let i = 0; i < this.MAX_ITERATIONS; i++) {
// 1. 选择
const leafNode = this.selectLeaf(rootNode)
// 2. 扩展
const newNode = this.expand(leafNode)
// 3. 模拟
const result = this.simulate(newNode || leafNode)
// 4. 回传
this.backpropagate(newNode || leafNode, result)
}
// 选择最佳子节点
return this.getBestChild(rootNode).move!
}
private selectLeaf(node: MCTSNode): MCTSNode {
let current = node
while (!current.isLeaf() && !current.gameState.isTerminal) {
current = this.getBestUCTChild(current)
}
return current
}
private getBestUCTChild(node: MCTSNode): MCTSNode {
let bestChild: MCTSNode | null = null
let bestValue = -Infinity
for (const child of node.children) {
const uctValue = this.calculateUCT(child, node.visits)
if (uctValue > bestValue) {
bestValue = uctValue
bestChild = child
}
}
return bestChild!
}
private calculateUCT(node: MCTSNode, parentVisits: number): number {
if (node.visits === 0) return Infinity
const exploitation = node.wins / node.visits
const exploration = this.EXPLORATION_CONSTANT *
Math.sqrt(Math.log(parentVisits) / node.visits)
return exploitation + exploration
}
private expand(node: MCTSNode): MCTSNode | null {
if (node.gameState.isTerminal) return null
const availableMoves = this.getAvailableMoves(node.gameState)
const untriedMoves = availableMoves.filter(move =>
!node.children.some(child => this.movesEqual(child.move!, move))
)
if (untriedMoves.length === 0) return null
// 选择随机未尝试的移动
const randomMove = untriedMoves[Math.floor(Math.random() * untriedMoves.length)]
const newGameState = this.applyMove(node.gameState, randomMove)
const newNode = new MCTSNode(newGameState, node, randomMove)
node.addChild(newNode)
return newNode
}
private simulate(node: MCTSNode): GameResult {
let currentState = { ...node.gameState }
let depth = 0
while (!currentState.isTerminal && depth < this.MAX_SIMULATION_DEPTH) {
const availableMoves = this.getAvailableMoves(currentState)
const randomMove = availableMoves[Math.floor(Math.random() * availableMoves.length)]
currentState = this.applyMove(currentState, randomMove)
depth++
}
return this.evaluateGameState(currentState)
}
private backpropagate(node: MCTSNode, result: GameResult): void {
let current: MCTSNode | null = node
while (current !== null) {
current.visits++
if (result.winner === current.gameState.currentPlayer) {
current.wins++
}
current = current.parent
}
}
}
class MCTSNode {
public visits: number = 0
public wins: number = 0
public children: MCTSNode[] = []
constructor(
public gameState: GameState,
public parent: MCTSNode | null,
public move: Position | null
) {}
isLeaf(): boolean {
return this.children.length === 0
}
addChild(child: MCTSNode): void {
this.children.push(child)
}
}
4.2.2 神经网络评估函数
export class NeuralNetworkEvaluator {
private model: any // 实际使用TensorFlow.js或其他ML库
private featureExtractor: FeatureExtractor
constructor() {
this.featureExtractor = new FeatureExtractor()
this.loadModel()
}
async evaluatePosition(gameState: GameState, position: Position): Promise<number> {
const features = this.featureExtractor.extractFeatures(gameState, position)
const normalized = this.normalizeFeatures(features)
const prediction = await this.model.predict(normalized)
return prediction[0] // 返回概率值
}
async evaluatePlacement(planes: Plane[]): Promise<number> {
const features = this.featureExtractor.extractPlacementFeatures(planes)
const normalized = this.normalizeFeatures(features)
const prediction = await this.model.predict(normalized)
return prediction[0] // 返回布置质量评分
}
private loadModel(): void {
// 加载预训练的神经网络模型
// 实际实现中需要使用TensorFlow.js等库
}
}
export class FeatureExtractor {
extractFeatures(gameState: GameState, position: Position): number[] {
const features: number[] = []
// 位置特征
features.push(
position.x / 10, // 归一化x坐标
position.y / 10, // 归一化y坐标
this.getDistanceToCenter(position), // 到中心距离
this.getDistanceToEdge(position) // 到边缘距离
)
// 邻域特征
const neighbors = this.getNeighborStates(gameState, position)
features.push(...neighbors)
// 历史攻击特征
const attackDensity = this.calculateAttackDensity(gameState, position)
features.push(attackDensity)
// 模式特征
const patternFeatures = this.extractPatternFeatures(gameState, position)
features.push(...patternFeatures)
return features
}
extractPlacementFeatures(planes: Plane[]): number[] {
const features: number[] = []
// 分散度特征
const dispersion = this.calculateDispersion(planes)
features.push(dispersion)
// 边缘使用特征
const edgeUsage = this.calculateEdgeUsage(planes)
features.push(edgeUsage)
// 对称性特征
const symmetry = this.calculateSymmetry(planes)
features.push(symmetry)
// 覆盖度特征
const coverage = this.calculateCoverage(planes)
features.push(coverage)
return features
}
private getDistanceToCenter(position: Position): number {
const center = { x: 5.5, y: 5.5 }
const dx = position.x - center.x
const dy = position.y - center.y
return Math.sqrt(dx * dx + dy * dy) / 7.07 // 归一化到[0,1]
}
private getDistanceToEdge(position: Position): number {
const distances = [
position.x - 1, // 左边缘
10 - position.x, // 右边缘
position.y - 1, // 上边缘
10 - position.y // 下边缘
]
return Math.min(...distances) / 10 // 归一化
}
}
4.3 网络同步算法
4.3.1 状态同步协议
export interface GameSyncProtocol {
// 状态同步消息
syncGameState(gameState: GameState): void
requestStateSync(): void
// 增量更新
sendDelta(delta: GameStateDelta): void
applyDelta(delta: GameStateDelta): void
// 冲突解决
resolveConflict(localState: GameState, remoteState: GameState): GameState
}
export class GameStateSynchronizer implements GameSyncProtocol {
private localState: GameState
private lastSyncTimestamp: number = 0
private pendingDeltas: GameStateDelta[] = []
constructor(initialState: GameState) {
this.localState = initialState
}
syncGameState(gameState: GameState): void {
// 检查时间戳,防止过期状态
if (gameState.timestamp <= this.lastSyncTimestamp) {
return
}
// 应用远程状态
this.localState = this.mergeStates(this.localState, gameState)
this.lastSyncTimestamp = gameState.timestamp
// 清除已应用的增量更新
this.clearAppliedDeltas(gameState.timestamp)
}
sendDelta(delta: GameStateDelta): void {
// 记录本地更改
this.pendingDeltas.push(delta)
// 应用到本地状态
this.applyDelta(delta)
// 发送给其他玩家
this.broadcastDelta(delta)
}
applyDelta(delta: GameStateDelta): void {
switch (delta.type) {
case 'ATTACK':
this.applyAttackDelta(delta)
break
case 'PLACE_PLANE':
this.applyPlacementDelta(delta)
break
case 'GAME_PHASE_CHANGE':
this.applyPhaseChangeDelta(delta)
break
}
}
resolveConflict(localState: GameState, remoteState: GameState): GameState {
// 基于时间戳的简单冲突解决
if (remoteState.timestamp > localState.timestamp) {
return remoteState
}
// 如果时间戳相同,使用更详细的冲突解决策略
if (remoteState.timestamp === localState.timestamp) {
return this.mergeConflictingStates(localState, remoteState)
}
return localState
}
private mergeStates(local: GameState, remote: GameState): GameState {
// 合并两个游戏状态,优先使用更新的数据
return {
...local,
...remote,
timestamp: Math.max(local.timestamp, remote.timestamp),
moveHistory: this.mergeMoveHistory(local.moveHistory, remote.moveHistory)
}
}
private mergeMoveHistory(localHistory: Move[], remoteHistory: Move[]): Move[] {
const combined = [...localHistory, ...remoteHistory]
// 去重并按时间戳排序
const unique = combined.filter((move, index, arr) =>
arr.findIndex(m => m.id === move.id) === index
)
return unique.sort((a, b) => a.timestamp - b.timestamp)
}
}
export interface GameStateDelta {
id: string
type: DeltaType
timestamp: number
playerId: string
data: any
checksum: string
}
enum DeltaType {
ATTACK = 'ATTACK',
PLACE_PLANE = 'PLACE_PLANE',
GAME_PHASE_CHANGE = 'GAME_PHASE_CHANGE',
PLAYER_JOIN = 'PLAYER_JOIN',
PLAYER_LEAVE = 'PLAYER_LEAVE'
}
4.3.2 断线重连机制
export class ReconnectionManager {
private connectionState: ConnectionState = ConnectionState.CONNECTED
private reconnectAttempts: number = 0
private maxReconnectAttempts: number = 5
private baseDelay: number = 1000
private maxDelay: number = 30000
private gameStateSyncQueue: GameStateDelta[] = []
private heartbeatInterval: NodeJS.Timeout | null = null
async handleDisconnection(): Promise<void> {
this.connectionState = ConnectionState.DISCONNECTED
this.stopHeartbeat()
// 开始重连流程
await this.attemptReconnection()
}
private async attemptReconnection(): Promise<void> {
while (
this.reconnectAttempts < this.maxReconnectAttempts &&
this.connectionState !== ConnectionState.CONNECTED
) {
this.connectionState = ConnectionState.RECONNECTING
const delay = Math.min(
this.baseDelay * Math.pow(2, this.reconnectAttempts),
this.maxDelay
)
await this.delay(delay)
try {
await this.connect()
await this.syncAfterReconnection()
this.connectionState = ConnectionState.CONNECTED
this.reconnectAttempts = 0
this.startHeartbeat()
} catch (error) {
this.reconnectAttempts++
console.warn(`重连失败 (${this.reconnectAttempts}/${this.maxReconnectAttempts}):`, error)
}
}
if (this.connectionState !== ConnectionState.CONNECTED) {
this.connectionState = ConnectionState.FAILED
throw new Error('重连失败,超过最大重试次数')
}
}
private async syncAfterReconnection(): Promise<void> {
// 请求完整的游戏状态同步
const currentGameState = await this.requestFullGameState()
// 应用断线期间可能错过的更新
await this.applyMissedUpdates(currentGameState)
// 重新发送断线期间的本地操作
await this.resendPendingOperations()
}
private startHeartbeat(): void {
this.heartbeatInterval = setInterval(() => {
this.sendHeartbeat().catch(() => {
this.handleDisconnection()
})
}, 10000) // 10秒心跳
}
private stopHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
this.heartbeatInterval = null
}
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
enum ConnectionState {
CONNECTED = 'CONNECTED',
DISCONNECTED = 'DISCONNECTED',
RECONNECTING = 'RECONNECTING',
FAILED = 'FAILED'
}
5. 数据存储设计
5.1 数据库表结构
5.1.1 用户相关表
-- 用户基本信息表
CREATE TABLE users (
user_id VARCHAR(50) PRIMARY KEY,
nickname VARCHAR(100) NOT NULL,
avatar_url VARCHAR(500),
level INT DEFAULT 1,
experience INT DEFAULT 0,
total_games INT DEFAULT 0,
total_wins INT DEFAULT 0,
win_rate DECIMAL(5,4) DEFAULT 0.0000,
ranking INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
last_login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ranking (ranking),
INDEX idx_level (level),
INDEX idx_last_login (last_login_at)
);
-- 用户成就表
CREATE TABLE user_achievements (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id VARCHAR(50) NOT NULL,
achievement_id VARCHAR(100) NOT NULL,
progress INT DEFAULT 0,
max_progress INT NOT NULL,
is_unlocked BOOLEAN DEFAULT FALSE,
unlocked_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(user_id),
UNIQUE KEY uk_user_achievement (user_id, achievement_id),
INDEX idx_user_unlocked (user_id, is_unlocked)
);
-- 好友关系表
CREATE TABLE friendships (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
requester_id VARCHAR(50) NOT NULL,
addressee_id VARCHAR(50) NOT NULL,
status ENUM('PENDING', 'ACCEPTED', 'BLOCKED') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (requester_id) REFERENCES users(user_id),
FOREIGN KEY (addressee_id) REFERENCES users(user_id),
UNIQUE KEY uk_friendship (requester_id, addressee_id),
INDEX idx_status (status)
);
5.1.2 游戏相关表
-- 游戏记录表
CREATE TABLE game_records (
game_id VARCHAR(50) PRIMARY KEY,
game_type ENUM('AI', 'ONLINE', 'LOCAL') NOT NULL,
game_mode VARCHAR(50) DEFAULT 'STANDARD',
status ENUM('IN_PROGRESS', 'COMPLETED', 'ABANDONED') NOT NULL,
winner_id VARCHAR(50),
total_moves INT DEFAULT 0,
game_duration INT DEFAULT 0, -- 秒
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL,
INDEX idx_game_type (game_type),
INDEX idx_status (status),
INDEX idx_created_at (created_at),
FOREIGN KEY (winner_id) REFERENCES users(user_id)
);
-- 游戏参与者表
CREATE TABLE game_players (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
game_id VARCHAR(50) NOT NULL,
user_id VARCHAR(50),
player_type ENUM('HUMAN', 'AI') NOT NULL,
ai_difficulty ENUM('BEGINNER', 'INTERMEDIATE', 'ADVANCED', 'EXPERT', 'MASTER') NULL,
player_index TINYINT NOT NULL, -- 1 or 2
result ENUM('WIN', 'LOSE', 'DRAW') NULL,
moves_made INT DEFAULT 0,
planes_destroyed INT DEFAULT 0,
accuracy_rate DECIMAL(5,4) DEFAULT 0.0000,
FOREIGN KEY (game_id) REFERENCES game_records(game_id),
FOREIGN KEY (user_id) REFERENCES users(user_id),
UNIQUE KEY uk_game_player (game_id, player_index)
);
-- 游戏操作记录表
CREATE TABLE game_moves (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
game_id VARCHAR(50) NOT NULL,
player_id VARCHAR(50) NOT NULL,
move_sequence INT NOT NULL,
move_type ENUM('PLACE_PLANE', 'ATTACK') NOT NULL,
position_x TINYINT NOT NULL,
position_y TINYINT NOT NULL,
result_type ENUM('MISS', 'HIT', 'DESTROYED') NULL,
plane_id VARCHAR(50) NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (game_id) REFERENCES game_records(game_id),
INDEX idx_game_sequence (game_id, move_sequence),
INDEX idx_timestamp (timestamp)
);
-- 飞机布置记录表
CREATE TABLE plane_placements (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
game_id VARCHAR(50) NOT NULL,
player_id VARCHAR(50) NOT NULL,
plane_id VARCHAR(50) NOT NULL,
center_x TINYINT NOT NULL,
center_y TINYINT NOT NULL,
direction ENUM('UP', 'DOWN', 'LEFT', 'RIGHT') NOT NULL,
is_destroyed BOOLEAN DEFAULT FALSE,
destroyed_at_move INT NULL,
FOREIGN KEY (game_id) REFERENCES game_records(game_id),
UNIQUE KEY uk_game_player_plane (game_id, player_id, plane_id)
);
5.2 缓存设计
5.2.1 Redis 缓存结构
// 游戏会话缓存
interface GameSessionCache {
key: string // `game_session:${gameId}`
data: {
gameState: GameState
playerIds: string[]
lastActivity: number
roomCode?: string
}
ttl: number // 3600 seconds (1 hour)
}
// 用户在线状态缓存
interface UserOnlineCache {
key: string // `user_online:${userId}`
data: {
isOnline: boolean
lastSeen: number
currentGameId?: string
socketId?: string
}
ttl: number // 1800 seconds (30 minutes)
}
// 房间匹配缓存
interface RoomMatchmakingCache {
key: string // `room_queue:${difficulty}`
data: {
waitingPlayers: Array<{
userId: string
joinedAt: number
preferences: MatchmakingPreferences
}>
}
ttl: number // 300 seconds (5 minutes)
}
// 排行榜缓存
interface LeaderboardCache {
key: string // `leaderboard:${type}:${timeframe}`
data: Array<{
userId: string
nickname: string
score: number
rank: number
}>
ttl: number // 3600 seconds (1 hour)
}
5.2.2 缓存操作类
export class GameCacheManager {
private redis: Redis
constructor(redisClient: Redis) {
this.redis = redisClient
}
async cacheGameSession(gameId: string, gameState: GameState): Promise<void> {
const key = `game_session:${gameId}`
const data = {
gameState,
playerIds: gameState.players.map(p => p.userId),
lastActivity: Date.now(),
roomCode: gameState.roomCode
}
await this.redis.
setex(key, 3600, JSON.stringify(data))
}
async getGameSession(gameId: string): Promise<GameState | null> {
const key = `game_session:${gameId}`
const cached = await this.redis.get(key)
if (cached) {
const data = JSON.parse(cached)
return data.gameState
}
return null
}
async updateUserOnlineStatus(userId: string, isOnline: boolean): Promise<void> {
const key = `user_online:${userId}`
const data = {
isOnline,
lastSeen: Date.now(),
socketId: isOnline ? this.getCurrentSocketId(userId) : undefined
}
await this.redis.setex(key, 1800, JSON.stringify(data))
}
async cacheLeaderboard(type: string, timeframe: string, data: any[]): Promise<void> {
const key = `leaderboard:${type}:${timeframe}`
await this.redis.setex(key, 3600, JSON.stringify(data))
}
async getCachedLeaderboard(type: string, timeframe: string): Promise<any[] | null> {
const key = `leaderboard:${type}:${timeframe}`
const cached = await this.redis.get(key)
return cached ? JSON.parse(cached) : null
}
private getCurrentSocketId(userId: string): string | undefined {
// 获取用户当前的Socket连接ID
return undefined // 实现细节
}
}
6. 安全性设计
6.1 数据安全
6.1.1 输入验证
export class InputValidator {
static validatePosition(position: Position): ValidationResult {
const errors: string[] = []
if (position.x < 1 || position.x > 10) {
errors.push('X坐标必须在1-10范围内')
}
if (position.y < 1 || position.y > 10) {
errors.push('Y坐标必须在1-10范围内')
}
const coordinatePattern = /^[A-J]_([1-9]|10)$/
if (!coordinatePattern.test(position.coordinate)) {
errors.push('坐标格式不正确')
}
return {
isValid: errors.length === 0,
errors
}
}
static validateGameMove(move: GameMove, gameState: GameState): ValidationResult {
const errors: string[] = []
// 验证玩家权限
if (move.playerId !== gameState.currentPlayer) {
errors.push('不是当前玩家的回合')
}
// 验证游戏阶段
if (gameState.currentPhase !== GamePhase.BATTLING) {
errors.push('当前游戏阶段不允许攻击')
}
// 验证位置是否已被攻击
const isAlreadyAttacked = gameState.moveHistory.some(
historyMove =>
historyMove.position.x === move.position.x &&
historyMove.position.y === move.position.y
)
if (isAlreadyAttacked) {
errors.push('该位置已被攻击过')
}
// 验证位置
const positionValidation = this.validatePosition(move.position)
errors.push(...positionValidation.errors)
return {
isValid: errors.length === 0,
errors
}
}
static sanitizeUserInput(input: string): string {
return input
.trim()
.replace(/[<>\"'&]/g, '') // 移除潜在的XSS字符
.substring(0, 100) // 限制长度
}
}
interface ValidationResult {
isValid: boolean
errors: string[]
}
6.1.2 反作弊系统
export class AntiCheatSystem {
private suspiciousActivityDetector: SuspiciousActivityDetector
private gameIntegrityChecker: GameIntegrityChecker
constructor() {
this.suspiciousActivityDetector = new SuspiciousActivityDetector()
this.gameIntegrityChecker = new GameIntegrityChecker()
}
validateGameAction(action: GameAction, context: GameContext): AntiCheatResult {
const checks: AntiCheatCheck[] = [
this.checkActionTiming(action, context),
this.checkActionSequence(action, context),
this.checkPlayerBehavior(action, context),
this.checkClientIntegrity(action, context)
]
const failures = checks.filter(check => !check.passed)
return {
isValid: failures.length === 0,
riskScore: this.calculateRiskScore(failures),
violations: failures.map(f => f.violation),
action: failures.length > 0 ? this.determineAction(failures) : 'ALLOW'
}
}
private checkActionTiming(action: GameAction, context: GameContext): AntiCheatCheck {
const timeSinceLastAction = action.timestamp - context.lastActionTimestamp
const minHumanReactionTime = 200 // 毫秒
const maxReasonableThinkTime = 300000 // 5分钟
if (timeSinceLastAction < minHumanReactionTime) {
return {
passed: false,
violation: 'INHUMAN_REACTION_TIME',
severity: 'HIGH',
details: `动作间隔过短: ${timeSinceLastAction}ms`
}
}
if (timeSinceLastAction > maxReasonableThinkTime) {
return {
passed: false,
violation: 'SUSPICIOUS_DELAY',
severity: 'LOW',
details: `动作间隔过长: ${timeSinceLastAction}ms`
}
}
return { passed: true, violation: 'NONE', severity: 'NONE' }
}
private checkActionSequence(action: GameAction, context: GameContext): AntiCheatCheck {
// 检查动作序列的合理性
const recentActions = context.actionHistory.slice(-10)
// 检查是否有不自然的攻击模式
if (this.detectUnhumanAttackPattern(recentActions)) {
return {
passed: false,
violation: 'PATTERN_ANALYSIS_FAIL',
severity: 'MEDIUM',
details: '检测到非人类攻击模式'
}
}
return { passed: true, violation: 'NONE', severity: 'NONE' }
}
private detectUnhumanAttackPattern(actions: GameAction[]): boolean {
if (actions.length < 5) return false
// 检查过度规律的攻击模式
const positions = actions.map(a => a.position)
const intervals = this.calculateIntervals(positions)
// 如果攻击位置间隔过于规律,可能是脚本行为
const variance = this.calculateVariance(intervals)
return variance < 0.1 // 方差过小表示过于规律
}
private calculateRiskScore(failures: AntiCheatCheck[]): number {
let score = 0
for (const failure of failures) {
switch (failure.severity) {
case 'LOW': score += 1; break
case 'MEDIUM': score += 3; break
case 'HIGH': score += 5; break
}
}
return Math.min(score, 10) // 最高10分
}
}
interface AntiCheatCheck {
passed: boolean
violation: string
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'NONE'
details?: string
}
interface AntiCheatResult {
isValid: boolean
riskScore: number
violations: string[]
action: 'ALLOW' | 'WARNING' | 'RESTRICT' | 'BAN'
}
6.2 网络安全
6.2.1 API 安全
export class APISecurityMiddleware {
private rateLimiter: RateLimiter
private tokenValidator: TokenValidator
constructor() {
this.rateLimiter = new RateLimiter({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100 // 每个IP最多100次请求
})
this.tokenValidator = new TokenValidator()
}
async validateRequest(req: Request): Promise<SecurityValidationResult> {
// 1. 速率限制检查
const rateLimitResult = await this.rateLimiter.checkLimit(req.ip)
if (!rateLimitResult.allowed) {
return {
isValid: false,
reason: 'RATE_LIMIT_EXCEEDED',
statusCode: 429
}
}
// 2. Token验证
const token = this.extractToken(req)
if (!token) {
return {
isValid: false,
reason: 'MISSING_TOKEN',
statusCode: 401
}
}
const tokenValidation = await this.tokenValidator.validate(token)
if (!tokenValidation.isValid) {
return {
isValid: false,
reason: 'INVALID_TOKEN',
statusCode: 401
}
}
// 3. 请求签名验证
const signatureValidation = this.validateSignature(req)
if (!signatureValidation.isValid) {
return {
isValid: false,
reason: 'INVALID_SIGNATURE',
statusCode: 400
}
}
return {
isValid: true,
userId: tokenValidation.userId
}
}
private extractToken(req: Request): string | null {
const authHeader = req.headers.authorization
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.substring(7)
}
return null
}
private validateSignature(req: Request): { isValid: boolean } {
const signature = req.headers['x-signature'] as string
const timestamp = req.headers['x-timestamp'] as string
const body = JSON.stringify(req.body)
if (!signature || !timestamp) {
return { isValid: false }
}
// 时间戳检查(防重放攻击)
const now = Date.now()
const requestTime = parseInt(timestamp)
if (Math.abs(now - requestTime) > 300000) { // 5分钟有效期
return { isValid: false }
}
// 签名验证
const expectedSignature = this.generateSignature(body, timestamp)
return { isValid: signature === expectedSignature }
}
private generateSignature(body: string, timestamp: string): string {
const crypto = require('crypto')
const secretKey = process.env.API_SECRET_KEY
const data = `${timestamp}.${body}`
return crypto
.createHmac('sha256', secretKey)
.update(data)
.digest('hex')
}
}
interface SecurityValidationResult {
isValid: boolean
reason?: string
statusCode?: number
userId?: string
}
6.2.2 WebSocket 安全
export class WebSocketSecurityManager {
private connectionLimiter: Map<string, number> = new Map()
private blacklistedIPs: Set<string> = new Set()
validateConnection(socket: any, request: any): ConnectionValidationResult {
const ip = this.getClientIP(request)
// IP黑名单检查
if (this.blacklistedIPs.has(ip)) {
return {
allowed: false,
reason: 'IP_BLACKLISTED'
}
}
// 连接数限制
const currentConnections = this.connectionLimiter.get(ip) || 0
if (currentConnections >= 5) { // 每个IP最多5个连接
return {
allowed: false,
reason: 'TOO_MANY_CONNECTIONS'
}
}
// Token验证
const token = this.extractTokenFromSocket(socket)
if (!token) {
return {
allowed: false,
reason: 'MISSING_AUTH_TOKEN'
}
}
return {
allowed: true,
ip,
token
}
}
onConnection(socket: any, ip: string): void {
// 增加连接计数
const current = this.connectionLimiter.get(ip) || 0
this.connectionLimiter.set(ip, current + 1)
// 设置连接超时
socket.setTimeout(60000) // 60秒无活动则断开
// 监听消息频率
let messageCount = 0
const messageWindow = setInterval(() => {
if (messageCount > 100) { // 每秒超过100条消息
socket.close(1008, 'Message rate too high')
}
messageCount = 0
}, 1000)
socket.on('message', () => {
messageCount++
})
socket.on('close', () => {
this.onDisconnection(ip)
clearInterval(messageWindow)
})
}
private onDisconnection(ip: string): void {
const current = this.connectionLimiter.get(ip) || 0
if (current <= 1) {
this.connectionLimiter.delete(ip)
} else {
this.connectionLimiter.set(ip, current - 1)
}
}
private getClientIP(request: any): string {
return request.headers['x-forwarded-for'] ||
request.connection.remoteAddress ||
request.socket.remoteAddress
}
private extractTokenFromSocket(socket: any): string | null {
// 从WebSocket握手中提取认证token
const url = new URL(socket.url, 'http://localhost')
return url.searchParams.get('token')
}
}
interface ConnectionValidationResult {
allowed: boolean
reason?: string
ip?: string
token?: string
}
7. 部署与运维
7.1 系统架构图
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 小程序客户端 │ │ H5客户端 │ │ 原生APP客户端 │
└─────────┬───────┘ └─────────┬───────┘ └─────────┬───────┘
│ │ │
└──────────────────────┼──────────────────────┘
│
┌─────────────▼──────────────┐
│ 负载均衡器 (Nginx) │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ API网关服务 │
│ - 认证鉴权 │
│ - 请求路由 │
│ - 限流熔断 │
└─────────────┬──────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌─────────▼─────────┐ ┌────────▼────────┐ ┌────────▼────────┐
│ 游戏服务集群 │ │ 用户服务 │ │ 匹配服务 │
│ - 游戏逻辑处理 │ │ - 用户管理 │ │ - 房间匹配 │
│ - 实时通信 │ │ - 好友系统 │ │ - 排行榜 │
│ - AI决策 │ │ - 成就系统 │ │ - 数据统计 │
└─────────┬─────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└────────────────────┼────────────────────┘
│
┌─────────────▼──────────────┐
│ 数据存储层 │
│ ┌─────────┐ ┌─────────────┐│
│ │ MongoDB │ │ Redis ││
│ │ 主数据库 │ │ 缓存 ││
│ └─────────┘ └─────────────┘│
└────────────────────────────┘
7.2 部署配置
7.2.1 Docker 配置
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:18-alpine AS production
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --chown=nextjs:nodejs . .
USER nextjs
EXPOSE 3000
ENV NODE_ENV production
CMD ["node", "dist/server.js"]
# docker-compose.yml
version: '3.8'
services:
game-api:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=mongodb
- REDIS_HOST=redis
- JWT_SECRET=${JWT_SECRET}
depends_on:
- mongodb
- redis
restart: unless-stopped
mongodb:
image: mongo:6.0
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
volumes:
- mongo_data:/data/db
restart: unless-stopped
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/ssl
depends_on:
- game-api
restart: unless-stopped
volumes:
mongo_data:
redis_data:
7.2.2 Nginx 配置
# nginx.conf
events {
worker_connections 1024;
}
http {
upstream api_servers {
least_conn;
server game-api:3000 max_fails=3 fail_timeout=30s;
# 可以添加更多服务器实例
# server game-api-2:3000 max_fails=3 fail_timeout=30s;
}
# 限流配置
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=ws:10m rate=5r/s;
server {
listen 80;
server_name your-domain.com;
# HTTP重定向到HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# API请求
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://api_servers;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# WebSocket连接
location /socket.io/ {
limit_req zone=ws burst=10 nodelay;
proxy_pass http://api_servers;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket超时设置
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# 静态资源
location /static/ {
expires 30d;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options nosniff;
}
# 健康检查
location /health {
access_log off;
proxy_pass http://api_servers;
}
}
}
7.3 监控与日志
7.3.1 应用监控
// 监控指标收集
export class MetricsCollector {
private metrics: Map<string, number> = new Map()
incrementCounter(name: string, tags?: Record<string, string>): void {
const key = this.buildMetricKey(name, tags)
const current = this.metrics.get(key) || 0
this.metrics.set(key, current + 1)
}
recordHistogram(name: string, value: number, tags?: Record<string, string>): void {
const key = this.buildMetricKey(name, tags)
// 实际实现中可以使用更复杂的直方图数据结构
this.metrics.set(`${key}.sum`, (this.metrics.get(`${key}.sum`) || 0) + value)
this.metrics.set(`${key}.count`, (this.metrics.get(`${key}.count`) || 0) + 1)
}
getMetrics(): Record<string, number> {
return Object.fromEntries(this.metrics)
}
// 游戏特定指标
recordGameStarted(gameType: string): void {
this.incrementCounter('games.started', { type: gameType })
}
recordGameCompleted(gameType: string, duration: number): void {
this.incrementCounter('games.completed', { type: gameType })
this.recordHistogram('game.duration', duration, { type: gameType })
}
recordAIDecisionTime(difficulty: string, decisionTime: number): void {
this.recordHistogram('ai.decision_time', decisionTime, { difficulty })
}
recordPlayerAction(action: string): void {
this.incrementCounter('player.actions', { action })
}
private buildMetricKey(name: string, tags?: Record<string, string>): string {
if (!tags || Object.keys(tags).length === 0) {
return name
}
const tagString = Object.entries(tags)
.map(([key, value]) => `${key}:${value}`)
.sort()
.join(',')
return `${name}{${tagString}}`
}
}
7.3.2 日志系统
// 结构化日志记录
export class GameLogger {
private logger: any // 实际使用winston或其他日志库
constructor() {
this.logger = this.createLogger()
}
logGameStart(gameId: string, players: string[], gameType: string): void {
this.logger.info('Game started', {
event: 'GAME_START',
gameId,
players,
gameType,
timestamp: new Date().toISOString()
})
}
logGameMove(gameId: string, playerId: string, move: GameMove): void {
this.logger.info('Game move', {
event: 'GAME_MOVE',
gameId,
playerId,
position: move.position,
result: move.result,
timestamp: new Date().toISOString()
})
}
logGameEnd(gameId: string, winner: string, duration: number): void {
this.logger.info('Game ended', {
event: 'GAME_END',
gameId,
winner,
duration,
timestamp: new Date().toISOString()
})
}
logError(error: Error, context?: any): void {
this.logger.error('Application error', {
event: 'ERROR',
message: error.message,
stack: error.stack,
context,
timestamp: new Date().toISOString()
})
}
logSecurityEvent(event: string, details: any): void {
this.logger.warn('Security event', {
event: 'SECURITY',
type: event,
details,
timestamp: new Date().toISOString()
})
}
private createLogger(): any {
// 实际实现中创建winston logger或其他日志实例
return {
info: (message: string, meta: any) => console.log(JSON.stringify({ level: 'info', message, ...meta })),
warn: (message: string, meta: any) => console.warn(JSON.stringify({ level: 'warn', message, ...meta })),
error: (message: string, meta: any) => console.error(JSON.stringify({ level: 'error', message, ...meta }))
}
}
}
8. 测试策略
8.1 测试分层策略
8.1.1 单元测试
// 游戏逻辑单元测试示例
describe('PlaneGeometry', () => {
describe('generatePlanePositions', () => {
test('should generate correct positions for UP direction', () => {
const center = { x: 5, y: 5, coordinate: 'E_5' }
const positions = PlaneGeometry.generatePlanePositions(center, 'UP')
expect(positions).toHaveLength(11)
expect(positions[0]).toEqual({ x: 5, y: 3, coordinate: 'E_3' }) // 机头
expect(positions).toContainEqual({ x: 3, y: 4, coordinate: 'C_4' }) // 左翼尖
expect(positions).toContainEqual({ x: 7, y: 4, coordinate: 'G_4' }) // 右翼尖
})
test('should throw error for invalid center position', () => {
const center = { x: 1, y: 1, coordinate: 'A_1' }
expect(() => {
PlaneGeometry.generatePlanePositions(center, 'UP')
}).toThrow('Position would exceed board boundaries')
})
})
describe('validatePlanePosition', () => {
test('should validate position within board boundaries', () => {
const center = { x: 5, y: 5, coordinate: 'E_5' }
const isValid = PlaneGeometry.validatePlanePosition(center, 'UP', 10)
expect(isValid).toBe(true)
})
test('should reject position near edges', () => {
const center = { x: 2, y: 2, coordinate: 'B_2' }
const isValid = PlaneGeometry.validatePlanePosition(center, 'UP', 10)
expect(isValid).toBe(false)
})
})
})
// AI决策测试
describe('AIDecisionEngine', () => {
let engine: AIDecisionEngine
let mockGameState: GameState
beforeEach(() => {
engine = new AIDecisionEngine('INTERMEDIATE')
mockGameState = createMockGameState()
})
test('should select valid attack position', async () => {
const position = await engine.selectAttackPosition(mockGameState)
expect(position.x).toBeGreaterThanOrEqual(1)
expect(position.x).toBeLessThanOrEqual(10)
expect(position.y).toBeGreaterThanOrEqual(1)
expect(position.y).toBeLessThanOrEqual(10)
})
test('should not attack same position twice', async () => {
// 模拟已攻击的位置
mockGameState.moveHistory.push({
id: 'move1',
position: { x: 5, y: 5, coordinate: 'E_5' },
playerId: 'ai',
timestamp: Date.now(),
result: { type: 'MISS', value: 0 }
})
const position = await engine.selectAttackPosition(mockGameState)
expect(position).not.toEqual({ x: 5, y: 5, coordinate: 'E_5' })
})
})
8.1.2 集成测试
// 游戏流程集成测试
describe('Game Integration Tests', () => {
let gameEngine: GameEngine
let player1Id: string
let player2Id: string
let gameId: string
beforeEach(async () => {
gameEngine = new GameEngine()
player1Id = 'player1'
player2Id = 'player2'
})
test('complete game flow', async () => {
// 1. 创建游戏
const game = await gameEngine.createGame({
gameType: 'ONLINE',
players: [player1Id, player2Id]
})
gameId = game.gameId
expect(game.status).toBe('WAITING_FOR_PLANES')
// 2. 玩家布置飞机
const player1Planes = generateTestPlanes()
const player2Planes = generateTestPlanes()
await gameEngine.placePlanes(gameId, player1Id, player1Planes)
await gameEngine.placePlanes(gameId, player2Id, player2Planes)
const updatedGame = await gameEngine.getGame(gameId)
expect(updatedGame.status).toBe('IN_PROGRESS')
// 3. 进行攻击
let gameResult = await gameEngine.processAttack(gameId, player1Id, { x: 5, y: 5, coordinate: 'E_5' })
expect(gameResult.isValid).toBe(true)
// 4. 验证游戏状态更新
const finalGame = await gameEngine.getGame(gameId)
expect(finalGame.moveHistory).toHaveLength(1)
expect(finalGame.currentPlayer).toBe(player2Id)
})
test('should handle game completion', async () => {
// 创建接近结束的游戏状态
const game = await createNearEndGame()
// 执行最后一击
const result = await gameEngine.processAttack(
game.gameId,
game.currentPlayer,
getWinningMove(game)
)
expect(result.gameEnded).toBe(true)
expect(result.winner).toBe(game.currentPlayer)
// 验证游戏记录已保存
const gameRecord = await gameEngine.getGameRecord(game.gameId)
expect(gameRecord.status).toBe('COMPLETED')
expect(gameRecord.winner).toBe(game.currentPlayer)
})
})
8.1.3 端到端测试
// E2E测试 - 使用Playwright或Selenium
describe('Game E2E Tests', () => {
let page: Page
let gameUrl: string
beforeEach(async () => {
page = await browser.newPage()
gameUrl = await setupTestGame()
})
test('complete multiplayer game session', async () => {
// 1. 第一个玩家加入游戏
await page.goto(gameUrl)
await page.waitForSelector('[data-testid="game-board"]')
// 2. 布置飞机
await placePlanesViaUI(page)
await page.click('[data-testid="confirm-placement"]')
// 3. 等待对手加入(模拟第二个玩家)
await simulateSecondPlayer()
// 4. 进行攻击
await page.click('[data-testid="cell-5-5"]')
await page.waitForSelector('[data-testid="attack-result"]')
// 5. 验证UI更新
const resultText = await page.textContent('[data-testid="attack-result"]')
expect(resultText).toMatch(/命中|未命中|击毁/)
// 6. 验证游戏状态
const gameStatus = await page.textContent('[data-testid="game-status"]')
expect(gameStatus).toContain('对手回合')
})
test('AI game session', async () => {
await page.goto(`${gameUrl}?mode=ai`)
// 选择AI难度
await page.click('[data-testid="ai-difficulty-intermediate"]')
// 布置飞机
await page.click('[data-testid="auto-place-planes"]')
await page.click('[data-testid="start-game"]')
// 进行攻击
await page.click('[data-testid="cell-3-3"]')
// 等待AI响应
await page.waitForSelector('[data-testid="ai-thinking"]', { state: 'hidden' })
// 验证AI已做出攻击
const aiMoveIndicator = await page.locator('[data-testid="ai-move-indicator"]')
await expect(aiMoveIndicator).toBeVisible()
})
async function placePlanesViaUI(page: Page): Promise<void> {
// 模拟拖拽布置飞机
const plane1 = page.locator('[data-testid="plane-template"]').first()
const targetCell = page.locator('[data-testid="cell-3-3"]')
await plane1.dragTo(targetCell)
// 验证飞机已正确放置
await expect(page.locator('[data-testid="placed-plane-1"]')).toBeVisible()
}
})
8.2 性能测试
8.2.1 负载测试
// 负载测试脚本
import { check, sleep } from 'k6'
import http from 'k6/http'
import ws from 'k6/ws'
export let options = {
stages: [
{ duration: '2m', target: 100 }, // 逐步增加到100个用户
{ duration: '5m', target: 100 }, // 保持100个用户5分钟
{ duration: '2m', target: 200 }, // 增加到200个用户
{ duration: '5m', target: 200 }, // 保持200个用户
{ duration: '2m', target: 0 }, // 逐步减少到0
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95%的请求在500ms内完成
http_req_failed: ['rate<0.1'], // 错误率小于10%
},
}
export default function () {
// 测试API性能
testAPIPerformance()
// 测试WebSocket性能
testWebSocketPerformance()
sleep(1)
}
function testAPIPerformance() {
// 用户登录
let loginResponse = http.post(`${__ENV.API_BASE_URL}/api/auth/login`, {
username: `user_${__VU}_${__ITER}`,
password: 'testpass123'
})
check(loginResponse, {
'login successful': (r) => r.status === 200,
'login response time OK': (r) => r.timings.duration < 200,
})
let authToken = loginResponse.json('token')
let headers = { 'Authorization': `Bearer ${authToken}` }
// 创建游戏
let createGameResponse = http.post(`${__ENV.API_BASE_URL}/api/games`, {
gameType: 'AI',
difficulty: 'INTERMEDIATE'
}, { headers })
check(createGameResponse, {
'create game successful': (r) => r.status === 201,
'create game response time OK': (r) => r.timings.duration < 300,
})
// 获取游戏状态
let gameId = createGameResponse.json('gameId')
let getGameResponse = http.get(`${__ENV.API_BASE_URL}/api/games/${gameId}`, { headers })
check(getGameResponse, {
'get game successful': (r) => r.status === 200,
'get game response time OK': (r) => r.timings.duration < 100,
})
}
function testWebSocketPerformance() {
let url = `ws://${__ENV.WS_HOST}/socket.io/?token=${__ENV.TEST_TOKEN}`
let response = ws.connect(url, {}, function (socket) {
socket.on('open', function () {
console.log('WebSocket connected')
// 发送游戏操作
socket.send(JSON.stringify({
type: 'ATTACK',
position: { x: 5, y: 5 },
gameId: 'test-game-id'
}))
})
socket.on('message', function (message) {
let data = JSON.parse(message)
check(data, {
'message received': (data) => data !== null,
'message has type': (data) => 'type' in data,
})
})
sleep(10) // 保持连接10秒
})
check(response, {
'WebSocket connection successful': (r) => r && r.status === 101,
})
}
9. 项目管理
9.1 开发计划
9.1.1 项目里程碑
gantt
title 打飞机小程序开发计划
dateFormat YYYY-MM-DD
section 第一阶段
需求分析 :done, req, 2025-01-01, 1w
技术选型 :done, tech, after req, 3d
架构设计 :done, arch, after tech, 1w
section 第二阶段
基础框架搭建 :frame, after arch, 1w
用户系统开发 :user, after frame, 2w
游戏核心逻辑 :core, after frame, 3w
section 第三阶段
AI系统开发 :ai, after core, 2w
在线对战功能 :online, after user, 2w
UI界面开发 :ui, after core, 2w
section 第四阶段
功能测试 :test, after ai, 1w
性能优化 :perf, after test, 1w
部署上线 :deploy, after perf, 3d
9.1.2 任务分解
interface ProjectTask {
id: string
name: string
description: string
assignee: string
status: TaskStatus
priority: Priority
estimatedHours: number
actualHours?: number
dependencies: string[]
startDate: Date
endDate: Date
}
enum TaskStatus {
TODO = 'TODO',
IN_PROGRESS = 'IN_PROGRESS',
IN_REVIEW = 'IN_REVIEW',
DONE = 'DONE',
BLOCKED = 'BLOCKED'
}
enum Priority {
LOW = 'LOW',
MEDIUM = 'MEDIUM',
HIGH = 'HIGH',
CRITICAL = 'CRITICAL'
}
// 任务列表示例
const projectTasks: ProjectTask[] = [
{
id: 'CORE-001',
name: '飞机几何模型实现',
description: '实现飞机位置生成、碰撞检测等核心几何算法',
assignee: '后端开发工程师',
status: TaskStatus.TODO,
priority: Priority.HIGH,
estimatedHours: 16,
dependencies: [],
startDate: new Date('2025-01-15'),
endDate: new Date('2025-01-17')
},
{
id: 'AI-001',
name: '概率热图算法',
description: '实现基于贝叶斯推理的攻击概率计算',
assignee: 'AI工程师',
status: TaskStatus.TODO,
priority: Priority.HIGH,
estimatedHours: 24,
dependencies: ['CORE-001'],
startDate: new Date('2025-01-18'),
endDate: new Date('2025-01-21')
},
{
id: 'UI-001',
name: '游戏棋盘组件',
description: '开发可交互的游戏棋盘UI组件',
assignee: '前端开发工程师',
status: TaskStatus.TODO,
priority: Priority.MEDIUM,
estimatedHours: 20,
dependencies: ['CORE-001'],
startDate: new Date('2025-01-18'),
endDate: new Date('2025-01-22')
}
]
9.2 质量保证
9.2.1 代码规范
// ESLint 配置示例
module.exports = {
extends: [
'@typescript-eslint/recommended',
'eslint:recommended'
],
rules: {
// 代码风格
'indent': ['error', 2],
'quotes': ['error', 'single'],
'semi': ['error', 'never'],
// TypeScript规则
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
// 游戏特定规则
'prefer-const': 'error',
'no-magic-numbers': ['warn', { ignore: [-1, 0, 1, 2] }],
// 性能相关
'no-console': 'warn',
'no-debugger': 'error'
}
}
// Prettier 配置
module.exports = {
semi: false,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
printWidth: 100,
bracketSpacing: true,
arrowParens: 'avoid'
}
9.2.2 代码审查流程
# 代码审查清单
## 功能性检查
- [ ] 功能是否按需求正确实现
- [ ] 边界条件是否正确处理
- [ ] 错误处理是否完善
- [ ] 单元测试是否覆盖主要场景
## 性能检查
- [ ] 算法复杂度是否合理
- [ ] 是否存在内存泄漏风险
- [ ] 数据库查询是否优化
- [ ] 缓存策略是否得当
## 安全性检查
- [ ] 输入验证是否充分
- [ ] 权限控制是否正确
- [ ] 敏感信息是否泄露
- [ ] SQL注入等安全风险
## 代码质量
- [ ] 命名是否清晰易懂
- [ ] 代码结构是否合理
- [ ] 注释是否充分
- [ ] 是否遵循团队规范
10. 风险评估与应对
10.1 技术风险
10.1.1 性能风险
风险描述: 在高并发场景下系统性能下降
风险等级: 中等
影响分析:
- 用户体验下降
- 服务器资源消耗过高
- 运营成本增加
应对策略:
-
预防措施:
- 实施负载测试
- 优化关键算法
- 采用缓存策略
- 数据库索引优化
-
应急处理:
- 自动扩容机制
- 熔断降级策略
- 优雅降级功能
10.1.2 AI算法风险
风险描述: AI决策质量不佳或响应时间过长
风险等级: 中等
应对策略:
- 多级AI难度系统
- 决策时间限制机制
- 备用简单策略算法
- 持续的模型训练和优化
10.2 业务风险
10.2.1 用户流失风险
风险描述: 游戏平衡性问题导致用户流失
应对策略:
- 数据驱动的平衡性调整
- A/B测试验证
- 用户反馈收集机制
- 快速迭代能力
10.2.2 竞品风险
风险描述: 市场出现更优秀的同类产品
应对策略:
- 持续创新和功能迭代
- 建立用户社区
- 差异化竞争策略
- 快速响应市场变化
11. 总结
11.1 项目特色
本需求说明书为打飞机小程序项目提供了全面详尽的技术指导,具有以下特色:
-
完整的技术栈: 从前端到后端,从数据库到缓存,覆盖了现代Web应用的所有技术层面
-
先进的AI算法: 集成了蒙特卡洛树搜索、神经网络评估、概率推理等前沿AI技术
-
企业级架构: 采用微服务架构、容器化部署、监控体系等企业级最佳实践
-
全面的安全考虑: 从输入验证到反作弊,从网络安全到数据保护的完整安全体系
-
可扩展设计: 支持多平台部署、水平扩展、功能模块化的灵活架构
11.2 实施建议
-
分阶段开发: 按照MVP->完整功能->优化增强的顺序逐步实施
-
技术选型灵活性: 根据团队技术栈和项目预算灵活调整技术选择
-
持续集成: 建立CI/CD流水线,确保代码质量和部署效率
-
数据驱动: 建立完善的数据收集和分析体系,指导产品优化
-
用户体验优先: 在所有技术决策中优先考虑用户体验
11.3 预期成果
通过本需求说明书的指导实施,预期能够开发出一个:
- 技术先进: 采用最新技术栈和算法
- 性能优秀: 支持高并发、低延迟的游戏体验
- 功能完善: 涵盖单机AI、在线对战、社交系统等完整功能
- 安全可靠: 具备企业级安全防护能力
- 可扩展: 支持后续功能扩展和技术升级
的高质量小程序产品。
文档版本: v1.0
最后更新: 2025年9月
文档状态: 完成
本需求说明书为打飞机小程序的完整开发提供了详尽的技术指导,涵盖了从架构设计到具体实现的所有关键环节,可作为独立的开发指南使用。