diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..22fe843 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,33 @@ +name: Test + +on: + push: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: write + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup mdBook + uses: peaceiris/actions-mdbook@v1 + with: + mdbook-version: 'latest' + + - run: mdbook build + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + if: ${{ github.ref == 'refs/heads/main' }} + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./book \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7585238 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +book diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..be08e47 --- /dev/null +++ b/book.toml @@ -0,0 +1,6 @@ +[book] +title = "Sokoban Tutorial" +authors = ["ShenMian "] +language = "cn" +multilingual = false +src = "src" diff --git a/src/SUMMARY.md b/src/SUMMARY.md new file mode 100644 index 0000000..56d40ca --- /dev/null +++ b/src/SUMMARY.md @@ -0,0 +1,7 @@ +# Summary + +[介绍](introduction.md) + +- [关卡](level/README.md) + - [解析](level/parse.md) + - [标准化](level/normalization.md) diff --git a/src/assets/boxworld_1.png b/src/assets/boxworld_1.png new file mode 100644 index 0000000..c3ed1b7 Binary files /dev/null and b/src/assets/boxworld_1.png differ diff --git a/src/introduction.md b/src/introduction.md new file mode 100644 index 0000000..800c027 --- /dev/null +++ b/src/introduction.md @@ -0,0 +1,11 @@ +# 介绍 + +本文将描述如何实现一个推箱子游戏, 代码示例使用 Rust 语言编写. +推箱子游戏具有以下特点: + +- 规则简单. 可以专注于实现功能, 而非理解复杂的游戏规则和机制. +- 基本功能易于实现. +- 有具有挑战的高级功能. 比如纯鼠标控制(也称为点推), 逆推等. +- 有需要深入钻研的求解器, 用于自动求解推箱子关卡. + +本文将由浅入深的介绍上面功能并提供实现的思路. diff --git a/src/level/README.md b/src/level/README.md new file mode 100644 index 0000000..a06f6ef --- /dev/null +++ b/src/level/README.md @@ -0,0 +1,126 @@ +# 关卡 + +推箱子关卡使用最广泛的格式为 XSB, 最初由 XSokoban 所使用. 该格式使用 ASCII 字符来表示地图元素, 支持注释和附加元数据. +以关卡 `Boxworld #1` 为例: + +Boxworld #1 + +其 XSB 格式关卡的数据如下: + +```txt +;Level 1 +__###___ +__#.#___ +__#-#### +###$-$.# +#.-$@### +####$#__ +___#.#__ +___###__ +Title: Boxworld 1 +Author: Thinking Rabbit +``` + +- 第 1 行, 以 `;` 开头的单行注释. +- 第 2-9 行, 使用 ASCII 字符表示的地图数据. +- 第 10-11 行, 包括关卡标题和作者的元数据. + +| ASCII 符号 | 描述 | +| ----------------- | -------------- | +| ``/`-`/`_` | Floor | +| `#` | Wall | +| `$` | Box | +| `.` | Goal | +| `@` | Player | +| `+` | Player on goal | +| `*` | Box on goal | + +## 表示地图 + +地图共包含 5 种元素, 其中部分元素可能叠加(比如玩家位于目标上). 因此可以使用比特位来表示地图中的每个格子包含哪些元素. +创建用于表示地图元素的比特位: + +```rs +use bitflags::bitflags; + +bitflags! { + pub struct Tiles: u8 { + const Floor = 1 << 0; + const Wall = 1 << 1; + const Box = 1 << 2; + const Goal = 1 << 3; + const Player = 1 << 4; + } +} +``` + +使用一维数组来存储地图数据并使用二维向量存储地图尺寸. + +```rs +use nalgebra::Vector2; + +pub struct Map { + data: Vec, + dimensions: Vector2, + // ... SKIP ... +} +``` + +使用一维数组而非二维数组是因为一维数组更平坦, 进行部分操作时更简单高效: + +```rs +impl Map { + pub fn with_dimensions(dimensions: Vector2) -> Self { + Self { + data: vec![Tiles::empty(); (dimensions.x * dimensions.y) as usize], + dimensions, + // ... SKIP ... + } + } + // ... SKIP ... +} +``` + +## 表示关卡 + +关卡数据可分为三个部分: 地图数据, 元数据和注释. 其中注释可以作为元数据. +元数据是一个键值对的集合, 因此可以使用 HashMap 来存储. +可以使用下面的结构体存储关卡数据: + +```rs +pub struct Level { + map: Map, + metadata: HashMap, + // ... SKIP ... +} +``` + +## 行程编码(Run length encoding) + +行程编码(Run length encoding, RLE)经常被用于压缩推箱子的关卡和解决方案. + +```txt +### +#.### +#*$ # +# @ # +##### +``` + +经 RLE 编码后可得: + +```txt +3# +#.3# +#*$-# +#--@# +5# +``` + +可以看出, 虽然编码后的关卡有更小的体积, 但不再能直观地看出关卡的结构. + +RLE 编码后的关卡通常还会使用 `|` 来分割行, 而非 `\n`. 使其看上去更加紧凑: + +```txt +3#|#.3#|#*$-#|#--@#|5# +``` diff --git a/src/level/normalization.md b/src/level/normalization.md new file mode 100644 index 0000000..b4b0af3 --- /dev/null +++ b/src/level/normalization.md @@ -0,0 +1,32 @@ +# 标准化 + +TODO + +## 激进的标准化 + +激进的标准化可能改变关卡的解. +以最简单的关卡为例: + +```txt +##### +#@$.# +##### +``` + +因为玩家开始的位置三面有墙, 位于死胡同, 只能向右移动. 得到结果: + +```txt +##### +# @*# +##### +``` + +玩家左侧死胡同属于无用的区域, 使用墙体填充. 玩家右侧位于目标上的箱子处于死锁状态, 属于无用的箱子和目标, 使用墙体填充. 最后去除多余的墙体得到激进的标准化结果: + +```txt +### +#@# +### +``` + +这是一个最简单的非标准关卡, 因为其没有箱子和目标, 解为空. diff --git a/src/level/parse.md b/src/level/parse.md new file mode 100644 index 0000000..316a637 --- /dev/null +++ b/src/level/parse.md @@ -0,0 +1,5 @@ +# 解析 + +## 单个关卡解析 + +## 多个关卡解析