基于 Spring Boot + LLM 构建智能狼人杀游戏后端

作者:青云 发布时间: 2026-06-26 阅读量:4 评论数:0
# 基于 Spring Boot + LLM 构建智能狼人杀游戏后端

## 项目概述

这是一个基于 **Spring Boot 3.5** 构建的狼人杀游戏后端系统,核心特色在于引入了 **LLM 大模型驱动的 AI 玩家智能决策系统**,让玩家可以与智能 AI 进行真实的狼人杀对战。项目整合了 WebSocket、SSE、Redis 等技术,实现了完整的实时游戏流程和即时通讯功能。

## 技术栈

| 类别 | 技术 | 版本 |
|------|------|------|
| 核心框架 | Spring Boot | 3.5.14 |
| 认证授权 | Spring Security + JWT | - |
| ORM | MyBatis-Plus | 3.5.5 |
| 数据库 | MySQL | 8.0+ |
| 缓存 | Redis | 6.0+ |
| 实时通信 | WebSocket / SSE / Netty | - |
| AI 能力 | Spring AI + DashScope | - |

## 核心架构设计

### 1. LLM 驱动的 AI 玩家智能决策系统

这是本项目最具创新性的核心模块。AI 玩家在整个游戏过程中全自动执行所有操作,无需额外 API 调用,所有决策均通过 **DeepSeek V3 大语言模型** 驱动。

#### 架构设计

```
┌─────────────────────────────────────────────────────────────────┐
│                      AI 决策流程                                 │
├─────────────────────────────────────────────────────────────────┤
│  游戏状态 (GameRoom)                                             │
│       │                                                          │
│       ▼                                                          │
│  AiPromptBuilder ──构建完整游戏上下文──▶ LLM Prompt               │
│       │                                                          │
│       │ 包含:游戏概况、玩家状态、发言历史、投票记录、死亡报告       │
│       ▼                                                          │
│  AiBehaviorService ──调用 DeepSeek V3──▶ 智能决策                 │
│       │                                                          │
│       ├─ 成功 → 解析响应 → 执行动作                               │
│       │                                                          │
│       └─ 失败 → 重试(最多2次) → 降级策略(随机选择)                │
└─────────────────────────────────────────────────────────────────┘
```

#### Prompt 构建策略

`AiPromptBuilder` 为每次 AI 决策构建完整的游戏上下文:

```java
// 核心提示词构建逻辑
public String buildGameContext(GameRoom room, int aiSeat) {
    StringBuilder sb = new StringBuilder();
    // 游戏状态
    sb.append("## 游戏状态\n");
    sb.append("- 当前天数:第").append(room.getRound()).append("天\n");
    sb.append("- 当前阶段:").append(room.getPhase() == null ? "未知" :
            (room.getPhase().name().equals("DAY") ? "白天" : "夜晚")).append("\n");
    
    // 玩家状态(表格形式)
    sb.append("\n## 玩家状态\n");
    sb.append("| 座位号 | 昵称 | 状态 | 出局原因 |\n");
    sb.append("|--------|------|------|----------|\n");
    // ... 遍历所有玩家
    
    // 历史发言摘要(压缩处理)
    sb.append("\n## 历史发言摘要\n");
    // ... 仅保留最近3轮,每条发言最多100字符
    
    // 投票记录、死亡信息、已亮牌信息等
    return sb.toString();
}
```

**Prompt 压缩策略(控制 Token 消耗):**

| 策略 | 说明 |
|------|------|
| 发言轮次限制 | 仅保留最近 3 轮的发言历史 |
| 发言内容截断 | 每条发言最多保留 100 个字符 |
| 原始数据不变 | 压缩仅作用于发送给 LLM 的 Prompt |

#### AI 决策覆盖的游戏阶段

| 阶段 | 角色 | 延迟 | LLM 决策内容 |
|------|------|------|-------------|
| 夜晚 | 守卫 | 1.5~3.5s | 分析谁最可能被狼人击杀,选择守护目标 |
| 夜晚 | 狼人 | 2.5~4.5s | 分析局势,选择击杀目标(避开同伴) |
| 夜晚 | 预言家 | 3~5s | 选择最可疑的玩家进行查验 |
| 夜晚 | 女巫 | 4~6s | 分析被杀玩家价值,决定是否使用解药/毒药 |
| 白天 | 发言 | 1.5~3.5s | 根据角色身份生成策略性发言 |
| 白天 | 投票 | 2~5s | 综合分析,投票给最可疑的目标 |
| 死亡 | 猎人 | 2~4s | 选择开枪带走的目标 |

#### LLM 调用与降级机制

