中国象棋是流传已久的传统益智棋类游戏,有着严谨完备的行走规则、独特的博弈逻辑。平日里时常进行象棋对弈,于是萌生了使用Java Swing GUI图形框架,复刻双人本地对战象棋程序的想法。
本次项目摒弃现成游戏引擎,依靠原生Java语法从零开发,利用二维数组存储整张棋盘的棋子排布信息,搭配鼠标监听器完成选子、预览落点、移动棋子、吃子整套交互。逐一编写车、马、炮、兵、士、象、将帅全部棋子的专属行走逻辑,同时实现别马腿、炮隔子打子、将帅不能隔空对视、红黑两方交替落子等核心对局规则,完整还原线下真人下棋的游玩体验。
撰写本篇开发手记,一方面梳理GUI界面搭建、事件监听、多层逻辑判断的知识点,复盘开发过程里遇到的落子失效、回合判定错乱、规则判断异常等各类问题;另一方面借助游戏项目实操,锻炼自身逻辑梳理与代码架构设计能力。后续计划在此基础上拓展对局存档、简易人机对战、历史对局回看等功能,进一步完善整套象棋程序。
一.项目概述:传统象棋游戏功能清单
(一)核心基础功能(已实现)
- 车:沿横竖直线行走,需判断路径上是否存在棋子阻挡。
- 炮:无棋子阻挡时可直线行走;吃子时,必须与目标棋子间隔恰好一颗棋子(俗称“炮架”)。
- 马:走“日”字形,并实现了精准的“蹩马腿”拦截判定。
- 象:走“田”字,受“象眼”规则限制,且不能过河。
- 士:仅限在九宫格内沿斜线移动。
- 将/帅:在九宫格内移动,并实现了“王不见王”规则,即双方将帅不能在同一直线上隔空相对。
- 兵/卒:过河前只能向前直行;过河后,可向前、向左、向右移动。
(二)后续可拓展规划功能(体现项目延展性)
二.项目开发环境
本项目基于 Java 标准库开发,主要依赖以下核心包:
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
- javax.swing.*:用于构建图形用户界面(GUI),包括窗口、面板、按钮、标签等组件。
- java.awt.*:提供基础的绘图、布局管理和事件处理框架。
- java.awt.event.MouseEvent 与 java.awt.event.MouseListener:用于监听和处理鼠标点击、移动等交互事件,是实现棋子选取与移动的核心。
同时,为了后期程序维护和新功能加入,我们将采取封装的知识,将所有的方法写在chessTools类中,在运用地到的地方直接调用
三. 整体项目架构划分(窗体层、数据层、鼠标监听逻辑层)
窗体层(View Layer):负责图形界面的绘制与刷新。为实现棋盘和棋子的实时渲染,我们采用继承(extends)机制重写绘制方法,动态更新画面。为保持代码结构清晰,所有与界面绘制相关的核心逻辑均封装在 chessTools 工具类中,便于统一调用和维护。
数据层(Model Layer):管理棋盘与棋子的核心状态数据。每颗棋子均包含其位置信息(行 row、列 column)和阵营标识(chessFlag)。这些数据不仅是游戏逻辑运算的基础,也为后续功能(如悔棋、对局回放)提供了必要的数据支撑。关于悔棋功能的具体实现,可参考类似项目(如五子棋4.0)中的数据结构与算法设计。
鼠标监听器层(Controller Layer):处理用户交互事件,将物理坐标转换为棋盘逻辑坐标。该层主要监听鼠标的按下(mousePressed)与释放(mouseReleased)事件:在 mousePressed 中调用棋子选中逻辑,在 mouseReleased 中触发棋子移动与规则判定,从而完成“点击-选中-移动”的完整交互闭环。
四. 棋盘与棋子的数据结构设计(二维数组存储棋子信息)
棋子编码与棋盘参数定义
为在程序中精确表示棋盘状态,我们定义了以下核心数据:
- 棋子编码:使用整数编码区分棋子类型与阵营。
- 0:空位
- 1 – 7:黑方棋子(将、士、象、马、车、炮、卒)
- 11 – 17:红方棋子(帅、仕、相、马、车、炮、兵)
- 棋盘布局参数:
- 棋盘左上角坐标:X = 100, Y = 100
- 格子尺寸:Size = 80
- 棋盘行列数:Row = 10, Col = 9
核心数据结构:二维数组 chessArrSrc
棋盘的初始状态通过一个二维整型数组 chessArrSrc 来定义,它直观地映射了象棋的标准开局布局。
int[][] chessArrSrc = {
{5, 4, 3, 2, 1, 2, 3, 4, 5}, // 黑方底线:车、马、象、士、将、士、象、马、车
{0, 0, 0, 0, 0, 0, 0, 0, 0}, // 空行
{0, 6, 0, 0, 0, 0, 0, 6, 0}, // 黑方炮位
{7, 0, 7, 0, 7, 0, 7, 0, 7}, // 黑方卒位
{0, 0, 0, 0, 0, 0, 0, 0, 0}, // 空行(楚河汉界)
{0, 0, 0, 0, 0, 0, 0, 0, 0}, // 空行(楚河汉界)
{17, 0, 17, 0, 17, 0, 17, 0, 17}, // 红方兵位
{0, 16, 0, 0, 0, 0, 0, 16, 0}, // 红方炮位
{0, 0, 0, 0, 0, 0, 0, 0, 0}, // 空行
{15, 14, 13, 12, 11, 12, 13, 14, 15} // 红方底线:车、马、相、仕、帅、仕、相、马、车
};
为了在游戏过程中独立操作棋盘状态而不影响初始布局,我们通过 getChessArrSrc() 方法复制一份初始数组作为游戏进行中的棋盘数据副本。
int[][] chessArr; // 游戏进行中的棋盘状态副本
public int[][] getChessArrSrc() {
int[][] chessArr = new int[10][9];
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 9; j++) {
chessArr[i][j] = chessArrSrc[i][j];
}
}
return chessArr;
}
五. 界面GUI搭建:棋盘底色、棋子绘制、窗体布局设计
棋盘绘制方法 drawChessBoard
绘制棋盘时,绘制顺序至关重要:必须先绘制白色背景底板,再绘制黑色网格线,最后覆盖“楚河汉界”区域和文字。
public void drawChessBoard(Graphics g) {
// 1. 绘制白色背景
g.setColor(Color.white);
g.fillRect(X – Size / 2, Y – Size / 2, Col * Size + Size, Row * Size + Size);
// 2. 绘制黑色棋盘网格线
g.setColor(Color.BLACK);
// 绘制横线
for (int i = 0; i < 10; i++) {
g.drawLine(X, Y + i * Size, X + (Col – 1) * Size, Y + i * Size);
}
// 绘制竖线
for (int i = 0; i < 9; i++) {
g.drawLine(X + i * Size, Y, X + i * Size, Y + (Row – 1) * Size);
}
// 3. 绘制“楚河汉界”区域
g.setColor(Color.YELLOW);
g.fillRect(X + 2, Y + Size * 4 + 1, (Col – 1) * Size – 4, Size – 2);
// 4. 绘制“楚河汉界”文字
String str1 = "楚", str2 = "河", str3 = "汉", str4 = "界";
g.setColor(Color.BLACK);
g.setFont(new Font("微软雅黑", Font.BOLD, 36));
g.drawString(str1, X + 20 + Size, Y + Size * 4 + 50);
g.drawString(str2, X + 20 + Size * 2, Y + Size * 4 + 50);
g.drawString(str3, X + 20 + Size * 5, Y + Size * 4 + 50);
g.drawString(str4, X + 20 + Size * 6, Y + Size * 4 + 50);
}
棋子绘制方法 drawChess
该方法根据棋子编码(type)在指定坐标(x, y)绘制棋子。棋子由橙色圆形背景和中央的汉字标识组成,颜色(红/黑)由编码值决定。
public void drawChess(Graphics g, int x, int y, int type) {
// 1. 绘制橙色圆形背景
g.setColor(Color.ORANGE);
g.fillOval(x, y, 80, 80);
// 2. 根据棋子编码设置文字颜色(红方或黑方)
if (type <= 7) {
g.setColor(Color.red); // 黑方棋子用红色字(传统显示习惯)
} else {
g.setColor(Color.BLACK); // 红方棋子用黑色字
}
// 3. 根据编码映射到对应的汉字
String str = "";
switch (type) {
case 1 -> str = "将";
case 2 -> str = "士";
case 3 -> str = "象";
case 4 -> str = "马";
case 5 -> str = "车";
case 6 -> str = "炮";
case 7 -> str = "卒";
case 11 -> str = "帅";
case 12 -> str = "仕";
case 13 -> str = "相";
case 14 -> str = "马";
case 15 -> str = "车";
case 16 -> str = "炮";
case 17 -> str = "兵";
}
// 4. 绘制棋子汉字
Font font = new Font("楷体", Font.BOLD, 36);
g.setFont(font);
g.drawString(str, x + 20, y + 50);
}
六. 对局核心机制:红黑交替落子与回合判定逻辑实现
对局的核心机制之一是确保红黑双方严格交替落子,这通过一个简单的布尔标志 isRedTurn 来控制。其核心思想是:在每次有效的落子操作后,切换当前回合的归属方。
回合状态变量定义
在游戏主类或逻辑控制类中,我们定义一个布尔类型的成员变量来记录当前回合:
// 用于标识当前是否为红方回合,true 表示红方,false 表示黑方
private boolean isRedTurn = true; // 默认红方先手
回合判定与切换逻辑
回合判定逻辑主要集成在鼠标监听器的 mouseReleased 事件处理中,在确认一次合法的棋子移动或吃子操作后执行。其流程如下:
// 在 mouseReleased 事件处理函数中的简化逻辑示例
public void mouseReleased(MouseEvent e) {
// … 之前的代码:计算目标行列,验证移动合法性 …
if (moveIsLegal) {
// 执行移动:更新棋盘数组 chessArr
chessArr[targetRow][targetCol] = chessArr[selectedRow][selectedCol];
chessArr[selectedRow][selectedCol] = 0;
// 核心:切换回合
isRedTurn = !isRedTurn;
// 重绘界面,刷新棋盘和棋子
repaint();
// (可选) 可以在这里添加回合提示,例如更新状态栏文本
// statusLabel.setText(isRedTurn ? "红方回合" : "黑方回合");
}
// … 后续清理(如清空选中状态)…
}
关键细节与边界处理
逻辑流程图
以下 Mermaid 流程图清晰地展示了从玩家点击到回合切换的完整决策过程:
#mermaid-svg-K7VB9D86AB0Hjj8F{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-K7VB9D86AB0Hjj8F .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-K7VB9D86AB0Hjj8F .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-K7VB9D86AB0Hjj8F .error-icon{fill:#552222;}#mermaid-svg-K7VB9D86AB0Hjj8F .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-K7VB9D86AB0Hjj8F .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-K7VB9D86AB0Hjj8F .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-K7VB9D86AB0Hjj8F .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-K7VB9D86AB0Hjj8F .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-K7VB9D86AB0Hjj8F .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-K7VB9D86AB0Hjj8F .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-K7VB9D86AB0Hjj8F .marker{fill:#333333;stroke:#333333;}#mermaid-svg-K7VB9D86AB0Hjj8F .marker.cross{stroke:#333333;}#mermaid-svg-K7VB9D86AB0Hjj8F svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-K7VB9D86AB0Hjj8F p{margin:0;}#mermaid-svg-K7VB9D86AB0Hjj8F .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-K7VB9D86AB0Hjj8F .cluster-label text{fill:#333;}#mermaid-svg-K7VB9D86AB0Hjj8F .cluster-label span{color:#333;}#mermaid-svg-K7VB9D86AB0Hjj8F .cluster-label span p{background-color:transparent;}#mermaid-svg-K7VB9D86AB0Hjj8F .label text,#mermaid-svg-K7VB9D86AB0Hjj8F span{fill:#333;color:#333;}#mermaid-svg-K7VB9D86AB0Hjj8F .node rect,#mermaid-svg-K7VB9D86AB0Hjj8F .node circle,#mermaid-svg-K7VB9D86AB0Hjj8F .node ellipse,#mermaid-svg-K7VB9D86AB0Hjj8F .node polygon,#mermaid-svg-K7VB9D86AB0Hjj8F .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-K7VB9D86AB0Hjj8F .rough-node .label text,#mermaid-svg-K7VB9D86AB0Hjj8F .node .label text,#mermaid-svg-K7VB9D86AB0Hjj8F .image-shape .label,#mermaid-svg-K7VB9D86AB0Hjj8F .icon-shape .label{text-anchor:middle;}#mermaid-svg-K7VB9D86AB0Hjj8F .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-K7VB9D86AB0Hjj8F .rough-node .label,#mermaid-svg-K7VB9D86AB0Hjj8F .node .label,#mermaid-svg-K7VB9D86AB0Hjj8F .image-shape .label,#mermaid-svg-K7VB9D86AB0Hjj8F .icon-shape .label{text-align:center;}#mermaid-svg-K7VB9D86AB0Hjj8F .node.clickable{cursor:pointer;}#mermaid-svg-K7VB9D86AB0Hjj8F .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-K7VB9D86AB0Hjj8F .arrowheadPath{fill:#333333;}#mermaid-svg-K7VB9D86AB0Hjj8F .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-K7VB9D86AB0Hjj8F .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-K7VB9D86AB0Hjj8F .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K7VB9D86AB0Hjj8F .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-K7VB9D86AB0Hjj8F .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K7VB9D86AB0Hjj8F .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-K7VB9D86AB0Hjj8F .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-K7VB9D86AB0Hjj8F .cluster text{fill:#333;}#mermaid-svg-K7VB9D86AB0Hjj8F .cluster span{color:#333;}#mermaid-svg-K7VB9D86AB0Hjj8F div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-K7VB9D86AB0Hjj8F .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-K7VB9D86AB0Hjj8F rect.text{fill:none;stroke-width:0;}#mermaid-svg-K7VB9D86AB0Hjj8F .icon-shape,#mermaid-svg-K7VB9D86AB0Hjj8F .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K7VB9D86AB0Hjj8F .icon-shape p,#mermaid-svg-K7VB9D86AB0Hjj8F .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-K7VB9D86AB0Hjj8F .icon-shape .label rect,#mermaid-svg-K7VB9D86AB0Hjj8F .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K7VB9D86AB0Hjj8F .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-K7VB9D86AB0Hjj8F .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-K7VB9D86AB0Hjj8F :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
否
是
否
是
否
是
是
否
玩家鼠标按下
点击位置有棋子?
忽略,无操作
棋子属于当前回合方?(isRedTurn 与棋子编码匹配)
提示“请等待对方回合”
记录选中棋子,显示可移动位置
玩家鼠标释放
释放位置是合法落点?
取消选中,不清除回合
执行移动/吃子,更新棋盘数据
切换 isRedTurn = !isRedTurn
重绘界面
检测游戏是否结束?
锁定回合,显示获胜信息
等待下一次操作
通过一个布尔变量 isRedTurn 和与之配套的判定逻辑,我们实现了象棋对局中最基础的回合制交替机制。这是所有后续复杂规则(如将军、困毙)得以正确运行的前提。将此逻辑清晰地封装在控制器(Controller)层,保证了游戏状态变更与用户界面刷新的同步,为项目后续添加悔棋、回放等高级功能奠定了良好的数据流基础。
在接下来的博客中,我们将继续书写棋子的行走规则和其他内容,继续加油!
网硕互联帮助中心![P1014 [NOIP 1999 普及组] Cantor 表-网硕互联帮助中心](https://www.wsisp.com/helps/wp-content/uploads/2026/08/20260811104049-6a7afc31a3c58-220x150.png)


评论前必须登录!
注册