五子棋在线对战

面向对象程序设计(Java)

从零构建一个基于C/S架构的在线对战平台

包含完整需求分析与5个分步作业详解

游戏规则与平台概述

1. 五子棋基础介绍

五子棋起源于中国,规则简单但变化无穷。

1.2 游戏规则

  • 棋盘: 15×15 的网格。
  • 落子: 黑方先手,白方后手,轮流落子。
  • 获胜: 横、竖、斜任一方向连续5子同色即胜。
  • 平局: 棋盘下满且无五连珠。
  • 禁手 (可选):
    • 本项目主要实现基础规则(无禁手)。
    • 扩展说明: 标准规则中黑方有"三三"、"四四"、"长连"禁手。

2. 在线对战平台概述

基于 C/S (Client-Server) 架构的命令行实时网络对战系统。

2.2 核心功能需求

  • 用户系统: 输入昵称登录,身份标识。
  • 房间与匹配:
    • 大厅(Lobby): 查看所有房间状态。
    • 加入(Join): 指定房间号进入,两人同房。
  • 实时对战:
    • 棋盘同步: 服务器维护权威状态并推送。
    • 回合控制: 严格轮流落子。
    • 胜负判定: 服务器实时计算。
  • 辅助功能: ANSI颜色渲染,清屏刷新。

架构设计与技术栈

3.1 系统架构图

系统采用标准的 Client-Server 架构:

  • Server (服务端): 负责处理连接请求、维护房间列表、管理游戏状态、执行游戏逻辑(规则判定)。
  • Client (客户端): 负责接收用户输入、发送指令、解析服务器响应、渲染游戏画面。

3.3 开发环境要求

  • 操作系统: Windows / macOS / Linux
  • JDK 版本: Java 8 或更高
  • IDE: IntelliJ IDEA / VS Code

3.2 技术选型说明

  • 编程语言: Java (JDK 8+)
  • 网络通信协议:
    • 基于 TCP/IP (Socket API)。
    • 自定义文本协议 (如 put 7 7)。
  • 数据存储方案:
    • 内存存储 (In-Memory)。
    • RoomManager 管理 GameRoom。
  • 并发模型:
    • ExecutorService 线程池。
    • ClientHandler 独立线程处理连接。
  • 构建工具: Maven

功能模块详细说明

4.1 服务器端功能

  • 连接管理:

    监听端口 8888,使用线程池分配 ClientHandler。

  • 用户管理:

    处理昵称注册,处理断线重连/异常退出。

  • 房间管理:

    初始化固定数量房间,提供 ls rooms 查询,处理 enter/leave。

  • 游戏逻辑:

    落子验证(越界/重复/轮次),胜负算法(4方向扫描),状态广播。