```java
// AiBehaviorService 核心决策方法
public int decideNightActionTarget(String roomCode, int aiSeat) {
    String prompt = promptBuilder.buildNightActionPrompt(room, aiSeat);
    
    for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
        try {
            String response = callLLM(prompt);
            int target = extractSeatNumber(response);
            if (isValidNightTarget(room, aiSeat, target)) {
                return target;
            }
        } catch (Exception e) {
            log.warn("AI夜间行动LLM调用失败 (尝试{}/{}): {}", attempt, MAX_RETRIES, e.getMessage());
        }
    }
    // 降级策略:随机选择合法目标
    return fallbackNightTarget(room, aiSeat);
}
```

#### AI 发言示例

AI 会根据角色身份和游戏局势生成策略性发言:

| 角色 | 发言策略 | 示例 |
|------|---------|------|
| 村民/守卫/猎人 | 分析投票记录和可疑行为 | "根据目前的分析,我认为3号的行为比较可疑,建议大家关注他的投票记录。" |
| 狼人 | 伪装好人,带节奏指控他人 | "作为好人,我必须站出来说话。5号的行为模式非常可疑,我强烈怀疑他是狼人。" |
| 预言家 | 适度分享查验信息 | "我的直觉告诉我,4号非常可疑。我有充分的理由怀疑他是狼人,建议大家重点关注。" |

### 2. 实时通信双通道架构

系统采用 WebSocket + SSE 双通道架构,确保实时通信的可靠性:

| 通道 | 用途 | 特点 |
|------|------|------|
| 游戏 WebSocket | 房间内游戏操作(投票、发言、夜间行动) | 低延迟、双向通信 |
| 聊天 WebSocket | 私聊、群聊消息 | 独立于游戏通道,互不干扰 |
| SSE | 在线状态推送、房间列表更新 | 自动重连、轻量级 |

#### WebSocket 会话隔离设计

```java
// WebSocketSessionManager 实现会话隔离
public class WebSocketSessionManager {
    // 游戏会话映射
    private final Map<Long, WebSocketSession> gameSessions = new ConcurrentHashMap<>();
    // 聊天会话映射
    private final Map<Long, WebSocketSession> chatSessions = new ConcurrentHashMap<>();
    // 两者独立管理,互不覆盖
}
```

### 3. 二级缓存架构

系统采用 **JVM 内存 + Redis** 的二级缓存架构,确保游戏数据的高性能和持久化:

| 层级 | 存储 | 说明 |
|------|------|------|
| L1 | JVM 内存 (ConcurrentHashMap) | 高性能读写,游戏运行时数据 |
| L2 | Redis | 持久化存储,服务重启后恢复数据 |

**缓存同步策略:**

```
写操作:更新内存 → 同步到 Redis
启动恢复:Redis → 数据库重建
关闭钩子:全量同步到 Redis
定时备份:每30秒自动备份
```

```java
// GameRoomCacheService 核心同步逻辑
@Slf4j
@Service
public class GameRoomCacheService {
    private static final String ROOM_KEY_PREFIX = "game:room:";
    private static final String PLAYER_ROOM_KEY = "game:player:room";

    public void syncRoom(GameRoom gameRoom) {
        try {
            String key = ROOM_KEY_PREFIX + gameRoom.getRoomCode();
            redisTemplate.opsForValue().set(key, gameRoom);
        } catch (Exception e) {
            log.error("同步房间到Redis失败: roomCode={}", gameRoom.getRoomCode(), e);
        }
    }

    public int restoreFromRedis() {
        int count = 0;
        try {
            Set<String> roomCodes = getAllRoomCodes();
            for (String roomCode : roomCodes) {
                GameRoom room = getRoom(roomCode);
                if (room != null) {
                    // 确保players字段使用线程安全的ConcurrentHashMap
                    if (room.getPlayers() != null && !(room.getPlayers() instanceof ConcurrentHashMap)) {
                        room.setPlayers(new ConcurrentHashMap<>(room.getPlayers()));
                    }
                    gameRoomCache.put(roomCode, room);
                    // 重建玩家-房间映射关系
                    for (var player : room.getPlayers().values()) {
                        if (player.getUserId() != null && !Boolean.TRUE.equals(player.getIsAi())) {
                            playerRoomCache.put(player.getUserId(), roomCode);
                        }
                    }
                    count++;
                }
            }
        } catch (Exception e) {
            log.error("从Redis恢复缓存失败", e);
        }
        return count;
    }
}
```

### 4. 完整游戏流程引擎

游戏流程引擎负责管理狼人杀的完整流程控制:

