java实现推箱子游戏(附带源码)

项目背景详细介绍

推箱子(Sokoban)是一款经典的益智游戏,最早由日本程序员仓田智昭(Toshirō Takahashi)于 1981 年设计,后在 1982 年由 Thinking Rabbit 发行并流行于全球。游戏的核心玩法简单:玩家(仓库管理员)需要在迷宫一样的仓库中,将箱子推到指定的目标位置。尽管规则简单,但在有限空间下怎样避免将箱子卡住、如何规划最少步数牵动人们的思维,因而成为风靡全球的脑力游戏之一。

推箱子游戏具有以下特性:

  • 空间规划:玩家只能推而不能拉,每一步都需考虑后续走位,很容易把箱子推到无法挪动的位置,导致“死局”。

  • 关卡设计:通常由一个个固定布局的关卡构成,不同布局带来难度差异,需要玩家反复尝试和思考。

  • 路径搜索:游戏常涉及深度优先或广度优先搜索,以求最优解,也催生了众多自动求解算法。

对于 Java 学习者来说,实现一款推箱子有助于:

  1. 面向对象设计与实践:把游戏要素(地图、玩家、箱子、目标、墙壁、地面)抽象为类,设计它们之间的关系与交互。

  2. 网格与坐标管理:掌握如何使用二维数组或嵌套集合表示地图,并在控制台或 GUI 中呈现。

  3. 游戏逻辑与规则实现:实现玩家移动、箱子推动、合法性校验、胜利判定等核心逻辑,培养系统思维。

  4. 文件读写与关卡加载:如果引入关卡加载功能,需要掌握文件 I/O,读取并解析文本格式的关卡布局。

  5. 算法思考:例如,为后续扩展实现自动求解或提示路径,可以借助 BFS、DFS 或 A* 算法。

本项目重点实现一个 控制台版本的推箱子,通过字符界面将整个游戏流程展现出来。适用于 Java 基础学习者,既能快速体验游戏乐趣,也能深入理解面向对象思想与游戏开发思路。


项目需求详细介绍

功能需求

  1. 地图与关卡管理

    • 支持硬编码一个或多个预设关卡(可后续扩展为文件加载)。

    • 地图由二维网格表示,每个格子可包含:墙壁(Wall)、地面(Floor)、目标点(Goal)。

    • 可以在地图上摆放箱子(Box)和玩家(Player),初始位置由预设关卡定义。

  2. 玩家移动与推箱

    • 玩家可以向上下左右四个方向移动:

      • 如果相邻单元是空地或目标,并且没有箱子,玩家直接移动。

      • 如果相邻单元是箱子,且箱子前方的格子是空地或目标,玩家推动箱子前进一格,并占据箱子原先的位置。

      • 其他情况(墙壁、箱子前方又有墙或另一箱子),移动非法,不执行。

    • 玩家 只能推 箱子,无法拉动。

  3. 胜利判定

    • 当所有箱子都被推到目标点上时,游戏胜利。

    • 在胜利后,显示恭贺信息并提示是否重玩或退出。

  4. 界面与交互

    • 控制台显示当前地图状态,使用不同字符表示墙壁、地面、目标、箱子、箱子在目标上、玩家、玩家在目标上等。

    • 每次移动后刷新并打印地图,让玩家直观地看到变化。

    • 使用 Scanner 读取玩家键盘输入(例如:W/A/S/D 或 U/L/D/R 表示上下左右)。

    • 对非法输入提供提示,并允许重新输入。

  5. 关卡切换与重置

    • 支持多关卡模式:玩家选择开始时可选择关卡编号。

    • 支持在任意时刻输入“重置”(Reset)命令,将当前关卡状态恢复到初始布局。

    • 支持在胜利或失败后选择“重新开始”或“退出”。

