-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 125793f
Showing
14 changed files
with
781 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
book |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Sokoban Tutorial | ||
|
||
本文将描述如何实现一个推箱子游戏 📦, 代码示例使用 Rust 语言 🦀 编写. | ||
|
||
完整的项目代码位于: | ||
|
||
- [ShenMian/soukoban](https://github.com/shenmian/soukoban): 推箱子相关算法实现. | ||
- [ShenMian/sokoban-rs](https://github.com/shenmian/sokoban-rs): 推箱子游戏实现, 使用 [Bevy] 引擎. | ||
|
||
[bevy]: https://github.com/bevyengine/bevy |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
[book] | ||
title = "Sokoban Tutorial" | ||
authors = ["ShenMian <sms_school@outlook.com>"] | ||
language = "cn" | ||
multilingual = false | ||
src = "src" | ||
|
||
[output.html] | ||
git-repository-url = "https://github.com/ShenMian/sokoban-tutorial" | ||
git-repository-icon = "fa-github" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Summary | ||
|
||
[介绍](introduction.md) | ||
|
||
- [关卡](level/README.md) | ||
- [构建](level/construct.md) | ||
- [标准化](level/normalization.md) | ||
- [动作]() | ||
- [求解器]() | ||
- [术语表](glossary.md) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# 术语表 | ||
|
||
| 中文 | 英文 | | ||
| ----------- | -------- | | ||
| 求解器 | solver | | ||
| 解/解决方案 | solution | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# 介绍 | ||
|
||
本文将描述如何实现一个推箱子游戏 📦, 代码示例使用 Rust 语言 🦀 编写. | ||
推箱子游戏具有以下特点: | ||
|
||
- 规则简单. 可以专注于实现功能, 而非理解复杂的游戏规则和机制. | ||
- 基本功能易于实现. | ||
- 有具有挑战的高级功能. 比如纯鼠标控制, 逆推等. | ||
- 有需要深入钻研的求解器, 用于自动求解推箱子关卡. | ||
|
||
本文将由浅入深的介绍上面功能并提供实现的思路. | ||
|
||
本文不会探讨的内容有: | ||
|
||
- 推箱子的其他变种. 感兴趣的读者可以自行了解, 可以作为一种拓展. | ||
- Rust 语言本身. 本文中出现的算法适用于其他编程语言. 因为示例代码使用了 Rust 语言, 可能会探讨 Rust 实现该算法的细节, 但不会专门介绍 Rust 语言的特性. | ||
- 游戏引擎. | ||
|
||
完整的项目代码位于: | ||
|
||
- [ShenMian/soukoban](https://github.com/shenmian/soukoban): 推箱子相关算法实现. | ||
- [ShenMian/sokoban-rs](https://github.com/shenmian/sokoban-rs): 推箱子游戏实现, 使用 [Bevy] 引擎. | ||
|
||
[bevy]: https://github.com/bevyengine/bevy |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
# 关卡 | ||
|
||
推箱子关卡使用最广泛的格式为 XSB, 最初由 XSokoban 所使用. 该格式使用 ASCII 字符来表示地图元素, 并支持注释和附加元数据. | ||
以关卡 `Boxworld #1` 为例: | ||
|
||
![Boxworld #1](../assets/boxworld_1.png) | ||
|
||
其 XSB 格式关卡的数据如下: | ||
|
||
```txt | ||
;Level 1 | ||
__###___ | ||
__#.#___ | ||
__#-#### | ||
###$-$.# | ||
#.-$@### | ||
####$#__ | ||
___#.#__ | ||
___###__ | ||
Title: Boxworld 1 | ||
Author: Thinking Rabbit | ||
``` | ||
|
||
- 第 1 行, 以 `;` 开头的单行注释. | ||
- 第 2-9 行, 使用 ASCII 字符表示的地图数据. | ||
- 第 10-11 行, 包括关卡标题和作者的元数据. | ||
|
||
| ASCII 符号 | 描述 | | ||
| ----------------- | -------------- | | ||
| `<SPACE>`/`-`/`_` | Floor | | ||
| `#` | Wall | | ||
| `$` | Box | | ||
| `.` | Goal | | ||
| `@` | Player | | ||
| `+` | Player on goal | | ||
| `*` | Box on goal | | ||
|
||
除了上面的元数据, 还有一种用于多行注释的特殊元数据. 内容通过 `comment:` 和 `comment-end:` 包裹. | ||
|
||
## 表示地图 | ||
|
||
地图共包含 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<Tiles>, | ||
dimensions: Vector2<i32>, | ||
// ... SKIP ... | ||
} | ||
``` | ||
|
||
使用一维数组而非二维数组是因为一维数组更平坦(flatten), 进行部分操作时更简单高效. 如: | ||
|
||
```rs | ||
impl Map { | ||
pub fn with_dimensions(dimensions: Vector2<i32>) -> Self { | ||
Self { | ||
data: vec![Tiles::empty(); (dimensions.x * dimensions.y) as usize], | ||
dimensions, | ||
// ... SKIP ... | ||
} | ||
} | ||
// ... SKIP ... | ||
} | ||
``` | ||
|
||
因为只需要操作一个数组(而非 n 个数组), 进行调整地图尺寸等操作的代码会更简单高效. | ||
而且能确保动态数组的元素是紧密排列的, 根据数据局部性原理, 读取数据的性能通常会更好. | ||
|
||
重载下标运算符, 以便后续直接通过二维坐标访问地图元素. | ||
|
||
```rs | ||
impl Index<Vector2<i32>> for Map { | ||
type Output = Tiles; | ||
fn index(&self, position: Vector2<i32>) -> &Tiles { | ||
&self.data[(position.y * self.dimensions.x + position.x) as usize] | ||
} | ||
} | ||
|
||
impl IndexMut<Vector2<i32>> for Map { | ||
fn index_mut(&mut self, position: Vector2<i32>) -> &mut Tiles { | ||
&mut self.data[(position.y * self.dimensions.x + position.x) as usize] | ||
} | ||
} | ||
``` | ||
|
||
## 表示关卡 | ||
|
||
关卡数据可分为三个部分: 地图数据, 元数据和注释. 其中注释可以作为元数据. | ||
元数据是一个键值对的集合, 因此可以使用 HashMap 来存储. | ||
可以使用下面的结构体存储关卡数据: | ||
|
||
```rs | ||
pub struct Level { | ||
map: Map, | ||
metadata: HashMap<String, String>, | ||
// ... SKIP ... | ||
} | ||
``` | ||
|
||
将地图从关卡中拆分是因为地图涉及大量的关联函数, 如果都放在 `Level` 里会导致其变得过于庞大. | ||
为了提升代码的可读性和可维护性, 将相关代码拆分到 `Map` 中, 并将 `Level` 的 Deref 操作指向 `Map`. 并通过实现 [Deref trait], 使得用户能够透明地引用到 `Level` 内部的 Map. | ||
|
||
## 行程编码(Run-length encoding) | ||
|
||
[行程编码](run-length encoding, RLE)经常被用于压缩推箱子的关卡和解决方案. | ||
|
||
```txt | ||
### | ||
#.### | ||
#*$ # | ||
# @ # | ||
##### | ||
``` | ||
|
||
经 RLE 编码后可得: | ||
|
||
```txt | ||
3# | ||
#.3# | ||
#*$-# | ||
#--@# | ||
5# | ||
``` | ||
|
||
可以看出, 虽然编码后的关卡有更小的体积, 但不再能直观地看出关卡的结构. | ||
|
||
RLE 编码后的关卡通常还会使用 `|` 来分割行, 而非 `\n`. 使其看上去更加紧凑: | ||
|
||
```txt | ||
3#|#.3#|#*$-#|#--@#|5# | ||
``` | ||
|
||
## 特殊关卡 | ||
|
||
### 玩家不可达区域存在箱子 | ||
|
||
![Sasquatch #41](../assets/sasquatch_41.png) | ||
|
||
```txt | ||
##### | ||
# # | ||
### ######## | ||
## *** # # # | ||
# * * ## # ##### | ||
## *** ## # ## ## | ||
### #### # # # # | ||
# # # # ####$ $### | ||
## ## # ## $...$ ## | ||
##### # ## .@. # | ||
# # # $...$ ## | ||
########$ $### | ||
# # | ||
##### | ||
``` | ||
|
||
### 存在只有空元素的行 | ||
|
||
!["Steaming Hot" by David Buchweitz](../assets/steaming_hot.png) | ||
|
||
```txt | ||
# # | ||
# # # | ||
# # # | ||
# # # | ||
# # # | ||
# # # | ||
# # # | ||
- | ||
########## | ||
#........#### | ||
# $$$$$$$# # | ||
#.$......# # | ||
# $$$$$$ # # | ||
#......$+# # | ||
#$$$$$$$ # # | ||
# #### | ||
########## | ||
``` | ||
|
||
### 无完整外墙 | ||
|
||
部分推箱子程序支持无完整外墙的关卡. | ||
在本文中, 这种关卡属于无效关卡. 但可以通过为其添加外墙的方式来转换为有效关卡. | ||
|
||
!["No walls" by Rincewind](../assets/no_walls_level.png) | ||
|
||
```txt | ||
* ** * | ||
** | ||
**@$.* | ||
** | ||
* ** * | ||
``` | ||
|
||
## 参考 | ||
|
||
- <http://sokobano.de/wiki/index.php?title=Level_format> | ||
|
||
[Deref trait]: https://doc.rust-lang.org/std/ops/trait.Deref.html | ||
[行程编码]: https://en.wikipedia.org/wiki/Run-length_encoding |
Oops, something went wrong.