4.2 客户端功能

  • 网络连接:

    独立线程持续接收消息。

  • 用户界面 (CLI):

    命令输入: ls, enter, start, put, quit, help。

  • 棋盘渲染:

    解析 ANSI 颜色代码。

    使用清屏控制码 \033[H\033[2J 实现“伪动态”刷新。

    显示黑子(●)、白子(○)、空位(┼)。

交付要求与评分标准

5. 项目交付要求

  • 5.1 代码规范: PascalCase/camelCase, 合理分包 (client/server/common), 关键逻辑注释。
  • 5.2 文档要求:
    • 需求文档 (即本文)。
    • 设计文档 (UML类图, 时序图)。
    • 用户手册 (启动说明, 操作指令)。
  • 5.3 测试用例: JUnit 单元测试 (胜负逻辑) + 集成测试。
  • 5.4 演示视频: 3-5分钟,展示完整流程 (启动->连接->对战->胜负->重开)。
  • 5.5 截止日期: 2025年X月X日

6. 评分标准 (基础分: 80%)

说明:基础功能满分为80分,剩余20分通过扩展功能获得

  • 6.1 功能完整性 (40%): 连接、进房、对战流畅、判定准确。
  • 6.2 代码质量 (25%): 规范、OOP设计、异常处理健壮。
  • 6.3 美观 (10%): UI界面美观、用户体验良好。
  • 6.4 文档质量 (15%): 齐全、清晰、规范。
  • 6.5 演示效果 (10%): 流程展示清晰,核心功能演示完整。

🚀 扩展功能加分项 (满分20分)

  • 聊天系统 (难度系数: 1.0) - 实现房间内文字聊天功能
  • AI对战 (难度系数: 1.5) - 实现人机对战,智能程度越高得分越高
  • 自动匹配 (难度系数: 1.2) - 实现玩家自动匹配对手功能
  • 积分系统 (难度系数: 1.2) - 实现胜率统计、积分排行榜

注:扩展功能得分 = 功能完成度 × 难度系数

Part 1: 色彩模块与日志系统

为什么要写这个作业?(Motivation)

工欲善其事,必先利其器。在写核心逻辑前,我们需要:

  • 调试工具 (对于开发者): 清晰的日志(红色的Error,绿色的Success)能让你在成千上万行日志中一眼定位问题。
  • 视觉体验 (对于玩家): 五子棋是黑白分明的。我们如何区分黑子和白子?如何高亮显示对手刚才下在了哪?全靠这里的颜色模块。

核心知识点 (Key Concepts)

1. ANSI 转义序列

通过发送特殊字符 ESC (ASCII 27, \u001B) 来控制终端样式。

  • \u001B[31m: 红色字体
  • \u001B[41m: 红色背景 (高亮关键棋子)
  • \u001B[0m: 重置样式 (Reset)
2. 静态成员 (static)

我们编写的是工具类 (AnsiColor, ServerLogger)。

工具类不需要实例化 (new)。

将方法和常量定义为 public static,可以在任何地方直接调用,如 ServerLogger.info(...)。


任务拆解 (Step-by-Step)

Task 1: 颜色字典 AnsiColor

创建 com.wuzi.common.AnsiColor。

  • 1.1 定义常量:
    public static final String RESET = "\u001B[0m";
    
    // 字体颜色 (Foreground)
    public static final String RED = "\u001B[31m";
    public static final String GREEN = "\u001B[32m";
    public static final String YELLOW = "\u001B[33m";
    public static final String BLUE = "\u001B[34m";
    public static final String MAGENTA = "\u001B[35m";
    public static final String CYAN = "\u001B[36m";
    public static final String WHITE = "\u001B[37m";
    
    // 背景颜色 (Background)
    public static final String BG_RED = "\u001B[41m"; // 高亮背景
  • 1.2 辅助方法:

    防止颜色泄漏,打印完自动重置。

    public static String color(String text, String colorCode) {
        return colorCode + text + RESET;
    }

Task 2: 日志管家 ServerLogger

创建 com.wuzi.server.ServerLogger。

  • 2.1 时间格式化:

    使用 DateTimeFormatter ("yyyy-MM-dd HH:mm:ss")。

  • 2.2 辅助方法 getTimestamp():

    返回如 [2023-10-01 12:00:00] 的字符串。

    建议给时间戳加上颜色(如白色或灰色)以区分日志内容。

  • 2.3 分级打印:
    • info(msg): 时间 + [INFO] (青色) + msg
    • success(msg): 时间 + [SUCCESS] (绿色) + msg
    • error(msg): 时间 + [ERROR] (红色) + msg (建议用 System.err)

验证与评分

验证与测试 (TestHomework1)

public class TestHomework1 {
    public static void main(String[] args) {
        System.out.println("--- 1. 测试颜色工具 ---");
        // 红色字
        System.out.println(AnsiColor.color("Error Text", AnsiColor.RED));
        // 红色背景 (模拟棋盘高亮)
        System.out.println(AnsiColor.color(" Last Move ", AnsiColor.BG_RED));

        System.out.println("\n--- 2. 测试日志格式 ---");
        // 观察输出是否带有 [当前时间]
        ServerLogger.info("服务器正在初始化...");
        ServerLogger.warn("CPU 温度过高 (模拟警告)");
        ServerLogger.error("数据库连接失败!");
        ServerLogger.success("系统启动完成。");
    }
}

评分标准

  • 颜色不泄露 (40%): 调用 color() 后,后续文字颜色必须恢复正常。
  • 系统完整性 (40%): AnsiColor 包含日志所需颜色,且预留了棋盘绘制常量。
  • 格式规范 (20%): 日志必须包含动态变化的时间戳。

Part 2: 核心算法与棋盘模型

为什么要写这个作业?(Motivation)

上一节我们有了“眼睛”(日志),这一节我们要造“大脑”。

五子棋看起来简单——“谁先连成5个谁赢”。但在计算机眼里,它只是一堆数字。

  • 数据建模: 你需要把现实中的棋盘映射到内存中。
  • 算法逻辑: 你需要告诉计算机,怎么才算“连成5个”。

GameBoard 类是整个项目的核心。如果它写错了(比如误判输赢),那么后面的网络对战、房间管理做得再好也是白搭。

核心知识点 (Key Concepts)

1. 二维数组与坐标系

int[15][15] 是最直观的表示。

  • x (row): 0-14 (垂直方向,从上到下)
  • y (col): 0-14 (水平方向,从左到右)
  • 值: 0=空, 1=黑, 2=白
2. 方向向量 (Direction Vectors)

如何检查五子连珠?笨办法是写4个大循环。

聪明办法是利用向量简化代码:

  • 水平: dx=0, dy=1
  • 垂直: dx=1, dy=0
  • 对角: dx=1, dy=1
  • 反斜: dx=1, dy=-1

任务拆解 (Step-by-Step)

Task 1 & 2: 棋盘定义与落子

类:com.wuzi.server.GameBoard

  • 1.1 成员变量:
    • BOARD_SIZE = 15
    • private int[][] board
    • lastMoveX, lastMoveY (用于高亮,也用于未来可能的AI扩展)。
  • 1.2 makeMove (防御性编程):
    1. 检查 x, y 是否越界?(<0 或>=15) -> return false
    2. 检查该位置是否有子?(!=0) -> return false
    3. 一切正常 -> 修改数组, 更新 lastMove。

Task 3: 核心算法 checkWin (重中之重)

五子棋获胜规则: 在横、竖、斜任意方向上连续5个相同颜色的棋子即获胜。

算法优化: 只需检查新落子点周围的4个方向,无需扫描全盘。


可视化、验证与评分

Task 4: toString 可视化

  • 必须使用 Part 1 的 AnsiColor。
  • 两层循环遍历数组:
    • 空(0): 打印 ┼ (黄色/青色)
    • 黑(1): 打印 ● (青色)
    • 白(2): 打印 ○ (白色)

🚀 进阶挑战:高亮最后一步

能不能把最后一步下的棋子背景标红?

实现: 判断 (i, j) == (lastMoveX, lastMoveY)。

如果是,使用 AnsiColor.BG_RED + AnsiColor.CYAN。

验证与评分

// TestHomework2.java
board.reset();
// 边界获胜测试: 在最底下一行连下5个黑子
for(int i=0; i<5; i++) {
     board.makeMove(14, i, "black");
     if (board.checkWin(14, i)) {
         System.out.println("第 " + (i+1) + " 子落下后获胜!");
     }
}
// 越界测试
boolean res = board.makeMove(-1, 0, "black"); // false

评分标准

  • 算法健壮性 (50%): 通过所有方向测试,边缘不抛出 IndexOutOfBoundsException。
  • 防御性编程 (30%): 禁止重复/越界落子。
  • UI美观度 (20%): 棋盘对齐,黑白分明。

Part 3: 面向对象建模

为什么要写这个作业?(Motivation)

在作业 2 中,我们有了一个永远不会犯错的“裁判”(GameBoard)。但现在的游戏是“死”的——没有玩家,没有回合,没有开始和结束。

我们需要引入 面向对象 (OOP) 的核心思想来模拟现实世界:

  • Player (玩家): 有名字、有颜色、有连接。
  • GameRoom (房间): 这就像棋牌室里的一张桌子。它把两个玩家和一副棋盘组合在一起,并负责维护规则。

这个作业将把零散的逻辑组装成一个可运行的游戏闭环。

核心知识点 (Key Concepts)

1. 组合 (Composition)

GameRoom 拥有 (Has-a) 棋盘。

这种设计让我们可以随时更换棋盘(如重置游戏),而不影响房间本身。

2. 线程安全初探

将来 Player1 和 Player2 在不同线程。

如果两人同时点“准备”或同时“落子”?

涉及修改状态的方法(addPlayer, makeMove)需加 synchronized。

3. 状态机 (State Machine)

房间有生命周期,不能在 GameOver 后继续落子。

维护标记位:isGameStarted, isGameOver。


任务拆解 (Step-by-Step)

Task 1: Player 类

简单的实体类 (POJO)。

  • 属性:name, socket (暂null), color ("black"/"white"), currentRoom。
  • 接口:sendMessage(String msg) (暂时用 System.out 模拟)。

Task 2: GameRoom 类 (核心)

  • 2.1 初始化:
    • roomId
    • new GameBoard()
    • playerCount (建议使用 AtomicInteger 以确保线程安全,或在同步块中操作)。
  • 2.2 addPlayer(Player p):
    • 人满(>2)返回 false。
    • 第一个是 p1,第二个是 p2。
    • 更新 p.currentRoom。
  • 2.3 startGame(): 重置棋盘,随机分配颜色,isGameStarted=true。

Task 2: GameRoom (续) - makeMove

makeMove(x, y, color) 五步逻辑

  1. 检查状态: 游戏是否开始?是否结束?
  2. 检查轮次: color 是否等于 currentTurnColor?(防止连下)。
  3. 执行落子: 调用 board.makeMove。
  4. 检查胜负: 调用 board.checkWin。
    • 赢了 -> isGameOver=true。
  5. 切换回合: 没赢 -> 黑变白,白变黑。

验证与评分

验证与测试 (TestHomework3)

不需要网络,左右互搏。

GameRoom room = new GameRoom(888);
Player alice = new Player("Alice", ...);
Player bob = new Player("Bob", ...);

room.addPlayer(alice);
room.addPlayer(bob);
// 假设触发 startGame...
System.out.println("当前执子: " + room.getCurrentTurnColor());

boolean p1Move = room.makeMove(7, 7, "black"); // 黑棋落子
System.out.println("黑棋落子: " + p1Move); // true

boolean p1Again = room.makeMove(7, 8, "black"); // 黑棋企图连下
System.out.println("黑棋连下: " + p1Again); // 应该 false

boolean p2Move = room.makeMove(7, 8, "white"); // 白棋落子
System.out.println("白棋落子: " + p2Move); // true

评分标准

  • 流程严谨 (50%): 严格遵守“一人一步”。黑棋下完再请求必须被拒绝。
  • 状态管理 (30%): 游戏未开始或已结束时不能落子。
  • 数据一致性 (20%): Room 里的棋盘状态必须与玩家操作同步。

Part 4: 资源管理

为什么要写这个作业?(Motivation)

现在你已经能造出一个完美的“对战房间”了。但问题来了:如果有 1000 个玩家同时登录,你难道要手动 new GameRoom() 500次吗?

我们需要一个房间管理器 (RoomManager),就像酒店的大堂经理:

  • 接待: 给新开的房间分配唯一的门牌号 (ID)。
  • 查询: 随时告诉玩家“3号房在这儿”。
  • 清理: 当房间空了,负责回收资源,而不是让它永远占用内存。

核心知识点 (Key Concepts)

1. 集合选型 (List vs Map)
  • List<GameRoom>: 查找 ID=888 需要遍历,复杂度 O(n)。效率低。
  • Map<Integer, GameRoom>: map.get(888),复杂度 O(1)。服务器开发中查找速度至关重要。
2. 线程安全 (Concurrency)
  • ConcurrentHashMap: 强烈推荐。Java为高并发设计的Map,防止多线程读写崩溃。
  • AtomicInteger: 替代 int id++。保证 ID 生成的原子性,多线程下绝不重复。

任务拆解 (Step-by-Step)

Task 1: RoomManager 定义

类:com.wuzi.server.RoomManager

  • 1.1 成员变量:
    private final Map rooms;
    private final AtomicInteger idGenerator;
  • 1.2 构造方法:
    • rooms = new ConcurrentHashMap<>();
    • idGenerator = new AtomicInteger(1);
    • 可选: 预先创建 10 个房间,避免第一批玩家等待。

Task 2: 核心业务方法

  • 创建房间:
    public GameRoom createRoom() {
        int id = idGenerator.getAndIncrement(); // 安全获取ID
        GameRoom room = new GameRoom(id);
        rooms.put(id, room);
        ServerLogger.info("房间 " + id + " 已创建");
        return room;
    }
  • 销毁房间: removeRoom(int id) -> rooms.remove(id) + 日志。
  • 查找与列表:
    • getRoom(int id): 简单代理 Map 的 get。
    • getAllRooms(): 直接返回 Map (或只读视图)。

验证与评分

验证与测试 (TestHomework4)

RoomManager manager = new RoomManager();

System.out.println("--- 1. 创建测试 ---");
GameRoom r1 = manager.createRoom();
GameRoom r2 = manager.createRoom();
System.out.println("R1 ID: " + r1.getRoomId()); // 1
System.out.println("R2 ID: " + r2.getRoomId()); // 2

System.out.println("--- 2. 查找测试 ---");
GameRoom found = manager.getRoom(1); 
System.out.println("查找 ID=1: " + (found == r1)); // true
GameRoom notFound = manager.getRoom(999); // null

System.out.println("--- 3. 销毁测试 ---");
manager.removeRoom(1);
System.out.println("剩余: " + manager.getAllRooms().size()); // 1

评分标准

  • 数据结构选型 (40%): 正确使用 Map 而非 List/Array。
  • ID 生成安全 (30%): 使用 AtomicInteger 确保唯一性。
  • 日志规范 (30%): 创建或销毁房间时必须有日志,体现服务器开发习惯。

Part 5: Socket 网络编程

为什么要写这个作业?(Motivation)

前四个作业都是“单机游戏”。现在的目标是让它们“联网”。

让北京的 Alice 输入 put 5 5,上海的 Bob 屏幕上立刻出现黑子。

这需要 Socket (套接字) —— 网络世界的“电话插孔”。

核心知识点 (Key Concepts)

1. C/S 架构

Server: 像24小时客服中心,一直监听 (ServerSocket)。

Client: 像顾客,主动拨打服务器电话 (Socket)。

2. 多线程并发

主线程只负责“接电话” (accept)。

一旦接通,雇佣新线程 (ClientHandler) 专门陪 Alice 聊天,主线程回到门口等待下一个。

3. 文本协议

约定交流语言:

  • ls rooms
  • enter room <id>
  • put <x> <y>

服务端任务拆解

Task 1: 服务员 ClientHandler

实现 Runnable 接口。

  • 1.1 构造: 接收 Socket 和 RoomManager。
  • 1.2 run() 方法:
    1. 获取输入流(BufferedReader)和输出流(PrintWriter)。
    2. 读取第一行作为名字,创建 Player。
    3. while(true) 循环读取指令。
    4. 解析指令 (split(" ")),调用 GameRoom 逻辑。
    5. 关键: 结果通过 out.println 发回客户端。

Task 2: 总机 GameServer

程序的入口。

public static void main(String[] args) {
    ServerSocket server = new ServerSocket(8888);
    ExecutorService pool = Executors.newCachedThreadPool();
    RoomManager manager = new RoomManager();
    
    ServerLogger.info("监听端口 8888...");
    
    while(true) {
        // 阻塞直到有人连接
        Socket client = server.accept();
        // 把客人交给服务员
        pool.execute(new ClientHandler(client, manager));
    }
}

客户端、联调与评分

Task 3: 玩家终端 GameClient

双线程架构: 为什么?你不知道对手何时落子,不能傻等。

  • 主线程: 读取键盘 (Scanner) -> 发送给服务器。
  • 接收线程: 后台循环读取服务器消息 -> 打印到屏幕。

The Grand Finale (联调步骤)

  1. 启动 Server。
  2. 启动 Client A (Alice)。
  3. 启动 Client B (Bob)。
  4. Alice: ls rooms -> 看到空房间。
  5. Alice: enter room 1 -> 等待。
  6. Bob: enter room 1 -> 游戏开始!
  7. Alice: put 7 7 -> 双方看黑子。
  8. Bob: put 7 8 -> 双方看白子。

评分标准

  • 并发能力 (40%): 服务器必须能同时处理 Alice 和 Bob,不能因 Alice 没说话就卡住 Bob。
  • 协议解析 (30%): 正确处理 ls, enter, put。错误命令(put abc)有提示且不崩溃。
  • 用户体验 (30%): 棋盘刷新及时。断开连接能正确清理资源(踢出房间)。
1 / X