非功能需求

  1. 代码可读性

    • 使用面向对象设计,将不同角色和元素抽象为类,职责分明。

    • 关键类和方法需写详细注释,便于教学与维护。

    • 所有代码集中在一个代码块内,不拆分示例包结构,但在注释标识文件名,方便复制到实际工程。

  2. 可扩展性

    • 预留关卡加载接口,方便后续从文本文件或外部资源加载关卡。

    • 设计合理的类与接口,为未来加入 GUI、撤销功能或自动求解留足空间。

  3. 用户体验

    • 地图显示要直观,包括行列坐标横纵标示,便于玩家选择移动方向。

    • 提示信息友好,如当前步数、剩余箱子与目标统计、非法操作原因等。

    • 控制台界面版式整洁、美观,减少视觉干扰,突出游戏内容。


相关技术详细介绍

Java 语言与面向对象特性

  1. 类与对象

    • 需要将“地图格子”(Tile)、“玩家”(Player)、“箱子”(Box)、“关卡管理”(Level)、“游戏逻辑”(GameController)等抽象为对应类。

    • 理解对象的属性与方法,通过构造函数初始化对象状态。

  2. 继承与多态

    • 地图格子可采用基类 Tile,然后派生出 WallTileFloorTileGoalTile 等不同类型,实现多态行为(例如是否可通行)。

    • 玩家与箱子在地图上移动时可通过统一接口 GameObjectMovable 管理。

  3. 集合与二维数组

    • 使用 Tile[][] board 代表地图网格,方便通过下标 board[row][col] 访问。

    • 关卡中需要维护箱子列表、目标列表、玩家位置,便于胜利判定与状态更新。

  4. 文件 I/O(可选)

    • 如需加载外部关卡,可使用 BufferedReaderFileReader 逐行读取关卡文本,并解析字符映射到 Tile 对象。

    • 解析时要注意字符编码与行尾处理。

  5. 异常处理

    • 对玩家输入进行校验,如方向命令不合法、无法推动等,需捕获并打印错误提示,不影响程序继续运行。

  6. 控制台输出

    • 使用 System.out.printlnSystem.out.print 按行刷新地图,并在刷新前清除屏幕(可以打印若干空行模拟清屏效果),保持界面连贯。

枚举类型与常量管理

  • 枚举 Direction:用于表示上下左右移动方向(UP、DOWN、LEFT、RIGHT),并提供对应的坐标偏移 dx, dy,便于统一移动逻辑。

  • 枚举 TileType:表示地图格子类型(WALL、FLOOR、GOAL),用于加载关卡时生成对应 Tile 对象。

  • 常量类 GameConstants:存放地图最大行列数、显示字符(墙、地面、目标、玩家、箱子等)及输入命令提示信息等静态常量。

