☰
Java实现华容道:移动判定、数据结构与BFS求解全解析
2026/10/13 20:58:42 网站建设 项目流程

简介:面向Java初学者的华容道小游戏源码包,适合通过实际项目巩固编程基础;以经典棋盘谜题为载体,覆盖语法基础、GUI设计与事件驱动开发,同时涉及类与对象、数组遍历等知识点,适合课堂练习或自学。代码使用Swing组件构建窗口、棋盘面板与按钮,借助ActionListener处理点击事件;棋盘用二维数组表示,移动时包含合法性判定、边界与阻挡判断,能给出非法操作提示,并带有定时动画,便于理解异常处理与Timer用法。压缩包共3个Java源文件,分别承担主控制、界面布局与棋子对象逻辑,大小仅2KB,结构精简,可直接导入IDE阅读;已有414人浏览学习。通过运行代码,可获得完整的华容道程序,理解棋盘初始化、移动判定、胜利判断等思路,并学会拆分界面与逻辑类、使用调试工具定位异常,体会多文件协作与面向对象设计,为独立开发小型游戏打基础。读者还可扩展难度、计分等功能,进一步掌握事件驱动设计。

1. 华容道小游戏用Java做:移动判定这一步最容易劝退新手

很多人第一次动手写Java小游戏,会在贪吃蛇、扫雷、华容道里选一个。华容道看起来是规则最简单的:一个4乘5棋盘,曹操带着几员大将和四个小卒,通过上下左右滑动,把曹操送到出口就算赢。可真用Java从零写一遍,踩坑最多的不是界面,而是移动判定——棋子尺寸不一样,横着的关羽占两格,竖着的张飞占两格,曹操占四格,一次移动要判断整个目标区域有没有越界、有没有被其他棋子占着,写不好就会“能滑出去但棋盘一片乱”。这篇笔记就沿着这个方向把实现链路完整拆开,先立数据结构,再做移动和胜负逻辑,然后接到Swing界面上,最后补上BFS求解和几个已经替你踩过的坑。

2. 棋盘与棋子的类结构:坐标+宽高把滑块抽象清楚

华容道虽然叫“小游戏”,但本质是一个滑块拼图,核心在于“棋子占格”。棋盘固定4列5行,棋子只分四类:1x1的小卒、2x1横将、1x2竖将、2x2曹操。第一次写这个游戏的人,最容易想当然地用(x1,y1,x2,y2)四个坐标表示一个棋子,结果移动判定变成一堆矩形相交判断,代码越写越长。更简洁的建模是:每个棋子记录左上角坐标(row,col)和宽高(width,height),用“左上角+尺寸”表达它占用的所有格子。

2.1 用Piece类把棋子抽象成带宽高的滑块

在设计棋子类之前,先明确棋子的尺寸表,后面所有移动判定都会围绕这张表展开。

棋子宽高占用格子数
曹操224
关羽212
张飞/赵云/马超/黄忠122
兵111

有了这个表,棋子类就不需要为每一种棋子写不同的逻辑,统一用宽度和高度两个字段表达。

