五子棋在线对战
面向对象程序设计(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)
通过发送特殊字符 ESC (ASCII 27, \u001B) 来控制终端样式。
\u001B[31m: 红色字体\u001B[41m: 红色背景 (高亮关键棋子)\u001B[0m: 重置样式 (Reset)
我们编写的是工具类 (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](青色) + msgsuccess(msg): 时间 +[SUCCESS](绿色) + msgerror(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)
int[15][15] 是最直观的表示。
- x (row): 0-14 (垂直方向,从上到下)
- y (col): 0-14 (水平方向,从左到右)
- 值: 0=空, 1=黑, 2=白
如何检查五子连珠?笨办法是写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 = 15private int[][] boardlastMoveX,lastMoveY(用于高亮,也用于未来可能的AI扩展)。
- 1.2 makeMove (防御性编程):
- 检查 x, y 是否越界?(<0 或>=15) -> return false
- 检查该位置是否有子?(!=0) -> return false
- 一切正常 -> 修改数组, 更新 lastMove。
Task 3: 核心算法 checkWin (重中之重)
五子棋获胜规则: 在横、竖、斜任意方向上连续5个相同颜色的棋子即获胜。
算法优化: 只需检查新落子点周围的4个方向,无需扫描全盘。
可视化、验证与评分
Task 4: toString 可视化
- 必须使用 Part 1 的
AnsiColor。 - 两层循环遍历数组:
- 空(0): 打印
┼(黄色/青色) - 黑(1): 打印
●(青色) - 白(2): 打印
○(白色)
- 空(0): 打印
🚀 进阶挑战:高亮最后一步
能不能把最后一步下的棋子背景标红?
实现: 判断 (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)
GameRoom 拥有 (Has-a) 棋盘。
这种设计让我们可以随时更换棋盘(如重置游戏),而不影响房间本身。
将来 Player1 和 Player2 在不同线程。
如果两人同时点“准备”或同时“落子”?
涉及修改状态的方法(addPlayer, makeMove)需加 synchronized。
房间有生命周期,不能在 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 初始化:
roomIdnew 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) 五步逻辑
- 检查状态: 游戏是否开始?是否结束?
- 检查轮次:
color是否等于currentTurnColor?(防止连下)。 - 执行落子: 调用
board.makeMove。 - 检查胜负: 调用
board.checkWin。- 赢了 ->
isGameOver=true。
- 赢了 ->
- 切换回合: 没赢 -> 黑变白,白变黑。
验证与评分
验证与测试 (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)
- List<GameRoom>: 查找 ID=888 需要遍历,复杂度 O(n)。效率低。
- Map<Integer, GameRoom>:
map.get(888),复杂度 O(1)。服务器开发中查找速度至关重要。
- ConcurrentHashMap: 强烈推荐。Java为高并发设计的Map,防止多线程读写崩溃。
- AtomicInteger: 替代
int id++。保证 ID 生成的原子性,多线程下绝不重复。
任务拆解 (Step-by-Step)
Task 1: RoomManager 定义
类:com.wuzi.server.RoomManager
- 1.1 成员变量:
private final Maprooms; 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)
Server: 像24小时客服中心,一直监听 (ServerSocket)。
Client: 像顾客,主动拨打服务器电话 (Socket)。
主线程只负责“接电话” (accept)。
一旦接通,雇佣新线程 (ClientHandler) 专门陪 Alice 聊天,主线程回到门口等待下一个。
约定交流语言:
ls roomsenter room <id>put <x> <y>
服务端任务拆解
Task 1: 服务员 ClientHandler
实现 Runnable 接口。
- 1.1 构造: 接收
Socket和RoomManager。 - 1.2 run() 方法:
- 获取输入流(
BufferedReader)和输出流(PrintWriter)。 - 读取第一行作为名字,创建 Player。
while(true)循环读取指令。- 解析指令 (
split(" ")),调用 GameRoom 逻辑。 - 关键: 结果通过
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 (联调步骤)
- 启动 Server。
- 启动 Client A (Alice)。
- 启动 Client B (Bob)。
- Alice:
ls rooms-> 看到空房间。 - Alice:
enter room 1-> 等待。 - Bob:
enter room 1-> 游戏开始! - Alice:
put 7 7-> 双方看黑子。 - Bob:
put 7 8-> 双方看白子。
评分标准
- 并发能力 (40%): 服务器必须能同时处理 Alice 和 Bob,不能因 Alice 没说话就卡住 Bob。
- 协议解析 (30%): 正确处理
ls,enter,put。错误命令(put abc)有提示且不崩溃。 - 用户体验 (30%): 棋盘刷新及时。断开连接能正确清理资源(踢出房间)。