设计模式与架构思想

  1. MVC 或者分层架构思想(Model-Controller-View)

    • Model(模型)TilePlayerBoxLevel 等类,用于维护游戏状态。

    • Controller(控制器)GameController 负责接收玩家输入、调用模型方法更新状态、检测胜利条件。

    • View(视图)ConsoleRenderer 负责将 Tile[][] 地图和 PlayerBox 位置渲染为控制台字符画面。

  2. 工厂模式(Factory)

    • 用于创建 Tile 对象:根据关卡字符(例如 # 表示墙壁、. 表示目标、空格表示地面)生成对应 Tile 实例。

  3. 单一职责原则(SRP)

    • 将地图渲染、游戏逻辑、关卡加载等功能拆分在不同类中,减少耦合。


实现思路详细介绍

1. 架构总体设计

+---------------------------------------------------+
|                   GameController                  |
|  - 读取玩家输入                                   |
|  - 处理移动与推箱逻辑                             |
|  - 调用 Model 更新状态                             |
|  - 检测胜利条件并切换关卡或结束游戏               |
+---------------------------------------------------+
                           |
                           v
+------------------+   +------------------+   +------------------+
|     Level        |   |      Tile        |   |   ConsoleRenderer |
| - 存储初始关卡布局|<--| - 抽象格子基类   |   | - 控制台地图渲染  |
| - 提供重置功能   |   | - 不同子类:Wall |   | - 打印玩家提示     |
| - 管理玩家、箱子 |   |   Floor Goal     |   +------------------+
+------------------+
         |
         v
+------------------+   +------------------+
|     Player       |   |      Box         |
| - 位置 pos       |   | - 位置 pos       |
| - 移动方法       |   | - 无直接行为      |
+------------------+   +------------------+
  1. GameController(游戏控制器)

    • 负责游戏主循环:

      1. 渲染当前地图(调用 ConsoleRenderer.render(...))。

      2. 提示玩家输入移动方向或命令(W/A/S/D 或 R=重置, Q=退出)。

      3. 根据输入调用 Player.move(...)Level.reset(),或退出。

      4. 在移动时,检查目标方向上一格是否是墙或箱子,并判断箱子前方是否可推,若合法则更新 PlayerBox 的位置。

      5. 每次移动后调用 Level.checkWin() 判断是否所有箱子都在目标点上,若胜利则打印信息并让玩家选择下一步。

  2. Level(关卡)

    • 存储当前关卡的初始布局字符串数组或硬编码列表,包含:

      • 二维字符数组 char initialMap[][],其中每个字符代表一个 TileType、可能还有玩家 @ 或箱子 $、目标 . 等。

      • 在初始化时,将这些字符转换为 Tile[][] tiles,并生成 Player 与若干 Box 对象,分别记录它们的坐标。

    • 保留一套副本 initialTilesinitialBoxes,用于执行 “重置” 操作时恢复初始状态。

    • 提供 reset() 方法,将地图和所有游戏对象恢复到初始状态。

    • 提供 checkWin() 方法,用当前所有箱子列表遍历检查是否每个箱子位置正好是一个目标(可通过在 Tile 类中标记目标格)。

  3. Tile(格子)及其子类

    • 抽象类 Tile,属性:行 row、列 col

    • 子类 WallTileFloorTileGoalTile:分别表示墙壁、普通地面、目标点。

    • Tile 应提供方法 isWalkable(),默认 FloorTileGoalTile 返回 trueWallTile 返回 false

  4. Player(玩家)

    • 属性:当前位置 Position pos

    • 方法:boolean attemptMove(Direction dir, Level level)

      1. 根据 dir 计算下一格位置 next = pos + dir.delta

      2. 如果 tiles[next] 是墙(!isWalkable()),返回 false。

      3. 如果 next 上没有箱子,直接将 pos = next,返回 true。

      4. 如果 next 上有箱子,则计算 beyond = next + dir.delta

        • 如果 tiles[beyond] 可通行(isWalkable())且 beyond 上无其它箱子,则:

          • 更新该 Box 的位置到 beyond,然后更新玩家位置到 next,返回 true;

        • 否则返回 false(推箱失败)。

  5. Box(箱子)

    • 属性:当前位置 Position pos

    • 无复杂方法,仅在被推动时更新 pos

  6. Position(坐标)

    • 简单的行列封装类,带 equals()hashCode(),用于放入集合与比较。

    • 可提供 Position translate(Direction dir) 返回一个新位置对象。

  7. Direction(方向枚举)

    • 四个方向:UP(-1,0)DOWN(1,0)LEFT(0,-1)RIGHT(0,1)

    • 属性:dx, dy,表示行、列方向的增量;

    • 对应输入字符映射:W->UP, S->DOWN, A->LEFT, D->RIGHT。

  8. ConsoleRenderer(控制台渲染器)

    • 提供 render(Level level) 方法,根据当前 tilesplayer 坐标、boxes 列表,将整个地图打印到控制台。

    • 不同元素显示字符约定:

      • # 墙壁

      • (空格)地面

      • . 目标(未被箱子占)

      • $ 箱子(在地面或目标上)

      • @ 玩家(在地面或目标上)

      • 当箱子在目标上时,用 * 表示;当玩家站在目标上时,用 + 表示。

  9. LevelManager(关卡管理,可选)

    • 如果需要支持多关卡,可以类 LevelManager 维护一个 List<Level>,按编号取出当前关卡。

    • 也可以直接在 GameController 中维护一个静态的关卡数组或列表。

2. 核心流程设计

  1. 游戏启动

    • GameController.main()

      1. 初始化若干预设关卡 Level[] levels = {...}

      2. 提示玩家选择关卡编号(或默认选择第一个)。

      3. 调用 currentLevel.initialize()

      4. 进入主循环。

  2. 主循环

    • while (!exitRequested) {

      1. ConsoleRenderer.render(currentLevel);

      2. 提示 System.out.println("请输入移动方向 (W/A/S/D),或 R 重置,Q 退出:");

      3. 读取 String input = scanner.nextLine().trim().toUpperCase();

      4. if (input.equals("Q")) break;

      5. else if (input.equals("R")) { currentLevel.reset(); continue; }

      6. else if (input 为 W/A/S/D)解析为 Direction dir

      7. 调用 boolean moved = player.attemptMove(dir, currentLevel);

        • 如果 moved == false,打印 “无法移动,请重试”。

        • 如果 moved == true,步数计数 steps++,然后检查 currentLevel.checkWin()

          • 若胜利:打印 “恭喜!第 X 步完成当前关卡”,提示“按任意键继续到下关或输入 Q 退出”。读取用户输入后决定是否进入下一个关卡或退出。

      8. 循环结束后,退出程序。

  3. 胜利判定

    • Level.checkWin():遍历 List<Box> boxes,如果所有 box.pos 都在某个 GoalTile 上,返回 true;否则 false

  4. 关卡重置

    • Level.reset()

      1. 将内部 tiles 恢复到 initialTiles(可通过深拷贝或重新生成)。

      2. Player 与每个 Box 坐标恢复到最初的 initialPlayerPosinitialBoxPositions

      3. 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();
    }
}