public class Piece { public final String name; // 界面显示的名字:曹操、关羽、兵等 public final int width; // 横向占几个格子,只能是 1 或 2 public final int height; // 纵向占几个格子,只能是 1 或 2 public Color color; // 画棋子用的颜色 public int row; // 左上角所在行,从 0 开始 public int col; // 左上角所在列,从 0 开始 public Piece(String name, int width, int height, Color color) { this.name = name; this.width = width; this.height = height; this.color = color; } }

这里row和col存的是左上角,而不是中心点。为什么这样设计?因为“占格子”的语义最直接:以一个宽2高1的关羽为例,只要知道(row,col),它占的格子就是(row,col)和(row,col+1),计算目标区域时不需要额外换算。如果存中心点,移动和碰撞判断时反而要反复把中心转成左上角,边界计算很容易出错。

需要提醒的是,name只用于显示,不要把它当成唯一标识。一盘游戏里会有四个“兵”,它们的name相同但状态独立。如果后面做存档或者BFS求解,我给棋子的建议是额外加一个id字段,或者直接用pieces列表下标作为身份。否则通过字符串去对比棋子,会在“四个兵分不清”这个问题上吃大亏。

2.2 占位矩阵:棋盘状态怎么记录才不容易错

棋子的位置信息都存在Piece里了,但棋盘还需要一个数据结构回答两个问题:某个格子现在被谁占着?某个区域能不能放一个新的棋子?很多初学会选择boolean[][] occupied,但boolean只能表示“有没有棋子”,不能表示“是哪一颗”,一旦棋盘状态异常,排查起来非常痛苦。

我一般直接用一个Piece[][] grid,每个格子存放指向棋子的引用,空着就是null。

public class Board { public static final int ROWS = 5; public static final int COLS = 4; private final Piece[][] grid = new Piece[ROWS][COLS]; public boolean inBoard(int row, int col) { return row >= 0 && row < ROWS && col >= 0 && col < COLS; } public boolean canPlaceAt(int row, int col, int width, int height) { for (int r = row; r < row + height; r++) { for (int c = col; c < col + width; c++) { if (!inBoard(r, c) || grid[r][c] != null) { return false; } } } return true; } }

canPlaceAt是后面移动判定的核心,也是初始化布局时校验配置合法性的工具。它把“越界”和“重叠”放在同一个循环里检查,目标矩形里任何一个格子超出棋盘或被其他棋子占用,就返回false。

这里有一个初学者很容易踩的数组问题:棋盘只有5行,一个height=2的棋子放在row=4,循环会访问grid[5][c],所以必须先经过inBoard判断,否则直接抛数组越界异常。这个判断不能省,也不能靠try-catch掩盖,因为游戏运行时需要频繁调用它,异常开销完全没有必要。

为什么用Piece[][]而不是boolean[][]?调试时如果发现两个棋子重叠,从grid里能直接看到“这个格子指向哪一个棋子”,定位是哪一个棋子没清干净;如果只有boolean,还得把所有棋子坐标拉出来逐一比对,效率低很多。后面做BFS求解时,把Piece[][]编码成字符串状态也顺理成章,每个格子的字符就是对应棋子的符号,这个优势到第6章会体现出来。

2.3 开局布局:把坐标从代码里拆出来

开局布局有两个要求:一是棋子的位置不能随意写死,二是最好让一份布局配置能复用。直接在main里new十个Piece再挨个set坐标也能跑,但每增加一个关卡就要改代码重新编译,不灵活。

我建议把开局配置做成一个列表,每一项表示“哪个名字的棋子、放在第几行第几列”,初始化时统一创建。下面这个“横刀立马”开局是华容道最经典的配置,曹操在中间上方,关羽横在曹操正下方挡住出口,四员竖将守在两侧,四个小卒分列下方左右。

public static List<InitialPiece> createInitialLayout() { // 开局名称:横刀立马 // 格式:名字, 左上角行, 左上角列, 宽, 高 return List.of( new InitialPiece("曹操", 1, 1, 2, 2), new InitialPiece("关羽", 3, 1, 2, 1), new InitialPiece("张飞", 1, 0, 1, 2), new InitialPiece("赵云", 1, 3, 1, 2), new InitialPiece("马超", 3, 0, 1, 2), new InitialPiece("黄忠", 3, 3, 1, 2), new InitialPiece("兵", 2, 0, 1, 1), new InitialPiece("兵", 2, 3, 1, 1), new InitialPiece("兵", 4, 0, 1, 1), new InitialPiece("兵", 4, 3, 1, 1) ); }

InitialPiece可以理解为一个只保存配置数据的简单对象,启动时遍历这个列表创建真正的Piece,再逐个调用canPlaceAt校验。如果某个棋子摆到了已经占用的格子上,直接抛出带位置的异常,避免“带病开局”。

用这个方式扩展新关卡时,只需要往游戏里添加一个静态方法返回不同的List ,不用动任何游戏逻辑。这里的坐标是以左上角为基准,所以曹操放在(1,1),宽2高2,实际占第1、2行和第1、2列,棋盘4列5行正好对称,视觉上是在棋盘正中偏上的位置。

3. 移动判定与胜负逻辑:不越界、不重叠,才能动得干净

这一章是整个游戏的核心。很多人把华容道写成了“看起来能玩但状态很乱”,多半是移动判定偷懒了。先定义方向,再讲目标矩形扫描,最后落到移动、悔棋和胜利判定。

3.1 移动判定:目标矩形不越界、不重叠才算合法

华容道的移动是一次滑动一格,所以移动一个棋子,本质就是把它左上角的(row,col)按方向加上一个偏移量。向上row-1,向下row+1,向左col-1,向右col+1。先把方向枚举写出来,后面到处都要用。

public enum Direction { UP(-1, 0), DOWN(1, 0), LEFT(0, -1), RIGHT(0, 1); public final int dr; public final int dc; Direction(int dr, int dc) { this.dr = dr; this.dc = dc; } }

有了方向之后,移动判定不要用“目标格子是否为空”来判断。曹操这种2x2棋子向上挪一格时,新位置和旧位置有一行重叠,重叠区域里还是它自己;如果不考虑这一点,棋子一动就被自己挡住。正确做法是先算出完整的目标矩形,再逐个格子检查“越界”和“非自身占用”。

public boolean canMove(Piece p, Direction dir) { // 目标位置:左上角移动到新坐标 int nRow = p.row + dir.dr; int nCol = p.col + dir.dc; // 目标矩形右下角 int endRow = nRow + p.height - 1; int endCol = nCol + p.width - 1; // 左上角和右下角都必须落在棋盘内 if (!inBoard(nRow, nCol) || !inBoard(endRow, endCol)) { return false; } // 目标矩形内不能有别的棋子,但可以包含自己 for (int r = nRow; r <= endRow; r++) { for (int c = nCol; c <= endCol; c++) { Piece occupied = grid[r][c]; if (occupied != null && occupied != p) { return false; } } } return true; }

关键就在 occupied != p 这一句。2x2棋子向上移动,新矩形是第0行到第1行,旧矩形是第1行到第2行,第1行的两个格子同时属于新旧矩形;如果grid里存着p却又不排除自身,曹操就被自己挡住了。这个细节是最容易让人翻车的地方,很多人写着写着发现大棋子完全动不了,基本都是这个问题。

另外,边界检查要检查左上角和右下角两个点。只查nRow和nCol的话,一个1x2竖将向下移动时,nRow可能还在棋盘里,endRow却已经等于5了,同样越界。把这条规则统一成“目标矩形扫描”后,四种尺寸的棋子共用同一段逻辑,不用按棋子类型写特例,后面BFS求解时生成合法移动也省心。

3.2 执行移动与悔棋:先清旧格子,再写新格子

判定通过后执行移动。顺序有讲究:先remove清掉旧占位,更新坐标,再place写入新占位。如果先改坐标再remove,remove时用的就是新坐标,旧格子里的引用永远清不掉。

public void movePiece(Piece p, Direction dir) { if (!canMove(p, dir)) { return; } removePiece(p); // 先清旧位置 p.row += dir.dr; p.col += dir.dc; placePiece(p); // 再写新位置 steps++; // 有效移动步数加一 } public void removePiece(Piece p) { for (int r = p.row; r < p.row + p.height; r++) { for (int c = p.col; c < p.col + p.width; c++) { grid[r][c] = null; } } } public void placePiece(Piece p) { for (int r = p.row; r < p.row + p.height; r++) { for (int c = p.col; c < p.col + p.width; c++) { grid[r][c] = p; } } }

removePiece必须遍历棋子当前占用的所有格子。一个2x2曹操占用4个格子,如果只写grid[p.row][p.col] = null,另外三个格子会残留引用,棋盘上就会出现“一个棋子占两片区域”的诡异效果。placePiece同理,遍历整个矩形把格子都写入引用,这一步不能省。

悔棋是华容道游戏标配的“后悔药”。我建议移动时顺便记录一个MoveRecord,而不是在悔棋时把方向反过来执行一次。反向移动可能因为目标位置被其他棋子占住而不合法,而记录移动前后坐标后,撤销时可以强制把棋子放回旧位置,完全不做合法性判断,逻辑简单可靠。

public void undo() { if (undoStack.isEmpty()) return; MoveRecord record = undoStack.pop(); removePiece(record.piece); record.piece.row = record.fromRow; // 直接回到移动前的位置 record.piece.col = record.fromCol; placePiece(record.piece); steps--; }

MoveRecord里至少要有piece、fromRow、fromCol、toRow、toCol五个信息。这里的撤销只在最后一次移动后调用,而旧区域是刚空出来的,所以不会被其他棋子占用,直接设置坐标是安全的。

3.3 胜利判定与步数统计

标准华容道的出口在棋盘底部中间,曹操2x2要占据第3行第1列到第4行第2列,也就是左上角坐标为(3,1)时游戏胜利。

public boolean isWin() { return caoCao.row == 3 && caoCao.col == 1; }

如果你的设计把出口放在顶部,把3改成0就行,其他逻辑完全不用动。为了界面上能明确看到出口,我会在paintComponent里把目标矩形画成一个浅色底框,曹操滑到那里时触发胜利流程,比如弹窗显示用时和步数。

步数统计的口径要提前想好:一种是只统计有效移动次数,movePiece执行一次就加一;另一种是“从开始到当前状态经过的步数”,悔棋时要减一。上面的undo里已经同步做了steps--,两种口径保持一致。

有一个细节新手容易忽略:胜利判定不能写成p.row == 4。曹操高2,如果左上角row等于4,它就已经超出棋盘下边界了,这局不可能赢。所以目标坐标应该用“左上角到达出口起始格”来判断,而不是“棋子的中心点”或“右下角到达边界”。如果只判曹操的左上角位置,当row=3且col=1时,它已经占住出口的四格,判定成立,这也符合玩家认知。

4. Swing界面:画棋盘、接鼠标键盘,把游戏跑起来

数据结构做好之后,界面部分反而简单,核心就是把棋子的逻辑坐标翻译成像素坐标。Swing方案我建议直接用JFrame + JPanel,重写paintComponent绘制所有元素,不要拖一堆JButton堆界面。

4.1 paintComponent绘制棋盘

绘制思路很直接:按固定的CELL_SIZE计算格子大小,先把棋盘底色画出来,再画出口区域,最后遍历pieces逐个绘制棋子。

public class GamePanel extends JPanel { private final Board board; private static final int CELL_SIZE = 90; @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 棋盘底色 g.setColor(new Color(222, 184, 135)); g.fillRect(0, 0, Board.COLS * CELL_SIZE, Board.ROWS * CELL_SIZE); // 画出口区域:底部中间两格 g.setColor(new Color(255, 215, 0, 80)); g.fillRect(1 * CELL_SIZE, 3 * CELL_SIZE, 2 * CELL_SIZE, 2 * CELL_SIZE); // 绘制棋子 for (Piece p : board.getPieces()) { g.setColor(p.color); g.fillRect(p.col * CELL_SIZE, p.row * CELL_SIZE, p.width * CELL_SIZE, p.height * CELL_SIZE); g.setColor(Color.WHITE); // 让名字尽量居中 g.drawString(p.name, p.col * CELL_SIZE + (p.width * CELL_SIZE - g.getFontMetrics().stringWidth(p.name)) / 2, p.row * CELL_SIZE + (p.height * CELL_SIZE + g.getFontMetrics().getAscent()) / 2); } } }

这段代码里,p.col乘以CELL_SIZE就是棋子的左边距,p.row乘以CELL_SIZE就是上边距,宽度和高度也各乘CELL_SIZE。逻辑坐标和像素坐标分离后,GamePanel里不需要知道棋子具体是曹操还是小卒,界面代码就稳定了。

CELL_SIZE决定了窗口大小,如果要自适应缩放,可以把CELL_SIZE改成根据getWidth()和getHeight()动态计算的变量,但要注意字体大小也要跟着缩放,否则窗口拉大后文字不会同步变大。对华容道这种4x5的小棋盘,固定尺寸更省心,直接setPreferredSize(new Dimension(Board.COLS * CELL_SIZE, Board.ROWS * CELL_SIZE))即可。

出口区域我用半透明黄色标出来,玩家一眼就能看到要把曹操送到哪里。Color的第四个参数是Alpha透明度,JDK的Swing是原生支持的。如果你改用JavaFX,绘制API完全不同,但整体思路一致:先画棋盘,再画棋子,最后画装饰。

4.2 鼠标选中和键盘操作的接入

交互建议做成鼠标点选棋子、方向键移动的组合。这样玩家一只手指就能完成大部分操作,也方便后续增加“自动走一步”的辅助功能。

panel.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { int row = e.getY() / CELL_SIZE; int col = e.getX() / CELL_SIZE; selectedPiece = board.findPieceAt(row, col); panel.repaint(); } });

findPieceAt返回grid[row][col]上的棋子引用,可能是null。点击空白就取消选中,点击棋子就选中它。被选中后,可以在paintComponent里给棋子加一个高亮外框,否则玩家不知道当前操作的是谁。

键盘方面,我强烈建议不要用KeyListener,而是用InputMap/ActionMap。

panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) .put(KeyStroke.getKeyStroke("LEFT"), "moveLeft"); panel.getActionMap().put("moveLeft", new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { moveSelected(Direction.LEFT); } });

很多人写Swing键盘交互时用KeyListener,结果发现鼠标点了一下按钮后方向键全无反应,原因是焦点跑到了按钮上,KeyEvent根本没有派发给面板。而WHEN_IN_FOCUSED_WINDOW模式下,只要事件发生在当前窗口内,不管焦点在哪个组件上都能触发,省去到处调requestFocus的麻烦。

moveSelected里还可以做一个很实用的细节:如果当前没有选中棋子,就自动找第一个“能往该方向移动”的棋子并移动它。

private void moveSelected(Direction dir) { if (selectedPiece == null) { for (Piece p : board.getPieces()) { if (board.canMove(p, dir)) { board.movePiece(p, dir); break; } } } else { board.movePiece(selectedPiece, dir); } panel.repaint(); }

这个交互习惯在手机上玩过华容道的人会很熟悉:按方向键不一定是移动当前选中的棋子,而是移动那个方向能走的棋子。桌面版本保留这个习惯,新手随便按方向键也能走第一步,不会觉得“怎么没反应”。

4.3 repaint与后台任务的基本礼仪

Swing是单线程模型,所有UI操作都要放在事件分发线程EDT里。paintComponent结束后,如果有耗时的逻辑在循环里频繁调用repaint,界面看起来会“忙”,但真正的卡顿往往是逻辑阻塞了EDT,而不是绘制本身。

一个值得养成的习惯:凡是计算可能超过一两秒的,都别直接写在事件回调里。第6章的BFS求解就是典型例子,如果直接在按钮点击事件里跑BFS,窗口会立刻变成“未响应”,直到求解完成才恢复。正确做法是用SwingWorker把重计算放到后台线程,在done()里更新结果。

移动完棋子,我通常只调用panel.repaint(),不做局部刷新。4x5的棋盘上整板重绘开销很小,代码也简单。如果以后扩展到更大的滑块拼图,再考虑局部绘制或双缓冲优化,对华容道这个规模来说,优化得太早反而增加复杂度。

5. 华容道Java小游戏避坑:越界、焦点、残留与无解开局

这一章写几个实际调试中很容易遇到的现象,每一条都按“现象、原因、解决”的顺序记录,方便你对照排查。

5.1 移动和状态类的三个典型坑:越界、残留、撤销错乱

坑一:棋子飞出了棋盘边界。现象是棋子可以移动到棋盘外,一半身体在外面,甚至直接消失不见。原因基本只有一个:移动判定只检查了左上角的新位置,没有检查右下角。解决方法是把canMove里的边界检查写成同时检查nRow/nCol和endRow/endCol两端,任何一个越界都返回false。尤其要注意往下和往右移动时,右下角最容易越界。

坑二:旧位置残留。现象是曹操从中间移走后,原位置还留着一块颜色,棋盘上出现了两个曹操一样的重影。原因是removePiece只清了一个格子,比如只写了grid[p.row][p.col] = null,2x2棋子还有三个格子保留着引用。解决方法是removePiece和placePiece都写成双重循环,把整个矩形全部置null或写入引用;移动顺序必须是remove、改坐标、place三步,顺序不能换。

坑三:撤销后步数和状态对不上。现象是点悔棋,棋子确实回去了,但步数不减,或者连续撤销两次后棋子叠在一起。原因是撤销栈里只存了方向,执行时用反向移动去恢复,一旦目标位置被其他棋子挡住,撤销就失败;或者步数没有同步减。解决方法是MoveRecord里记录移动前后坐标,undo直接设置坐标并重放占位,不再判断合法性,同时同步执行steps--。

这三条如果按顺序排查,能省下一个星期的调试时间。我自己的习惯是每次改动移动逻辑后,先跑一遍针对2x2曹操的上下左右移动,再跑一遍四个小卒的移动,确认没有越界和残留后再接UI。

5.2 交互和关卡的两个典型坑:键盘无响应、自定义布局无解

坑四:键盘方向键完全没反应。现象是鼠标点击能够选中棋子,但按上下左右键,游戏毫无反应。原因大多是用了KeyListener且焦点不在GamePanel上。解决方法是改用InputMap/ActionMap并设置WHEN_IN_FOCUSED_WINDOW,不要依赖焦点。还有一个容易被忽略的点:如果界面上有按钮,按下方向键时焦点可能会跑到按钮上,这时即便用KeyListener也收不到事件。换成ActionMap后,这个坑就基本消失了。

坑五:自定义关卡跑半天也赢不了。现象是新增关卡时随手摆了一些棋子,玩家走了很多步,还是到不了终点,程序也不报错。原因是滑块拼图不是随便摆就一定可解的,有些布局从初始状态到目标状态根本不可达。解决方法是把BFS求解器集成到关卡编辑器里,启动关卡前跑一次求解,无解就提示“当前布局无解”。这比让玩家试错要友好得多。

要注意的是,用canPlaceAt校验初始位置不重叠、不越界只能排除明显非法的配置,不代表一定有解。真正可靠的可解性判定还是第6章的BFS。如果一个自定义关卡的棋子数量或出口位置设置得很怪异,BFS会搜索完整状态空间,然后告诉你这个布局不可达,省去玩家反复折腾的挫败感。

6. 进阶玩法:用BFS算最少步数,顺手把存档加上

到了这一步,基础游戏已经能玩了,但还可以再加一个真正体现价值的功能:最少步求解。华容道每走一步代价都是1,BFS天然适合找最短路径,第一次到达目标状态的路径就是最优解。

6.1 BFS求解的核心:状态压缩与去重

BFS去重不能直接拿棋盘对象做HashMap key,必须先把状态压缩成字符串。做法是按行扫描4x5的grid,每个格子用对应棋子的符号表示,空格用点号。

public String encode() { char[][] map = new char[Board.ROWS][Board.COLS]; for (char[] row : map) Arrays.fill(row, '.'); for (Piece p : pieces) { char symbol = symbolOf(p); // 曹操C、张飞Z、兵B等 for (int r = p.row; r < p.row + p.height; r++) { for (int c = p.col; c < p.col + p.width; c++) { map[r][c] = symbol; } } } StringBuilder sb = new StringBuilder(); for (char[] row : map) sb.append(row); return sb.toString(); }

BFS主循环用HashMap记录前驱状态,等于同时完成去重和路径回溯。expand过程就是还原棋盘、尝试每个棋子四个方向、生成新编码,具体逻辑可以直接复用第3章的canMove和movePiece。

public List<String> bfs(String start, String goal) { Deque<String> queue = new ArrayDeque<>(); Map<String, String> prev = new HashMap<>(); queue.offer(start); prev.put(start, null); while (!queue.isEmpty()) { String cur = queue.poll(); if (cur.equals(goal)) break; for (String next : expand(cur)) { if (!prev.containsKey(next)) { prev.put(next, cur); queue.offer(next); } } } if (!prev.containsKey(goal)) return null; // 无解 LinkedList<String> path = new LinkedList<>(); for (String s = goal; s != null; s = prev.get(s)) { path.addFirst(s); } return path; }

经典开局的状态量其实并不大,普通内存完全能扛住。等到开发关卡编辑器时,这个BFS就能顺手用来做无解校验,给玩家一个有意义的提示。

6.2 把求解做成提示按钮,并放在后台线程

求解器直接放在按钮回调里会把Swing界面卡死,这个问题我在自己项目里踩过。正确做法是用SwingWorker在后台跑BFS,完成后回到EDT更新界面,再把第一步动作高亮显示,玩家就能照着提示走。

6.3 存档和关卡编辑器的扩展方向

存档可以把每个棋子的名称、row、col、宽高写成一行文本,读取时按行恢复并调用canPlaceAt自检。关卡编辑器则是一块能拖拽棋子的画布,拖完之后先做初始化合法性校验,再用BFS判断是否可解,做到这一步,这个游戏就不只是“小游戏”,而是一个完整的Java桌面应用练习了。

一通操作下来,你会发现华容道最难的并不是画面,而是状态表达和对移动规则的敬畏。我自己的教训是:先花半天把Board和move逻辑想清楚,界面很快就出来了;反过来先做UI再补逻辑,大多会把时间浪费在反复调试上。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询