```
夜晚阶段:守卫守护 → 狼人击杀 → 预言家查验 → 女巫用药 → 猎人确认 → 天亮
    │
    ▼
白天阶段:公布死讯 → 顺序发言 → 投票放逐 → 猎人开枪(可选)→ 下一夜
    │
    ▼
胜负判定:好人阵营胜利(全灭狼人)或狼人阵营胜利(屠神/屠民)
```

#### 核心规则实现

**奶穿规则(守卫守护 + 女巫救活 = 玩家死亡):**

```java
// GameFlowServiceImpl 夜间结算逻辑
private void endNightPhase(String roomCode) {
    Integer guardedPlayer = gameRoom.getGuardedPlayer();
    Integer witchHealedPlayer = gameRoom.getWitchHealedPlayer();
    
    for (Integer killedSeat : wolfKills) {
        boolean isGuarded = killedSeat.equals(guardedPlayer);
        boolean isWitchHealed = killedSeat.equals(witchHealedPlayer);
        
        // 奶穿判定:两种保护叠加反而导致死亡
        if (isGuarded && isWitchHealed) {
            deaths.add(killedSeat);
            player.setDeathReason("奶穿(守卫守护+女巫救活)");
        } else if (isGuarded) {
            // 仅被守卫守护 → 存活
        } else if (isWitchHealed) {
            // 仅被女巫救活 → 存活
        } else {
            // 无任何保护 → 死亡
            deaths.add(killedSeat);
        }
    }
}
```

#### 超时自动处理机制

```java
// 操作超时处理
public void handleOperationTimeout(String roomCode) {
    GamePhase phase = gameRoom.getPhase();
    
    if (phase == GamePhase.NIGHT) {
        // 夜晚超时:当前步骤未操作视为弃权,推进到下一步骤
        String currentStep = gameRoom.getCurrentNightStep();
        switch (currentStep) {
            case "guard" -> log.info("[守卫] 超时弃权");
            case "wolf" -> // 结算已有投票
            case "seer" -> log.info("[预言家] 超时弃权");
            case "witch" -> log.info("[女巫] 超时弃权(未用药)");
        }
        advanceNightStep(roomCode);
    } else if (phase == GamePhase.DAY) {
        if (gameRoom.getCurrentSpeaker() != null) {
            // 发言超时:自动结束当前发言
            advanceSpeaker(roomCode);
        } else {
            // 投票超时:未投票玩家视为弃权
            executeVoteResult(roomCode);
        }
    }
}
```

### 5. 安全与认证体系

#### JWT Token 提取规则

```java
// JwtAuthenticationFilter 认证逻辑
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private static final String NEW_TOKEN_HEADER = "X-New-Token";
    private static final String TOKEN_REMAINING_TIME_HEADER = "X-Token-Remaining-Time";

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
            HttpServletResponse response, FilterChain filterChain) {
        String token = getTokenFromRequest(request);

        if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) {
            String username = jwtUtil.getUsername(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(userDetails, null, 
                            userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // 添加Token剩余时间到响应头
            long remainingTime = jwtUtil.getTokenRemainingTime(token);
            response.setHeader(TOKEN_REMAINING_TIME_HEADER, String.valueOf(remainingTime));

            // 检查是否需要续期
            if (jwtUtil.shouldRenewToken(token)) {
                String newToken = jwtUtil.refreshToken(token);
                response.setHeader(NEW_TOKEN_HEADER, jwtProperties.getPrefix() + newToken);
            }
        }
        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        // 1. 从Header中获取(标准方式)
        String bearerToken = request.getHeader(jwtProperties.getHeader());
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(jwtProperties.getPrefix())) {
            return bearerToken.substring(jwtProperties.getPrefix().length());
        }
        // 2. 从URL参数中获取(SSE/WebSocket等无法自定义Header的场景)
        String tokenParam = request.getParameter("token");
        if (StringUtils.hasText(tokenParam)) {
            return tokenParam;
        }
        return null;
    }
}
```

**Token 自动续期机制:**
- 响应头携带 `X-Token-Remaining-Time`(剩余时间,毫秒)
- 剩余时间 < 30 分钟时自动续期
- 新 Token 通过响应头 `X-New-Token` 返回(格式:`Bearer <新Token>`)

### 6. IM 即时通讯系统

项目实现了完整的即时通讯功能,包括好友管理、私聊、群聊等,支持 WebSocket + SSE 双通道推送。

#### 好友管理

| 功能 | 接口 | 说明 |
|------|------|------|
| 搜索用户 | `GET /api/im/friend/search` | 关键词模糊匹配 |
| 好友列表 | `GET /api/im/friend/list` | 包含在线状态、未读消息数 |
| 发送申请 | `POST /api/im/friend/apply` | 双向检查避免重复申请 |
| 处理申请 | `POST /api/im/friend/apply/handle` | 接受/拒绝 |
| 删除好友 | `DELETE /api/im/friend/{friendId}` | 双向删除 |