代码详细解读

以下部分只对各主要方法及其作用进行说明,不再复写代码。

  1. GameConstants 类

    • 存放全局常量,包括地图最大行列数、字符显示映射(墙壁、地面、目标、箱子、玩家等)、玩家输入命令字符串常量以及提示信息等。便于统一修改与维护。

  2. Position 类

    • 封装地图坐标 rowcol,提供 translate(Direction dir) 方法,通过方向偏移生成新坐标,简化玩家与箱子的移动计算。

    • 重写 equals()hashCode(),确保 Position 在容器中对比时按值比较。

  3. Direction 枚举

    • 定义四个方向 UP、DOWN、LEFT、RIGHT,每个方向包含行、列的偏移量 dx、dy

    • 提供静态方法 fromInput(String input),将玩家输入的字符(W/A/S/D)转换成对应方向枚举,便于统一移动逻辑处理。

  4. Tile 抽象类及其子类

    • Tile 是地图格子的基类,包含属性 Position pos,并定义抽象方法 isWalkable()isGoal()

    • WallTile 重写 isWalkable() 返回 false,表示墙壁不可通行;isGoal() 返回 false

    • FloorTile 重写 isWalkable() 返回 trueisGoal() 返回 false,表示普通地面。

    • GoalTile 重写 isWalkable() 返回 true,且 isGoal() 返回 true,表示目标格子、玩家与箱子都可进入。

  5. Box 类

    • 表示一个箱子,只包含当前位置 Position pos,提供 getPosition()setPosition(...) 方法。

    • 在尝试推动时,直接修改其 pos

  6. Player 类

    • 表示玩家,包含当前位置 Position pos

    • 核心方法 attemptMove(Direction dir, Level level)

      1. 根据方向 dir 计算玩家想要前进的一格 nextPos

      2. 检查 nextPos 是否在地图内,若越界返回 false

      3. 检查 tiles[nextPos] 是否可通行,若是墙返回 false

      4. 调用 level.getBoxAtPosition(nextPos) 查看 nextPos 是否有箱子:

        • 如果没有箱子,则直接 pos = nextPos,返回 true

        • 如果有箱子,则计算 beyondPos = nextPos.translate(dir),并依次检查越界、是否可通行、是否还有其他箱子;若都合法,则先更新箱子位置到 beyondPos,再更新玩家到 nextPos,返回 true;否则返回 false

    • 该方法实现了“推箱”核心逻辑:只能推而不能拉,且要检查箱子前方是否空位或目标。

  7. Level 类

    • 接收一个字符串数组 initialMap,其中每个字符表示不同的地图元素(墙、地面、目标、箱子、玩家等)。

    • parseInitialMap() 方法将字符解析为 Tile 对象,并实例化 PlayerBox 对象列表。解析过程:

      • #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 方法,以便其他类获取 tilesplayerboxesstepsrowscols 等信息。

  8. ConsoleRenderer 类

    • 提供 render(Level level) 方法,将当前地图状态打印到控制台。渲染逻辑:

      1. 先打印列坐标(从 0 开始)。

      2. 遍历行列,判断每个格子是否包含玩家(level.getPlayer().getPosition()),如果是则根据此 Tile 是否为目标选择 @+

      3. 否则判断是否有箱子(level.getBoxAtPosition(pos)),如果有,则根据 Tile.isGoal() 选择 *(在目标上)或 $(在普通地面)。

      4. 如果玩家与箱子都不在该格,则根据 Tile 类型打印 #(墙壁)、.(目标)或空格(地面)。

      5. 行尾打印当前步数(level.getSteps())。

  9. GameController 类

    • 是游戏的入口与主控类,包含:

      • Level[] levels:存放多个预设关卡。

      • int currentLevelIndex:当前关卡索引,从 0 开始。

      • Scanner scanner:读取用户输入。

    • 构造方法 GameController()

      1. 调用 initializeLevels(),硬编码三个示例关卡的字符串数组并创建 Level 对象;

      2. currentLevelIndex = 0

      3. 初始化 scanner

    • start() 方法:

      1. 打印欢迎信息。

      2. 进入 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

      3. 退出主循环后,关闭 scanner 并结束程序。


