3178 lines
88 KiB
Markdown
3178 lines
88 KiB
Markdown
|
||
# 打飞机小程序完整需求说明书
|
||
|
||
## 文档信息
|
||
|
||
| 项目 | 详情 |
|
||
|------|------|
|
||
| 项目名称 | 打飞机对战小程序 |
|
||
| 文档版本 | v1.0 |
|
||
| 创建日期 | 2025年9月 |
|
||
| 目标平台 | 微信小程序 / 原生APP |
|
||
| 开发语言 | TypeScript + JavaScript |
|
||
|
||
## 此文档已经过期失效 **参考价值可以忽略**
|
||
> 此文档已经过期失效 **参考价值可以忽略**
|
||
|
||
## 1. 项目概述
|
||
|
||
### 1.1 项目背景
|
||
打飞机是一款经典的双人对战策略游戏,玩家需要在棋盘上布置飞机并猜测对手飞机位置。本项目旨在开发一个现代化的小程序版本,支持人机对战和在线对战功能。
|
||
|
||
### 1.2 项目目标
|
||
- 提供流畅的游戏体验和直观的用户界面
|
||
- 实现智能AI对战系统
|
||
- 支持在线实时对战功能
|
||
- 建立完整的用户系统和数据统计
|
||
- 确保跨平台兼容性和高性能表现
|
||
|
||
### 1.3 目标用户群体
|
||
- **主要用户**: 8-60岁喜欢益智游戏的用户
|
||
- **使用场景**: 休闲娱乐、朋友对战、碎片时间游戏
|
||
- **用户特征**: 追求简单易上手但有一定策略性的游戏
|
||
|
||
## 2. 功能需求规格
|
||
|
||
### 2.1 核心功能模块
|
||
|
||
#### 2.1.1 用户系统
|
||
```typescript
|
||
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. 棋盘系统
|
||
```typescript
|
||
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. 飞机系统
|
||
```typescript
|
||
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. 游戏状态管理
|
||
```typescript
|
||
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难度等级
|
||
```typescript
|
||
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决策算法接口
|
||
```typescript
|
||
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. 房间系统
|
||
```typescript
|
||
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. 实时通信协议
|
||
```typescript
|
||
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 游戏界面布局
|
||
```typescript
|
||
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. 飞机布置交互
|
||
1. **拖拽模式**: 从侧边栏拖拽飞机到棋盘
|
||
2. **点击模式**: 点击棋盘位置,选择飞机方向
|
||
3. **辅助功能**:
|
||
- 半透明预览显示
|
||
- 红色提示无效位置
|
||
- 绿色确认有效位置
|
||
- 自动布置功能
|
||
|
||
##### B. 攻击操作交互
|
||
1. **点击攻击**: 直接点击对方棋盘位置
|
||
2. **确认机制**: 重要攻击需二次确认
|
||
3. **视觉反馈**:
|
||
- **实时瞄准提示**: 在对手回合,棋盘上应实时显示对手正在瞄准(已选择但未确认)的格子。
|
||
- 攻击动画效果
|
||
- 结果显示动画
|
||
- 音效配合
|
||
|
||
#### 2.2.4 响应式设计要求
|
||
```css
|
||
/* 屏幕适配断点 */
|
||
@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
|
||
```typescript
|
||
// 原因:
|
||
// 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
|
||
```typescript
|
||
// 游戏状态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 + 自定义组件
|
||
```typescript
|
||
// 自定义游戏棋盘组件
|
||
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
|
||
```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
|
||
```typescript
|
||
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
|
||
```typescript
|
||
// 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 核心算法框架
|
||
```typescript
|
||
// 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 概率计算算法
|
||
```typescript
|
||
// 概率热力图实现
|
||
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 模式识别算法
|
||
```typescript
|
||
// 模式分析器
|
||
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 飞机位置生成算法
|
||
```typescript
|
||
// 飞机几何模型定义
|
||
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 碰撞检测算法
|
||
```typescript
|
||
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 自动布置算法
|
||
```typescript
|
||
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)
|
||
```typescript
|
||
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 神经网络评估函数
|
||
```typescript
|
||
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 状态同步协议
|
||
```typescript
|
||
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 断线重连机制
|
||
```typescript
|
||
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 用户相关表
|
||
```sql
|
||
-- 用户基本信息表
|
||
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 游戏相关表
|
||
```sql
|
||
-- 游戏记录表
|
||
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 缓存结构
|
||
```typescript
|
||
// 游戏会话缓存
|
||
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 缓存操作类
|
||
```typescript
|
||
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 输入验证
|
||
```typescript
|
||
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 反作弊系统
|
||
```typescript
|
||
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 安全
|
||
```typescript
|
||
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 安全
|
||
```typescript
|
||
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
|
||
# 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"]
|
||
```
|
||
|
||
```yaml
|
||
# 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
|
||
# 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 应用监控
|
||
```typescript
|
||
// 监控指标收集
|
||
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 日志系统
|
||
```typescript
|
||
// 结构化日志记录
|
||
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 单元测试
|
||
```typescript
|
||
// 游戏逻辑单元测试示例
|
||
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 集成测试
|
||
```typescript
|
||
// 游戏流程集成测试
|
||
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 端到端测试
|
||
```typescript
|
||
// 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 负载测试
|
||
```typescript
|
||
// 负载测试脚本
|
||
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 项目里程碑
|
||
```mermaid
|
||
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 任务分解
|
||
```typescript
|
||
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 代码规范
|
||
```typescript
|
||
// 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 代码审查流程
|
||
```markdown
|
||
# 代码审查清单
|
||
|
||
## 功能性检查
|
||
- [ ] 功能是否按需求正确实现
|
||
- [ ] 边界条件是否正确处理
|
||
- [ ] 错误处理是否完善
|
||
- [ ] 单元测试是否覆盖主要场景
|
||
|
||
## 性能检查
|
||
- [ ] 算法复杂度是否合理
|
||
- [ ] 是否存在内存泄漏风险
|
||
- [ ] 数据库查询是否优化
|
||
- [ ] 缓存策略是否得当
|
||
|
||
## 安全性检查
|
||
- [ ] 输入验证是否充分
|
||
- [ ] 权限控制是否正确
|
||
- [ ] 敏感信息是否泄露
|
||
- [ ] SQL注入等安全风险
|
||
|
||
## 代码质量
|
||
- [ ] 命名是否清晰易懂
|
||
- [ ] 代码结构是否合理
|
||
- [ ] 注释是否充分
|
||
- [ ] 是否遵循团队规范
|
||
```
|
||
|
||
## 10. 风险评估与应对
|
||
|
||
### 10.1 技术风险
|
||
|
||
#### 10.1.1 性能风险
|
||
**风险描述**: 在高并发场景下系统性能下降
|
||
|
||
**风险等级**: 中等
|
||
|
||
**影响分析**:
|
||
- 用户体验下降
|
||
- 服务器资源消耗过高
|
||
- 运营成本增加
|
||
|
||
**应对策略**:
|
||
1. **预防措施**:
|
||
- 实施负载测试
|
||
- 优化关键算法
|
||
- 采用缓存策略
|
||
- 数据库索引优化
|
||
|
||
2. **应急处理**:
|
||
- 自动扩容机制
|
||
- 熔断降级策略
|
||
- 优雅降级功能
|
||
|
||
#### 10.1.2 AI算法风险
|
||
**风险描述**: AI决策质量不佳或响应时间过长
|
||
|
||
**风险等级**: 中等
|
||
|
||
**应对策略**:
|
||
1. 多级AI难度系统
|
||
2. 决策时间限制机制
|
||
3. 备用简单策略算法
|
||
4. 持续的模型训练和优化
|
||
|
||
### 10.2 业务风险
|
||
|
||
#### 10.2.1 用户流失风险
|
||
**风险描述**: 游戏平衡性问题导致用户流失
|
||
|
||
**应对策略**:
|
||
1. 数据驱动的平衡性调整
|
||
2. A/B测试验证
|
||
3. 用户反馈收集机制
|
||
4. 快速迭代能力
|
||
|
||
#### 10.2.2 竞品风险
|
||
**风险描述**: 市场出现更优秀的同类产品
|
||
|
||
**应对策略**:
|
||
1. 持续创新和功能迭代
|
||
2. 建立用户社区
|
||
3. 差异化竞争策略
|
||
4. 快速响应市场变化
|
||
|
||
## 11. 总结
|
||
|
||
### 11.1 项目特色
|
||
|
||
本需求说明书为打飞机小程序项目提供了全面详尽的技术指导,具有以下特色:
|
||
|
||
1. **完整的技术栈**: 从前端到后端,从数据库到缓存,覆盖了现代Web应用的所有技术层面
|
||
|
||
2. **先进的AI算法**: 集成了蒙特卡洛树搜索、神经网络评估、概率推理等前沿AI技术
|
||
|
||
3. **企业级架构**: 采用微服务架构、容器化部署、监控体系等企业级最佳实践
|
||
|
||
4. **全面的安全考虑**: 从输入验证到反作弊,从网络安全到数据保护的完整安全体系
|
||
|
||
5. **可扩展设计**: 支持多平台部署、水平扩展、功能模块化的灵活架构
|
||
|
||
### 11.2 实施建议
|
||
|
||
1. **分阶段开发**: 按照MVP->完整功能->优化增强的顺序逐步实施
|
||
|
||
2. **技术选型灵活性**: 根据团队技术栈和项目预算灵活调整技术选择
|
||
|
||
3. **持续集成**: 建立CI/CD流水线,确保代码质量和部署效率
|
||
|
||
4. **数据驱动**: 建立完善的数据收集和分析体系,指导产品优化
|
||
|
||
5. **用户体验优先**: 在所有技术决策中优先考虑用户体验
|
||
|
||
### 11.3 预期成果
|
||
|
||
通过本需求说明书的指导实施,预期能够开发出一个:
|
||
|
||
- **技术先进**: 采用最新技术栈和算法
|
||
- **性能优秀**: 支持高并发、低延迟的游戏体验
|
||
- **功能完善**: 涵盖单机AI、在线对战、社交系统等完整功能
|
||
- **安全可靠**: 具备企业级安全防护能力
|
||
- **可扩展**: 支持后续功能扩展和技术升级
|
||
|
||
的高质量小程序产品。
|
||
|
||
---
|
||
|
||
**文档版本**: v1.0
|
||
**最后更新**: 2025年9月
|
||
**文档状态**: 完成
|
||
|
||
本需求说明书为打飞机小程序的完整开发提供了详尽的技术指导,涵盖了从架构设计到具体实现的所有关键环节,可作为独立的开发指南使用。 |