#### 私聊消息

```java
// ChatServiceImpl 发送消息逻辑
@Transactional
public ChatMessageDTO sendMessage(Long fromUserId, Long toUserId, 
        String messageType, String content, String extra) {
    // 消息去重:检查客户端 messageId 是否已存在
    String messageId = clientMessageId != null ? clientMessageId : IdUtil.fastSimpleUUID();
    
    // 创建消息实体并入库
    ChatMessageEntity message = new ChatMessageEntity();
    message.setMessageId(messageId);
    message.setFromUserId(fromUserId);
    message.setToUserId(toUserId);
    message.setContent(content);
    message.setStatus(1); // 已发送
    chatMessageMapper.insert(message);

    // 实时推送:优先 WebSocket,降级 SSE
    if (isUserReachable(toUserId)) {
        chatWebSocketHandler.sendToUser(toUserId,
                WebSocketMessage.of("chat_message", dto));
    }
    return dto;
}

private boolean isUserReachable(Long userId) {
    return sessionManager.isOnline(userId) || sseManager.hasEmitter(userId);
}
```

#### 群聊系统

群聊支持完整的权限管理功能:

| 操作 | 群主 | 管理员 | 普通成员 |
|------|------|--------|---------|
| 更新群信息 | ✓ | ✓ | ✗ |
| 解散群聊 | ✓ | ✗ | ✗ |
| 邀请成员 | ✓ | ✓ | ✓ |
| 移除成员 | ✓ | ✓(不能移除管理员) | ✗ |
| 设置管理员 | ✓ | ✗ | ✗ |
| 禁言/解除禁言 | ✓ | ✓(不能禁言群主) | ✗ |

#### 在线状态判定

```java
// WebSocketSessionManager 在线状态判定
public boolean isOnline(Long userId) {
    // 游戏 WebSocket 会话
    boolean hasGameSession = gameSessions.containsKey(userId);
    // 聊天 WebSocket 会话
    boolean hasChatSession = chatSessions.containsKey(userId);
    // SSE 连接
    boolean hasSse = sseManager.hasEmitter(userId);
    return hasGameSession || hasChatSession || hasSse;
}
```

## 数据库设计

### 核心数据表

| 表名 | 说明 |
|------|------|
| `sys_user` | 用户表 |
| `game_room` | 游戏房间表 |
| `game_record` | 游戏记录表(完整过程记录) |
| `im_friend` | 好友关系表 |
| `im_chat_message` | 私聊消息表 |
| `im_group` | 群聊表 |
| `im_group_message` | 群消息表 |

### 游戏记录存储

游戏结束后自动保存完整记录:

```json
{
  "winCamp": "wolf",
  "winReason": "wolf_god_slaughter",
  "totalRounds": 3,
  "players": [
    {
      "userId": 1,
      "nickname": "张三",
      "seatNumber": 1,
      "role": "seer",
      "status": "死亡",
      "deathReason": "被狼人击杀"
    }
  ]
}
```

## 总结与展望

### 项目亮点

1. **LLM 驱动的智能 AI 玩家** - 基于完整游戏上下文进行智能分析和决策
2. **双通道实时通信架构** - WebSocket + SSE 确保通信可靠性
3. **二级缓存设计** - 高性能内存缓存 + Redis 持久化
4. **完整的游戏规则引擎** - 支持奶穿、守卫限制、猎人技能等复杂规则
5. **超时自动处理** - 确保游戏流程不会因玩家不操作而卡住
6. **完整的 IM 系统** - 好友管理、私聊、群聊、权限管理

### 技术价值

该项目展示了如何将大语言模型与传统游戏后端结合,通过 Prompt 工程和降级策略实现可靠的 AI 决策系统。核心技术点包括:

- **Prompt 工程**:构建结构化的游戏上下文,控制 Token 消耗
- **异步调度**:通过 `ScheduledExecutorService` 模拟 AI 思考延迟
- **缓存一致性**:JVM 内存与 Redis 的双写一致性保障
- **实时通信**:WebSocket 会话隔离与 SSE 自动重连

### 未来优化方向

1. **多模型支持**:扩展支持更多 LLM 模型,通过配置切换
2. **AI 难度分级**:根据玩家水平调整 AI 决策策略
3. **游戏回放**:基于 `actionHistory` 实现完整游戏回放
4. **分布式部署**:支持多节点部署,通过 Redis 共享游戏状态
5. **性能优化**:引入消息队列处理非关键操作,降低主流程延迟

---

展示:

评论