项目详细总结

  1. 功能完整且逻辑清晰

    • 本项目实现了推箱子游戏的核心玩法:玩家上下左右移动、推箱子、检查合法性、胜利判定、关卡重置等。

    • 通过字符界面在控制台呈现,简单易懂,适合快速上手。

  2. 面向对象设计

    • 利用 TilePlayerBoxLevelConsoleRendererGameController 等类,把游戏要素和逻辑分工明确,职责单一、易于维护。

    • 枚举 Direction 便于方向判读和偏移计算。

  3. 关卡可重置且易扩展

    • Level 类保留了初始状态的深拷贝,实现了 reset() 功能,让玩家可以随时重设当前关卡。

    • 关卡定义放在 initializeLevels(),只需添加新的 String[] 地图,即可增加关卡或修改布局。

  4. 用户体验与易用性

    • 控制台渲染清晰,打印行列坐标帮助玩家定位;展示玩家、箱子、目标叠加状态(+*)一目了然。

    • 每步移动后自动刷新界面并显示当前步数,让玩家了解进度;非法操作给出友好提示。

  5. 可扩展性与后续方向

    • 可以方便地将关卡加载改为从外部文本文件读取,只需把 String[] mapLines 替换成读取文件行即可。

    • 提供了合理的类结构,可后续加入撤销(Undo)功能、自动求解(Solver)、图形界面(Swing/JavaFX)、网络对战等。

  6. 性能表现

    • 地图规模通常在几十行以内,玩家每次移动只做常量级的检查与更新,不会有性能瓶颈。

    • 渲染是按行列遍历,一般地图尺寸不大,刷新速度足够快。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值