Skip to content

Commit

Permalink
feat: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ShenMian committed Mar 1, 2024
0 parents commit 5d438e6
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 0 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/deploy.yml
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
book
6 changes: 6 additions & 0 deletions book.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[book]
title = "Sokoban Tutorial"
authors = ["ShenMian <sms_school@outlook.com>"]
language = "cn"
multilingual = false
src = "src"
8 changes: 8 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Summary

[介绍](introduction.md)

- [关卡](level/README.md)
- [解析](level/parse.md)
- [标准化](level/normalization.md)
- [术语表](glossary.md)
Binary file added src/assets/boxworld_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/glossary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 术语表

| 中文 | 英文 |
| ----------- | -------- |
| 求解器 | Solver |
| 解/解决方案 | Solution |
11 changes: 11 additions & 0 deletions src/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 介绍

本文将描述如何实现一个推箱子游戏, 代码示例使用 Rust 语言编写.
推箱子游戏具有以下特点:

- 规则简单. 可以专注于实现功能, 而非理解复杂的游戏规则和机制.
- 基本功能易于实现.
- 有具有挑战的高级功能. 比如纯鼠标控制(也称为点推), 逆推等.
- 有需要深入钻研的求解器, 用于自动求解推箱子关卡.

本文将由浅入深的介绍上面功能并提供实现的思路.
129 changes: 129 additions & 0 deletions src/level/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# 关卡

推箱子关卡使用最广泛的格式为 XSB, 最初由 XSokoban 所使用. 该格式使用 ASCII 字符来表示地图元素, 并支持注释和附加元数据.
以关卡 `Boxworld #1` 为例:

<img src="../assets/boxworld_1.png" alt="Boxworld #1" width="70%" style="display: block; margin: 0 auto"/>

其 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 |

## 表示地图

地图共包含 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 个数组), 进行调整地图尺寸等操作的代码会更简单高效.
而且能确保动态数组的元素是紧密排列的, 根据数据局部性原理, 读取数据的性能通常会更好.

## 表示关卡

关卡数据可分为三个部分: 地图数据, 元数据和注释. 其中注释可以作为元数据.
元数据是一个键值对的集合, 因此可以使用 HashMap 来存储.
可以使用下面的结构体存储关卡数据:

```rs
pub struct Level {
map: Map,
metadata: HashMap<String, String>,
// ... SKIP ...
}
```

## 行程编码(Run length encoding)

行程编码(Run length encoding, RLE)经常被用于压缩推箱子的关卡和解决方案.

```txt
###
#.###
#*$ #
# @ #
#####
```

经 RLE 编码后可得:

```txt
3#
#.3#
#*$-#
#--@#
5#
```

可以看出, 虽然编码后的关卡有更小的体积, 但不再能直观地看出关卡的结构.

RLE 编码后的关卡通常还会使用 `|` 来分割行, 而非 `\n`. 使其看上去更加紧凑:

```txt
3#|#.3#|#*$-#|#--@#|5#
```
79 changes: 79 additions & 0 deletions src/level/normalization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 标准化

有些关卡为了美观会在原关卡的基础上添加一些装饰, 但这些装饰并不影响关卡的解决方案, 本质上还是一样的关卡.
标准化可以将关卡中与求解无关/无用的元素去除, 这有利于本质相同的关卡的去重.

理想状态下, 具有相同最优解的关卡都可以被标准化为同一个关卡.

关卡的标准化可以分为以下几个步骤:

1. 将不可移动的箱子变为墙.

箱子可能默认就处于死锁状态, 所以可以当作墙体处理.

2. 将无需使用的地板变为墙.

被且仅被三面墙包围的地板是死胡同, 可以当作墙处理.

3. 去除无法到达的墙.

无法于玩家产生交互, 因此删除不影响关卡的解决方案.

4. 标准化外墙.

外墙有不同的包围类型, 如:

```txt
### #####
#@$.# #@$.#
### #####
```
应该统一为其中一种类型, 通常标准化为右边的类型.
5. 去除无法到达的箱子.
无法于玩家产生交互, 因此删除不影响关卡的解决方案.
6. 收紧地图尺寸.
地图中的元素被删除可能导致地图尺寸缩小.
7. 对旋转和翻转进行标准化.
将经过不同旋转和翻转的关卡标准化为同一个关卡.
一个简单的方法是计算不同旋转和翻转后地图的哈希值,选择哈希值最小的版本.
8. 玩家初始位置标准化.
需要对先对玩家的位置进行标准化. 因为即使玩家位置不一样, 但都在同一个区域也可以转变为同一个关卡.
一个简单的玩家位置标准化方法是将玩家的位置设为可达区域的位置中 X 坐标最小, 其次 Y 坐标最小的位置.
## 激进的标准化
激进的标准化可能改变关卡的解.
以最简单的关卡为例:
```txt
#####
#@$.#
#####
```

因为玩家开始的位置三面有墙, 位于死胡同, 只能向右移动. 得到结果:

```txt
#####
# @*#
#####
```

玩家左侧死胡同属于无用的区域, 使用墙体填充. 玩家右侧位于目标上的箱子处于死锁状态, 属于无用的箱子和目标, 使用墙体填充. 最后去除多余的墙体得到激进的标准化结果:

```txt
###
#@#
###
```

这是一个最简单的非标准关卡, 因为其没有箱子和目标, 解为空.
7 changes: 7 additions & 0 deletions src/level/parse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 解析

## 单个关卡解析

## 多个关卡解析

## 从解决方案构建关卡

0 comments on commit 5d438e6

Please sign in to comment.