项目背景详细介绍
推箱子(Sokoban)是一款经典的益智游戏,最早由日本程序员仓田智昭(Toshirō Takahashi)于 1981 年设计,后在 1982 年由 Thinking Rabbit 发行并流行于全球。游戏的核心玩法简单:玩家(仓库管理员)需要在迷宫一样的仓库中,将箱子推到指定的目标位置。尽管规则简单,但在有限空间下怎样避免将箱子卡住、如何规划最少步数牵动人们的思维,因而成为风靡全球的脑力游戏之一。
推箱子游戏具有以下特性:
-
空间规划:玩家只能推而不能拉,每一步都需考虑后续走位,很容易把箱子推到无法挪动的位置,导致“死局”。
-
关卡设计:通常由一个个固定布局的关卡构成,不同布局带来难度差异,需要玩家反复尝试和思考。
-
路径搜索:游戏常涉及深度优先或广度优先搜索,以求最优解,也催生了众多自动求解算法。
对于 Java 学习者来说,实现一款推箱子有助于:
-
面向对象设计与实践:把游戏要素(地图、玩家、箱子、目标、墙壁、地面)抽象为类,设计它们之间的关系与交互。
-
网格与坐标管理:掌握如何使用二维数组或嵌套集合表示地图,并在控制台或 GUI 中呈现。
-
游戏逻辑与规则实现:实现玩家移动、箱子推动、合法性校验、胜利判定等核心逻辑,培养系统思维。
-
文件读写与关卡加载:如果引入关卡加载功能,需要掌握文件 I/O,读取并解析文本格式的关卡布局。
-
算法思考:例如,为后续扩展实现自动求解或提示路径,可以借助 BFS、DFS 或 A* 算法。
本项目重点实现一个 控制台版本的推箱子,通过字符界面将整个游戏流程展现出来。适用于 Java 基础学习者,既能快速体验游戏乐趣,也能深入理解面向对象思想与游戏开发思路。
项目需求详细介绍
功能需求
-
地图与关卡管理
-
支持硬编码一个或多个预设关卡(可后续扩展为文件加载)。
-
地图由二维网格表示,每个格子可包含:墙壁(Wall)、地面(Floor)、目标点(Goal)。
-
可以在地图上摆放箱子(Box)和玩家(Player),初始位置由预设关卡定义。
-
-
玩家移动与推箱
-
玩家可以向上下左右四个方向移动:
-
如果相邻单元是空地或目标,并且没有箱子,玩家直接移动。
-
如果相邻单元是箱子,且箱子前方的格子是空地或目标,玩家推动箱子前进一格,并占据箱子原先的位置。
-
其他情况(墙壁、箱子前方又有墙或另一箱子),移动非法,不执行。
-
-
玩家 只能推 箱子,无法拉动。
-
-
胜利判定
-
当所有箱子都被推到目标点上时,游戏胜利。
-
在胜利后,显示恭贺信息并提示是否重玩或退出。
-
-
界面与交互
-
控制台显示当前地图状态,使用不同字符表示墙壁、地面、目标、箱子、箱子在目标上、玩家、玩家在目标上等。
-
每次移动后刷新并打印地图,让玩家直观地看到变化。
-
使用
Scanner
读取玩家键盘输入(例如:W/A/S/D 或 U/L/D/R 表示上下左右)。 -
对非法输入提供提示,并允许重新输入。
-
-
关卡切换与重置
-
支持多关卡模式:玩家选择开始时可选择关卡编号。
-
支持在任意时刻输入“重置”(Reset)命令,将当前关卡状态恢复到初始布局。
-
支持在胜利或失败后选择“重新开始”或“退出”。
-
非功能需求
-
代码可读性
-
使用面向对象设计,将不同角色和元素抽象为类,职责分明。
-
关键类和方法需写详细注释,便于教学与维护。
-
所有代码集中在一个代码块内,不拆分示例包结构,但在注释标识文件名,方便复制到实际工程。
-
-
可扩展性
-
预留关卡加载接口,方便后续从文本文件或外部资源加载关卡。
-
设计合理的类与接口,为未来加入 GUI、撤销功能或自动求解留足空间。
-
-
用户体验
-
地图显示要直观,包括行列坐标横纵标示,便于玩家选择移动方向。
-
提示信息友好,如当前步数、剩余箱子与目标统计、非法操作原因等。
-
控制台界面版式整洁、美观,减少视觉干扰,突出游戏内容。
-
相关技术详细介绍
Java 语言与面向对象特性
-
类与对象
-
需要将“地图格子”(Tile)、“玩家”(Player)、“箱子”(Box)、“关卡管理”(Level)、“游戏逻辑”(GameController)等抽象为对应类。
-
理解对象的属性与方法,通过构造函数初始化对象状态。
-
-
继承与多态
-
地图格子可采用基类
Tile
,然后派生出WallTile
、FloorTile
、GoalTile
等不同类型,实现多态行为(例如是否可通行)。 -
玩家与箱子在地图上移动时可通过统一接口
GameObject
或Movable
管理。
-
-
集合与二维数组
-
使用
Tile[][] board
代表地图网格,方便通过下标board[row][col]
访问。 -
关卡中需要维护箱子列表、目标列表、玩家位置,便于胜利判定与状态更新。
-
-
文件 I/O(可选)
-
如需加载外部关卡,可使用
BufferedReader
、FileReader
逐行读取关卡文本,并解析字符映射到Tile
对象。 -
解析时要注意字符编码与行尾处理。
-
-
异常处理
-
对玩家输入进行校验,如方向命令不合法、无法推动等,需捕获并打印错误提示,不影响程序继续运行。
-
-
控制台输出
-
使用
System.out.println
、System.out.print
按行刷新地图,并在刷新前清除屏幕(可以打印若干空行模拟清屏效果),保持界面连贯。
-
枚举类型与常量管理
-
枚举
Direction
:用于表示上下左右移动方向(UP、DOWN、LEFT、RIGHT),并提供对应的坐标偏移dx, dy
,便于统一移动逻辑。 -
枚举
TileType
:表示地图格子类型(WALL、FLOOR、GOAL),用于加载关卡时生成对应Tile
对象。 -
常量类
GameConstants
:存放地图最大行列数、显示字符(墙、地面、目标、玩家、箱子等)及输入命令提示信息等静态常量。
设计模式与架构思想
-
MVC 或者分层架构思想(Model-Controller-View)
-
Model(模型):
Tile
、Player
、Box
、Level
等类,用于维护游戏状态。 -
Controller(控制器):
GameController
负责接收玩家输入、调用模型方法更新状态、检测胜利条件。 -
View(视图):
ConsoleRenderer
负责将Tile[][]
地图和Player
、Box
位置渲染为控制台字符画面。
-
-
工厂模式(Factory)
-
用于创建
Tile
对象:根据关卡字符(例如#
表示墙壁、.
表示目标、空格表示地面)生成对应Tile
实例。
-
-
单一职责原则(SRP)
-
将地图渲染、游戏逻辑、关卡加载等功能拆分在不同类中,减少耦合。
-
实现思路详细介绍
1. 架构总体设计
+---------------------------------------------------+
| GameController |
| - 读取玩家输入 |
| - 处理移动与推箱逻辑 |
| - 调用 Model 更新状态 |
| - 检测胜利条件并切换关卡或结束游戏 |
+---------------------------------------------------+
|
v
+------------------+ +------------------+ +------------------+
| Level | | Tile | | ConsoleRenderer |
| - 存储初始关卡布局|<--| - 抽象格子基类 | | - 控制台地图渲染 |
| - 提供重置功能 | | - 不同子类:Wall | | - 打印玩家提示 |
| - 管理玩家、箱子 | | Floor Goal | +------------------+
+------------------+
|
v
+------------------+ +------------------+
| Player | | Box |
| - 位置 pos | | - 位置 pos |
| - 移动方法 | | - 无直接行为 |
+------------------+ +------------------+
-
GameController(游戏控制器)
-
负责游戏主循环:
-
渲染当前地图(调用
ConsoleRenderer.render(...)
)。 -
提示玩家输入移动方向或命令(W/A/S/D 或 R=重置, Q=退出)。
-
根据输入调用
Player.move(...)
或Level.reset()
,或退出。 -
在移动时,检查目标方向上一格是否是墙或箱子,并判断箱子前方是否可推,若合法则更新
Player
与Box
的位置。 -
每次移动后调用
Level.checkWin()
判断是否所有箱子都在目标点上,若胜利则打印信息并让玩家选择下一步。
-
-
-
Level(关卡)
-
存储当前关卡的初始布局字符串数组或硬编码列表,包含:
-
二维字符数组
char initialMap[][]
,其中每个字符代表一个TileType
、可能还有玩家@
或箱子$
、目标.
等。 -
在初始化时,将这些字符转换为
Tile[][] tiles
,并生成Player
与若干Box
对象,分别记录它们的坐标。
-
-
保留一套副本
initialTiles
与initialBoxes
,用于执行 “重置” 操作时恢复初始状态。 -
提供
reset()
方法,将地图和所有游戏对象恢复到初始状态。 -
提供
checkWin()
方法,用当前所有箱子列表遍历检查是否每个箱子位置正好是一个目标(可通过在Tile
类中标记目标格)。
-
-
Tile(格子)及其子类
-
抽象类
Tile
,属性:行row
、列col
。 -
子类
WallTile
、FloorTile
、GoalTile
:分别表示墙壁、普通地面、目标点。 -
Tile
应提供方法isWalkable()
,默认FloorTile
和GoalTile
返回true
,WallTile
返回false
。
-
-
Player(玩家)
-
属性:当前位置
Position pos
。 -
方法:
boolean attemptMove(Direction dir, Level level)
:-
根据
dir
计算下一格位置next = pos + dir.delta
。 -
如果
tiles[next]
是墙(!isWalkable()
),返回 false。 -
如果
next
上没有箱子,直接将pos = next
,返回 true。 -
如果
next
上有箱子,则计算beyond = next + dir.delta
;-
如果
tiles[beyond]
可通行(isWalkable()
)且beyond
上无其它箱子,则:-
更新该
Box
的位置到beyond
,然后更新玩家位置到next
,返回 true;
-
-
否则返回 false(推箱失败)。
-
-
-
-
Box(箱子)
-
属性:当前位置
Position pos
。 -
无复杂方法,仅在被推动时更新
pos
。
-
-
Position(坐标)
-
简单的行列封装类,带
equals()
与hashCode()
,用于放入集合与比较。 -
可提供
Position translate(Direction dir)
返回一个新位置对象。
-
-
Direction(方向枚举)
-
四个方向:
UP(-1,0)
,DOWN(1,0)
,LEFT(0,-1)
,RIGHT(0,1)
; -
属性:
dx, dy
,表示行、列方向的增量; -
对应输入字符映射:W->UP, S->DOWN, A->LEFT, D->RIGHT。
-
-
ConsoleRenderer(控制台渲染器)
-
提供
render(Level level)
方法,根据当前tiles
、player
坐标、boxes
列表,将整个地图打印到控制台。 -
不同元素显示字符约定:
-
#
墙壁 -
(空格)地面
-
.
目标(未被箱子占) -
$
箱子(在地面或目标上) -
@
玩家(在地面或目标上) -
当箱子在目标上时,用
*
表示;当玩家站在目标上时,用+
表示。
-
-
-
LevelManager(关卡管理,可选)
-
如果需要支持多关卡,可以类
LevelManager
维护一个List<Level>
,按编号取出当前关卡。 -
也可以直接在
GameController
中维护一个静态的关卡数组或列表。
-
2. 核心流程设计
-
游戏启动
-
GameController.main()
:-
初始化若干预设关卡
Level[] levels = {...}
。 -
提示玩家选择关卡编号(或默认选择第一个)。
-
调用
currentLevel.initialize()
。 -
进入主循环。
-
-
-
主循环
-
while (!exitRequested) {
-
ConsoleRenderer.render(currentLevel);
-
提示
System.out.println("请输入移动方向 (W/A/S/D),或 R 重置,Q 退出:");
-
读取
String input = scanner.nextLine().trim().toUpperCase();
-
if (input.equals("Q")) break;
-
else if (input.equals("R")) { currentLevel.reset(); continue; }
-
else if (input
为 W/A/S/D)解析为Direction dir
。 -
调用
boolean moved = player.attemptMove(dir, currentLevel);
-
如果
moved == false
,打印 “无法移动,请重试”。 -
如果
moved == true
,步数计数steps++
,然后检查currentLevel.checkWin()
:-
若胜利:打印 “恭喜!第 X 步完成当前关卡”,提示“按任意键继续到下关或输入 Q 退出”。读取用户输入后决定是否进入下一个关卡或退出。
-
-
-
循环结束后,退出程序。
-
-
-
胜利判定
-
Level.checkWin()
:遍历List<Box> boxes
,如果所有box.pos
都在某个GoalTile
上,返回true
;否则false
。
-
-
关卡重置
-
Level.reset()
:-
将内部
tiles
恢复到initialTiles
(可通过深拷贝或重新生成)。 -
将
Player
与每个Box
坐标恢复到最初的initialPlayerPos
和initialBoxPositions
。 -
steps = 0
(步数清零)。
-
-
完整实现代码
说明:以下所有代码集中在一个代码块里,按“文件名”注释分隔不同的
.java
文件,可以直接复制粘贴到相应文件进行编译。代码中已包含详细注释,便于理解每行逻辑。
// 文件名:GameConstants.java
// 描述:存放游戏全局常量和显示字符
public class GameConstants {
// 地图最大行列数(根据预设关卡定义,保持足够大)
public static final int MAX_ROWS = 10;
public static final int MAX_COLS = 10;
// 控制台显示字符映射
public static final char WALL_CHAR = '#'; // 墙壁
public static final char FLOOR_CHAR = ' '; // 普通地面
public static final char GOAL_CHAR = '.'; // 目标点
public static final char BOX_CHAR = '$'; // 箱子
public static final char BOX_ON_GOAL = '*'; // 在目标上的箱子
public static final char PLAYER_CHAR = '@'; // 玩家
public static final char PLAYER_ON_GOAL = '+'; // 在目标上的玩家
// 控制台输入命令
public static final String CMD_UP = "W"; // 向上
public static final String CMD_DOWN = "S"; // 向下
public static final String CMD_LEFT = "A"; // 向左
public static final String CMD_RIGHT = "D"; // 向右
public static final String CMD_RESET = "R"; // 重置当前关卡
public static final String CMD_QUIT = "Q"; // 退出游戏
// 提示信息
public static final String PROMPT_MOVE = "请输入移动方向 (W/A/S/D),或 R=重置,Q=退出:";
public static final String MSG_INVALID = "非法输入或无法移动,请重试。";
public static final String MSG_WIN = "恭喜!您已完成当前关卡,用时步数:";
// 预留:支持多关卡,可将关卡总数常量化
public static final int TOTAL_LEVELS = 3;
}
// 文件名:Position.java
// 描述:封装地图坐标
import java.util.Objects;
public class Position {
private int row; // 行索引,从 0 开始
private int col; // 列索引,从 0 开始
public Position(int row, int col) {
this.row = row;
this.col = col;
}
public int getRow() {
return row;
}
public int getCol() {
return col;
}
// 根据方向偏移返回一个新 Position 对象
public Position translate(Direction dir) {
return new Position(row + dir.getDx(), col + dir.getDy());
}
// 检查坐标是否在合理范围内(非负)
public boolean isValid() {
return row >= 0 && col >= 0;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Position)) return false;
Position that = (Position) o;
return row == that.row && col == that.col;
}
@Override
public int hashCode() {
return Objects.hash(row, col);
}
@Override
public String toString() {
return "(" + row + "," + col + ")";
}
}
// 文件名:Direction.java
// 描述:4 个方向枚举,包含坐标偏移
public enum Direction {
UP(-1, 0), // 向上,行 -1
DOWN(1, 0), // 向下,行 +1
LEFT(0, -1), // 向左,列 -1
RIGHT(0, 1); // 向右,列 +1
private int dx; // 行偏移
private int dy; // 列偏移
Direction(int dx, int dy) {
this.dx = dx;
this.dy = dy;
}
public int getDx() {
return dx;
}
public int getDy() {
return dy;
}
// 将玩家输入字符映射为 Direction;返回 null 表示非法
public static Direction fromInput(String input) {
switch (input) {
case GameConstants.CMD_UP:
return UP;
case GameConstants.CMD_DOWN:
return DOWN;
case GameConstants.CMD_LEFT:
return LEFT;
case GameConstants.CMD_RIGHT:
return RIGHT;
default:
return null;
}
}
}
// 文件名:Tile.java
// 描述:地图格子抽象类
public abstract class Tile {
protected Position pos; // 此格子在地图中的坐标
public Tile(Position pos) {
this.pos = pos;
}
public Position getPosition() {
return pos;
}
// 判断是否可通行(玩家与箱子都可以推入该格)
public abstract boolean isWalkable();
// 判断此格是否为目标点
public abstract boolean isGoal();
}
// 文件名:WallTile.java
// 描述:墙壁格子,不可通行
public class WallTile extends Tile {
public WallTile(Position pos) {
super(pos);
}
@Override
public boolean isWalkable() {
return false; // 墙壁不可通行
}
@Override
public boolean isGoal() {
return false;
}
}
// 文件名:FloorTile.java
// 描述:普通地面,可通行但不是目标
public class FloorTile extends Tile {
public FloorTile(Position pos) {
super(pos);
}
@Override
public boolean isWalkable() {
return true; // 地面可通行
}
@Override
public boolean isGoal() {
return false;
}
}
// 文件名:GoalTile.java
// 描述:目标格子,可通行且标记为目标
public class GoalTile extends Tile {
public GoalTile(Position pos) {
super(pos);
}
@Override
public boolean isWalkable() {
return true; // 目标点可通行
}
@Override
public boolean isGoal() {
return true;
}
}
// 文件名:Box.java
// 描述:箱子对象,只有位置属性
public class Box {
private Position pos; // 箱子当前位置
public Box(Position pos) {
this.pos = pos;
}
public Position getPosition() {
return pos;
}
public void setPosition(Position pos) {
this.pos = pos;
}
}
// 文件名:Player.java
// 描述:玩家对象,负责移动与推箱逻辑
public class Player {
private Position pos; // 玩家当前位置
public Player(Position startPos) {
this.pos = startPos;
}
public Position getPosition() {
return pos;
}
public void setPosition(Position pos) {
this.pos = pos;
}
/**
* 尝试向指定方向移动玩家,并在可能时推动箱子
*
* @param dir 玩家移动方向
* @param level 当前关卡对象
* @return 如果移动(或推动)成功,返回 true;否则 false
*/
public boolean attemptMove(Direction dir, Level level) {
Position nextPos = pos.translate(dir); // 玩家前进一格的目标位
Tile[][] tiles = level.getTiles(); // 当前关卡的格子地图
java.util.List<Box> boxes = level.getBoxes(); // 当前关卡的所有箱子列表
// 1. 检查 nextPos 是否越界
if (!level.isWithinBounds(nextPos)) {
return false;
}
// 2. 检查 nextPos 上是否是墙壁
Tile nextTile = tiles[nextPos.getRow()][nextPos.getCol()];
if (!nextTile.isWalkable()) {
return false; // 墙壁阻挡
}
// 3. 检查 nextPos 上是否有箱子
Box boxAtNext = level.getBoxAtPosition(nextPos);
if (boxAtNext == null) {
// 没有箱子,直接移动玩家
setPosition(nextPos);
return true;
} else {
// 有箱子,需要尝试推动
Position beyondPos = nextPos.translate(dir); // 箱子前方一格
// 3.1. 检查 beyondPos 是否越界
if (!level.isWithinBounds(beyondPos)) {
return false;
}
// 3.2. 检查 beyondPos 上的格子是否可通行
Tile beyondTile = tiles[beyondPos.getRow()][beyondPos.getCol()];
if (!beyondTile.isWalkable()) {
return false; // 箱子前方被墙或非通行格挡住
}
// 3.3. 检查 beyondPos 上是否还有其他箱子
if (level.getBoxAtPosition(beyondPos) != null) {
return false; // 已有箱子,推不动
}
// 3.4. 推动箱子:更新箱子位置到 beyondPos
boxAtNext.setPosition(beyondPos);
// 3.5. 更新玩家位置到 nextPos
setPosition(nextPos);
return true;
}
}
}
// 文件名:Level.java
// 描述:关卡类,存储地图布局、玩家和箱子状态,并提供重置与胜利判定功能
import java.util.*;
/**
* 将一个关卡初始字符地图解析为 Tile[][]、Player 和 List<Box>
* 支持重置功能,将当前状态恢复到最初状态
*/
public class Level {
// 初始关卡字符数组,以多行字符串形式存储
// 使用以下字符约定:
// '#' - 墙壁
// ' ' - 普通地面
// '.' - 目标
// '$' - 箱子(箱子可能在地面或目标上,用 '$' 表示)
// '@' - 玩家(玩家可能在地面或目标上,用 '@' 表示)
private String[] initialMap;
// 地图尺寸
private int rows;
private int cols;
// 当前关卡状态
private Tile[][] tiles; // Tile 对象网格
private Player player; // 玩家对象
private List<Box> boxes; // 当前所有箱子对象
private int steps; // 玩家当前关卡移动步数
// 备份初始状态,用于 reset()
private Tile[][] initialTiles; // 对 tiles 的深拷贝
private Position initialPlayerPos;
private List<Position> initialBoxPositions;
/**
* 构造方法:接收初始关卡字符串数组并解析
*
* @param mapLines 多行字符串,每行长度相同,表示关卡字符地图
*/
public Level(String[] mapLines) {
this.initialMap = mapLines;
this.rows = mapLines.length;
this.cols = mapLines[0].length();
// 解析初始地图,生成 tiles、player、boxes,以及备份初始状态
parseInitialMap();
backupInitialState();
this.steps = 0;
}
/**
* 解析 initialMap,将字符转换为对应的 Tile、Player、Box 对象
*/
private void parseInitialMap() {
tiles = new Tile[rows][cols];
boxes = new ArrayList<>();
for (int i = 0; i < rows; i++) {
String line = initialMap[i];
for (int j = 0; j < cols; j++) {
char ch = line.charAt(j);
Position pos = new Position(i, j);
switch (ch) {
case GameConstants.WALL_CHAR:
tiles[i][j] = new WallTile(pos);
break;
case GameConstants.GOAL_CHAR:
tiles[i][j] = new GoalTile(pos);
break;
case GameConstants.BOX_CHAR:
// 箱子在普通地面上
tiles[i][j] = new FloorTile(pos);
boxes.add(new Box(pos));
break;
case GameConstants.BOX_ON_GOAL:
// 箱子在目标上
tiles[i][j] = new GoalTile(pos);
boxes.add(new Box(pos));
break;
case GameConstants.PLAYER_CHAR:
// 玩家在普通地面上
tiles[i][j] = new FloorTile(pos);
player = new Player(pos);
break;
case GameConstants.PLAYER_ON_GOAL:
// 玩家在目标上
tiles[i][j] = new GoalTile(pos);
player = new Player(pos);
break;
default:
// 默认视为空白地面,包括空格 ' '
tiles[i][j] = new FloorTile(pos);
break;
}
}
}
}
/**
* 备份当前初始状态,用于 reset 时恢复
*/
private void backupInitialState() {
// 深拷贝 tiles 数组
initialTiles = new Tile[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
Tile t = tiles[i][j];
Position pos = t.getPosition();
if (t instanceof WallTile) {
initialTiles[i][j] = new WallTile(pos);
} else if (t instanceof GoalTile) {
initialTiles[i][j] = new GoalTile(pos);
} else {
initialTiles[i][j] = new FloorTile(pos);
}
}
}
// 备份玩家初始位置
initialPlayerPos = new Position(player.getPosition().getRow(), player.getPosition().getCol());
// 备份所有箱子初始位置
initialBoxPositions = new ArrayList<>();
for (Box b : boxes) {
Position p = b.getPosition();
initialBoxPositions.add(new Position(p.getRow(), p.getCol()));
}
}
/**
* 重置当前关卡:将 tiles、player、boxes 恢复到初始状态
*/
public void reset() {
// 1. 恢复 tiles
tiles = new Tile[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
Tile t = initialTiles[i][j];
Position pos = t.getPosition();
if (t instanceof WallTile) {
tiles[i][j] = new WallTile(pos);
} else if (t instanceof GoalTile) {
tiles[i][j] = new GoalTile(pos);
} else {
tiles[i][j] = new FloorTile(pos);
}
}
}
// 2. 恢复玩家位置
player.setPosition(new Position(initialPlayerPos.getRow(), initialPlayerPos.getCol()));
// 3. 恢复箱子位置
boxes.clear();
for (Position p : initialBoxPositions) {
boxes.add(new Box(new Position(p.getRow(), p.getCol())));
}
// 4. 步数重置
steps = 0;
}
/**
* 检查是否胜利:即所有箱子都在目标上
*
* @return 如果所有箱子均在目标格,返回 true;否则 false
*/
public boolean checkWin() {
for (Box b : boxes) {
Position p = b.getPosition();
Tile t = tiles[p.getRow()][p.getCol()];
if (!t.isGoal()) {
return false;
}
}
return true;
}
/**
* 根据坐标返回此处是否有箱子
*
* @param pos 目标坐标
* @return 如果有箱子,返回对应 Box 对象;否则返回 null
*/
public Box getBoxAtPosition(Position pos) {
for (Box b : boxes) {
if (b.getPosition().equals(pos)) {
return b;
}
}
return null;
}
/**
* 检查给定坐标是否在地图范围内
*
* @param pos 目标坐标
* @return 如果行列都在 [0, rows-1] [0, cols-1] 范围内,返回 true;否则 false
*/
public boolean isWithinBounds(Position pos) {
int r = pos.getRow();
int c = pos.getCol();
return (r >= 0 && r < rows && c >= 0 && c < cols);
}
// Getter 方法
public Tile[][] getTiles() {
return tiles;
}
public Player getPlayer() {
return player;
}
public List<Box> getBoxes() {
return boxes;
}
public int getSteps() {
return steps;
}
public void incrementSteps() {
steps++;
}
public int getRows() {
return rows;
}
public int getCols() {
return cols;
}
}
// 文件名:ConsoleRenderer.java
// 描述:控制台渲染器,将当前关卡状态绘制为字符界面
import java.util.List;
public class ConsoleRenderer {
/**
* 在控制台打印当前关卡地图状态
*
* @param level 当前关卡对象
*/
public void render(Level level) {
Tile[][] tiles = level.getTiles();
Player player = level.getPlayer();
List<Box> boxes = level.getBoxes();
int rows = level.getRows();
int cols = level.getCols();
// 首先打印列坐标
System.out.print(" "); // 左侧留空用于行坐标
for (int c = 0; c < cols; c++) {
System.out.print(c + " ");
}
System.out.println();
// 遍历每个格子,根据内容决定显示哪个字符
for (int r = 0; r < rows; r++) {
System.out.print(r + " "); // 打印行坐标
for (int c = 0; c < cols; c++) {
Position pos = new Position(r, c);
Tile t = tiles[r][c];
// 先判断是否有玩家
if (player.getPosition().equals(pos)) {
// 玩家在目标上或地面上,用不同字符
if (t.isGoal()) {
System.out.print(GameConstants.PLAYER_ON_GOAL + " ");
} else {
System.out.print(GameConstants.PLAYER_CHAR + " ");
}
continue;
}
// 判断是否有箱子
Box box = level.getBoxAtPosition(pos);
if (box != null) {
// 箱子在目标或地面
if (t.isGoal()) {
System.out.print(GameConstants.BOX_ON_GOAL + " ");
} else {
System.out.print(GameConstants.BOX_CHAR + " ");
}
continue;
}
// 没有玩家和箱子,则显示 Tile 本身
if (t instanceof WallTile) {
System.out.print(GameConstants.WALL_CHAR + " ");
} else if (t instanceof GoalTile) {
System.out.print(GameConstants.GOAL_CHAR + " ");
} else {
System.out.print(GameConstants.FLOOR_CHAR + " ");
}
}
System.out.println();
}
// 打印当前步数
System.out.println("当前步数:" + level.getSteps());
}
}
// 文件名:GameController.java
// 描述:游戏主控类,负责主循环、用户输入与流程管理
import java.util.Scanner;
public class GameController {
private Level[] levels; // 关卡数组
private int currentLevelIndex; // 当前关卡索引
private Scanner scanner;
public GameController() {
// 初始化关卡
levels = new Level[GameConstants.TOTAL_LEVELS];
initializeLevels();
currentLevelIndex = 0; // 默认第一关
scanner = new Scanner(System.in);
}
/**
* 初始化预设关卡
* 这里硬编码三个关卡示例,可以自行修改或扩展
*/
private void initializeLevels() {
// 关卡 1
String[] map1 = new String[] {
"#####",
"#.@ #",
"# $ #",
"# . #",
"#####"
};
levels[0] = new Level(map1);
// 关卡 2
String[] map2 = new String[] {
" ##### ",
" # # ",
"###$ ###",
"# .$ #",
"#@$ $ #",
"### ### ",
" # # ",
" ##### "
};
levels[1] = new Level(map2);
// 关卡 3
String[] map3 = new String[] {
"#######",
"# #",
"# $@$ #",
"# .$ #",
"# $ #",
"# . #",
"#######"
};
levels[2] = new Level(map3);
}
/**
* 启动游戏主循环
*/
public void start() {
System.out.println("欢迎来到 Java 推箱子游戏!");
while (true) {
// 渲染当前关卡
Level currentLevel = levels[currentLevelIndex];
ConsoleRenderer renderer = new ConsoleRenderer();
renderer.render(currentLevel);
// 提示玩家输入
System.out.print(GameConstants.PROMPT_MOVE);
String input = scanner.nextLine().trim().toUpperCase();
if (input.equals(GameConstants.CMD_QUIT)) {
System.out.println("游戏结束,感谢游玩!");
break;
}
else if (input.equals(GameConstants.CMD_RESET)) {
// 重置当前关卡
currentLevel.reset();
continue;
}
else {
Direction dir = Direction.fromInput(input);
if (dir == null) {
System.out.println(GameConstants.MSG_INVALID);
continue;
}
// 尝试移动
boolean moved = currentLevel.getPlayer().attemptMove(dir, currentLevel);
if (!moved) {
System.out.println(GameConstants.MSG_INVALID);
continue;
}
// 移动成功,步数 +1
currentLevel.incrementSteps();
// 检查是否胜利
if (currentLevel.checkWin()) {
// 渲染最终局面
renderer.render(currentLevel);
System.out.println(GameConstants.MSG_WIN + currentLevel.getSteps());
// 提示是否进入下一关
if (currentLevelIndex < levels.length - 1) {
System.out.print("按 N 进入下一关,按 Q 退出游戏:");
String nextCmd = scanner.nextLine().trim().toUpperCase();
if (nextCmd.equals("N")) {
currentLevelIndex++;
} else {
System.out.println("游戏结束,感谢游玩!");
break;
}
} else {
// 已经是最后一关
System.out.println("恭喜您通关所有关卡!游戏结束。");
break;
}
}
// 否则继续当前关卡循环
}
}
scanner.close();
}
public static void main(String[] args) {
GameController game = new GameController();
game.start();
}
}
代码详细解读
以下部分只对各主要方法及其作用进行说明,不再复写代码。
-
GameConstants 类
-
存放全局常量,包括地图最大行列数、字符显示映射(墙壁、地面、目标、箱子、玩家等)、玩家输入命令字符串常量以及提示信息等。便于统一修改与维护。
-
-
Position 类
-
封装地图坐标
row
和col
,提供translate(Direction dir)
方法,通过方向偏移生成新坐标,简化玩家与箱子的移动计算。 -
重写
equals()
与hashCode()
,确保Position
在容器中对比时按值比较。
-
-
Direction 枚举
-
定义四个方向
UP、DOWN、LEFT、RIGHT
,每个方向包含行、列的偏移量dx、dy
。 -
提供静态方法
fromInput(String input)
,将玩家输入的字符(W/A/S/D)转换成对应方向枚举,便于统一移动逻辑处理。
-
-
Tile 抽象类及其子类
-
Tile
是地图格子的基类,包含属性Position pos
,并定义抽象方法isWalkable()
与isGoal()
。 -
WallTile
重写isWalkable()
返回false
,表示墙壁不可通行;isGoal()
返回false
。 -
FloorTile
重写isWalkable()
返回true
,isGoal()
返回false
,表示普通地面。 -
GoalTile
重写isWalkable()
返回true
,且isGoal()
返回true
,表示目标格子、玩家与箱子都可进入。
-
-
Box 类
-
表示一个箱子,只包含当前位置
Position pos
,提供getPosition()
和setPosition(...)
方法。 -
在尝试推动时,直接修改其
pos
。
-
-
Player 类
-
表示玩家,包含当前位置
Position pos
。 -
核心方法
attemptMove(Direction dir, Level level)
:-
根据方向
dir
计算玩家想要前进的一格nextPos
; -
检查
nextPos
是否在地图内,若越界返回false
; -
检查
tiles[nextPos]
是否可通行,若是墙返回false
; -
调用
level.getBoxAtPosition(nextPos)
查看nextPos
是否有箱子:-
如果没有箱子,则直接
pos = nextPos
,返回true
; -
如果有箱子,则计算
beyondPos = nextPos.translate(dir)
,并依次检查越界、是否可通行、是否还有其他箱子;若都合法,则先更新箱子位置到beyondPos
,再更新玩家到nextPos
,返回true
;否则返回false
。
-
-
-
该方法实现了“推箱”核心逻辑:只能推而不能拉,且要检查箱子前方是否空位或目标。
-
-
Level 类
-
接收一个字符串数组
initialMap
,其中每个字符表示不同的地图元素(墙、地面、目标、箱子、玩家等)。 -
parseInitialMap()
方法将字符解析为Tile
对象,并实例化Player
与Box
对象列表。解析过程:-
#
→WallTile
-
.
→GoalTile
-
$
→FloorTile
+ 把Box
加入到boxes
列表 -
*
(BOX_ON_GOAL
)→GoalTile
+ 把Box
加入boxes
-
@
(PLAYER_CHAR
)→FloorTile
+ 创建Player
-
+
(PLAYER_ON_GOAL
)→GoalTile
+ 创建Player
-
其他字符(空格)→
FloorTile
-
-
backupInitialState()
方法保存解析后tiles
、玩家位置和所有箱子位置的副本,用于后续reset()
恢复。 -
reset()
方法通过深拷贝initialTiles
和复制初始坐标,实现地图与所有对象恢复到初始状态,并把steps
清零。 -
checkWin()
方法遍历当前所有boxes
,判断其Position
是否在对应的GoalTile
上,如果所有箱子都在目标上,返回true
。 -
getBoxAtPosition(Position pos)
方法遍历boxes
列表寻找与pos
相同位置的箱子,若找到则返回该Box
,否则返回null
。 -
isWithinBounds(Position pos)
用于检查坐标是否在地图范围之内(0 ≤ row < rows 且 0 ≤ col < cols)。 -
还提供了若干 Getter 方法,以便其他类获取
tiles
、player
、boxes
、steps
、rows
、cols
等信息。
-
-
ConsoleRenderer 类
-
提供
render(Level level)
方法,将当前地图状态打印到控制台。渲染逻辑:-
先打印列坐标(从 0 开始)。
-
遍历行列,判断每个格子是否包含玩家(
level.getPlayer().getPosition()
),如果是则根据此Tile
是否为目标选择@
或+
。 -
否则判断是否有箱子(
level.getBoxAtPosition(pos)
),如果有,则根据Tile.isGoal()
选择*
(在目标上)或$
(在普通地面)。 -
如果玩家与箱子都不在该格,则根据
Tile
类型打印#
(墙壁)、.
(目标)或空格(地面)。 -
行尾打印当前步数(
level.getSteps()
)。
-
-
-
GameController 类
-
是游戏的入口与主控类,包含:
-
Level[] levels
:存放多个预设关卡。 -
int currentLevelIndex
:当前关卡索引,从 0 开始。 -
Scanner scanner
:读取用户输入。
-
-
构造方法
GameController()
:-
调用
initializeLevels()
,硬编码三个示例关卡的字符串数组并创建Level
对象; -
将
currentLevelIndex = 0
; -
初始化
scanner
。
-
-
start()
方法:-
打印欢迎信息。
-
进入
while (true)
主循环:-
获取
Level currentLevel = levels[currentLevelIndex]
; -
实例化
ConsoleRenderer
并调用renderer.render(currentLevel)
绘制地图; -
提示并读取玩家输入
input = scanner.nextLine().trim().toUpperCase()
; -
如果输入
Q
,打印结束提示并break
; -
如果输入
R
,调用currentLevel.reset()
并continue
; -
否则尝试通过
Direction.fromInput(input)
获得方向枚举;-
如果
dir == null
,说明非法输入,打印提示并continue
; -
否则调用
currentLevel.getPlayer().attemptMove(dir, currentLevel)
;-
如果返回
false
,打印 “非法移动” 提示并continue
; -
如果返回
true
,调用currentLevel.incrementSteps()
累加步数; -
调用
currentLevel.checkWin()
判断是否胜利:-
如果胜利,则再次调用
renderer.render(currentLevel)
显示最终局面,打印“恭喜完成”并显示当前步数; -
如果还有下一关(
currentLevelIndex < levels.length - 1
),提示玩家输入N
进入下一关或Q
退出;根据输入决定是否currentLevelIndex++
或break
; -
如果已经是最后一关,则打印“通关提示”并
break
。
-
-
-
-
-
退出主循环后,关闭
scanner
并结束程序。
-
-
项目详细总结
-
功能完整且逻辑清晰
-
本项目实现了推箱子游戏的核心玩法:玩家上下左右移动、推箱子、检查合法性、胜利判定、关卡重置等。
-
通过字符界面在控制台呈现,简单易懂,适合快速上手。
-
-
面向对象设计
-
利用
Tile
、Player
、Box
、Level
、ConsoleRenderer
、GameController
等类,把游戏要素和逻辑分工明确,职责单一、易于维护。 -
枚举
Direction
便于方向判读和偏移计算。
-
-
关卡可重置且易扩展
-
Level
类保留了初始状态的深拷贝,实现了reset()
功能,让玩家可以随时重设当前关卡。 -
关卡定义放在
initializeLevels()
,只需添加新的String[]
地图,即可增加关卡或修改布局。
-
-
用户体验与易用性
-
控制台渲染清晰,打印行列坐标帮助玩家定位;展示玩家、箱子、目标叠加状态(
+
、*
)一目了然。 -
每步移动后自动刷新界面并显示当前步数,让玩家了解进度;非法操作给出友好提示。
-
-
可扩展性与后续方向
-
可以方便地将关卡加载改为从外部文本文件读取,只需把
String[] mapLines
替换成读取文件行即可。 -
提供了合理的类结构,可后续加入撤销(Undo)功能、自动求解(Solver)、图形界面(Swing/JavaFX)、网络对战等。
-
-
性能表现
-
地图规模通常在几十行以内,玩家每次移动只做常量级的检查与更新,不会有性能瓶颈。
-
渲染是按行列遍历,一般地图尺寸不大,刷新速度足够快。
-