rumen 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rumen-0.1.0/Cargo.lock +216 -0
- rumen-0.1.0/Cargo.toml +23 -0
- rumen-0.1.0/LICENSE +21 -0
- rumen-0.1.0/LICENSES/tetris-fumen.txt +24 -0
- rumen-0.1.0/PKG-INFO +108 -0
- rumen-0.1.0/README.md +97 -0
- rumen-0.1.0/pyproject.toml +16 -0
- rumen-0.1.0/rumen.pyi +121 -0
- rumen-0.1.0/src/block.rs +32 -0
- rumen-0.1.0/src/coding/base64.rs +25 -0
- rumen-0.1.0/src/coding/base96.rs +7 -0
- rumen-0.1.0/src/coding/unicode.rs +142 -0
- rumen-0.1.0/src/coding.rs +3 -0
- rumen-0.1.0/src/color_scheme.rs +17 -0
- rumen-0.1.0/src/decode.rs +142 -0
- rumen-0.1.0/src/error.rs +26 -0
- rumen-0.1.0/src/facing.rs +22 -0
- rumen-0.1.0/src/lib.rs +72 -0
- rumen-0.1.0/src/location.rs +43 -0
- rumen-0.1.0/src/main.rs +22 -0
- rumen-0.1.0/src/page.rs +277 -0
- rumen-0.1.0/src/python.rs +108 -0
- rumen-0.1.0/src/quiz.rs +209 -0
- rumen-0.1.0/src/view_mode.rs +22 -0
- rumen-0.1.0/tests/decoder.rs +274 -0
- rumen-0.1.0/tests/fixtures/v115.json +11435 -0
- rumen-0.1.0/tests/generate_fixtures.cjs +164 -0
- rumen-0.1.0/tests/stubtest.allowlist +2 -0
- rumen-0.1.0/tests/test_python.py +202 -0
rumen-0.1.0/Cargo.lock
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "heck"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
10
|
+
|
|
11
|
+
[[package]]
|
|
12
|
+
name = "itoa"
|
|
13
|
+
version = "1.0.18"
|
|
14
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
15
|
+
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "libc"
|
|
19
|
+
version = "0.2.191"
|
|
20
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
21
|
+
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
|
22
|
+
|
|
23
|
+
[[package]]
|
|
24
|
+
name = "memchr"
|
|
25
|
+
version = "2.7.4"
|
|
26
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
27
|
+
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
|
28
|
+
|
|
29
|
+
[[package]]
|
|
30
|
+
name = "once_cell"
|
|
31
|
+
version = "1.21.4"
|
|
32
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
33
|
+
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
34
|
+
|
|
35
|
+
[[package]]
|
|
36
|
+
name = "portable-atomic"
|
|
37
|
+
version = "1.15.0"
|
|
38
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
39
|
+
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
|
40
|
+
|
|
41
|
+
[[package]]
|
|
42
|
+
name = "proc-macro2"
|
|
43
|
+
version = "1.0.107"
|
|
44
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
45
|
+
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
|
46
|
+
dependencies = [
|
|
47
|
+
"unicode-ident",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[[package]]
|
|
51
|
+
name = "pyo3"
|
|
52
|
+
version = "0.30.1"
|
|
53
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
54
|
+
checksum = "229ca13b8a15bb0d1487b81ab975bfcd97513416499a8312011b6036e42e0de4"
|
|
55
|
+
dependencies = [
|
|
56
|
+
"libc",
|
|
57
|
+
"once_cell",
|
|
58
|
+
"portable-atomic",
|
|
59
|
+
"pyo3-build-config",
|
|
60
|
+
"pyo3-ffi",
|
|
61
|
+
"pyo3-macros",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[[package]]
|
|
65
|
+
name = "pyo3-build-config"
|
|
66
|
+
version = "0.30.1"
|
|
67
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
68
|
+
checksum = "70130e2b6dd1ee589a8788026a429561ca5c40fa7a2fa7914c2282eddae24700"
|
|
69
|
+
dependencies = [
|
|
70
|
+
"target-lexicon",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[[package]]
|
|
74
|
+
name = "pyo3-ffi"
|
|
75
|
+
version = "0.30.1"
|
|
76
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
77
|
+
checksum = "5e7ff73c555c286d212d4ce7e5fbda845a973c4f61bd5f160f5f2c3c2a04d1c4"
|
|
78
|
+
dependencies = [
|
|
79
|
+
"libc",
|
|
80
|
+
"pyo3-build-config",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
[[package]]
|
|
84
|
+
name = "pyo3-macros"
|
|
85
|
+
version = "0.30.1"
|
|
86
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
87
|
+
checksum = "c46d2e5862c7bb8207e3b639f713dda3c499933444784154c11d013c5a691b60"
|
|
88
|
+
dependencies = [
|
|
89
|
+
"proc-macro2",
|
|
90
|
+
"pyo3-macros-backend",
|
|
91
|
+
"quote",
|
|
92
|
+
"syn",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[[package]]
|
|
96
|
+
name = "pyo3-macros-backend"
|
|
97
|
+
version = "0.30.1"
|
|
98
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
99
|
+
checksum = "38b89f066f2ae72a2aef52d1537ca2fcd104440ebfadee06e97aa90738a25b9b"
|
|
100
|
+
dependencies = [
|
|
101
|
+
"heck",
|
|
102
|
+
"proc-macro2",
|
|
103
|
+
"quote",
|
|
104
|
+
"syn",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
[[package]]
|
|
108
|
+
name = "quote"
|
|
109
|
+
version = "1.0.37"
|
|
110
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
111
|
+
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
|
112
|
+
dependencies = [
|
|
113
|
+
"proc-macro2",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
[[package]]
|
|
117
|
+
name = "rumen"
|
|
118
|
+
version = "0.1.0"
|
|
119
|
+
dependencies = [
|
|
120
|
+
"pyo3",
|
|
121
|
+
"serde",
|
|
122
|
+
"serde_json",
|
|
123
|
+
"thiserror",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
[[package]]
|
|
127
|
+
name = "serde"
|
|
128
|
+
version = "1.0.229"
|
|
129
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
130
|
+
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
|
131
|
+
dependencies = [
|
|
132
|
+
"serde_core",
|
|
133
|
+
"serde_derive",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
[[package]]
|
|
137
|
+
name = "serde_core"
|
|
138
|
+
version = "1.0.229"
|
|
139
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
140
|
+
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
|
141
|
+
dependencies = [
|
|
142
|
+
"serde_derive",
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
[[package]]
|
|
146
|
+
name = "serde_derive"
|
|
147
|
+
version = "1.0.229"
|
|
148
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
149
|
+
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|
150
|
+
dependencies = [
|
|
151
|
+
"proc-macro2",
|
|
152
|
+
"quote",
|
|
153
|
+
"syn",
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
[[package]]
|
|
157
|
+
name = "serde_json"
|
|
158
|
+
version = "1.0.154"
|
|
159
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
160
|
+
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
|
161
|
+
dependencies = [
|
|
162
|
+
"itoa",
|
|
163
|
+
"memchr",
|
|
164
|
+
"serde",
|
|
165
|
+
"serde_core",
|
|
166
|
+
"zmij",
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
[[package]]
|
|
170
|
+
name = "syn"
|
|
171
|
+
version = "3.0.9"
|
|
172
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
173
|
+
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
|
174
|
+
dependencies = [
|
|
175
|
+
"proc-macro2",
|
|
176
|
+
"quote",
|
|
177
|
+
"unicode-ident",
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
[[package]]
|
|
181
|
+
name = "target-lexicon"
|
|
182
|
+
version = "0.13.5"
|
|
183
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
184
|
+
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
185
|
+
|
|
186
|
+
[[package]]
|
|
187
|
+
name = "thiserror"
|
|
188
|
+
version = "2.0.21"
|
|
189
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
190
|
+
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
|
191
|
+
dependencies = [
|
|
192
|
+
"thiserror-impl",
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
[[package]]
|
|
196
|
+
name = "thiserror-impl"
|
|
197
|
+
version = "2.0.21"
|
|
198
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
199
|
+
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
|
200
|
+
dependencies = [
|
|
201
|
+
"proc-macro2",
|
|
202
|
+
"quote",
|
|
203
|
+
"syn",
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
[[package]]
|
|
207
|
+
name = "unicode-ident"
|
|
208
|
+
version = "1.0.13"
|
|
209
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
210
|
+
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
|
211
|
+
|
|
212
|
+
[[package]]
|
|
213
|
+
name = "zmij"
|
|
214
|
+
version = "1.0.23"
|
|
215
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
216
|
+
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
rumen-0.1.0/Cargo.toml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rumen"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Fumen v115 decoder with optional Python bindings"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
repository = "https://github.com/A-Minos/rumen"
|
|
8
|
+
include = ["src/**/*.rs", "tests/**/*", "LICENSES/*", "Cargo.toml", "Cargo.lock", "pyproject.toml", "rumen.pyi", "/README.md", "/LICENSE"]
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
|
|
11
|
+
[lib]
|
|
12
|
+
crate-type = ["rlib", "cdylib"]
|
|
13
|
+
|
|
14
|
+
[features]
|
|
15
|
+
python = ["dep:pyo3"]
|
|
16
|
+
|
|
17
|
+
[dependencies]
|
|
18
|
+
pyo3 = { version = "0.30.1", optional = true, features = ["abi3-py311"] }
|
|
19
|
+
thiserror = "2.0.21"
|
|
20
|
+
|
|
21
|
+
[dev-dependencies]
|
|
22
|
+
serde = { version = "1", features = ["derive"] }
|
|
23
|
+
serde_json = "1"
|
rumen-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shoucandanghehe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
tetris-fumen 1.1.3 — https://github.com/knewjade/tetris-fumen
|
|
2
|
+
Reference revision: 4a77d0dc52216432227a43847012deed1b2f4536
|
|
3
|
+
|
|
4
|
+
MIT License
|
|
5
|
+
|
|
6
|
+
Copyright (c) 2019
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
rumen-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rumen
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
License-File: LICENSE
|
|
5
|
+
License-File: LICENSES/tetris-fumen.txt
|
|
6
|
+
Summary: Fumen v115 decoder backed by Rust
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
10
|
+
|
|
11
|
+
# rumen
|
|
12
|
+
|
|
13
|
+
**小方块,大吞吐。Rust 干重活,Python 管好用。**
|
|
14
|
+
|
|
15
|
+
用 Rust 编写的 **fumen v115 解码器**,通过 PyO3 提供 Python 绑定。输入谱面字符串或查看器链接,得到盘面、操作和注释,用来分析谱面、处理训练数据,或者接进你的方块工具。
|
|
16
|
+
|
|
17
|
+
- 每页独立的 `24 × 10` 盘面快照、方块操作与事件标志,支持注释继承和 Quiz。
|
|
18
|
+
- 紧凑盘面存储、原地转义、Quiz 缓冲区复用,减少重复分配与搬运。
|
|
19
|
+
- Python 支持按页访问、批量整数矩阵和类型提示,解码时释放 GIL。
|
|
20
|
+
|
|
21
|
+
## Python:几行就够
|
|
22
|
+
|
|
23
|
+
需要 **Python 3.11+、Rust 工具链和 uv**。以下从源码安装,在仓库根目录执行:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
uv venv
|
|
27
|
+
uv pip install .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
用 `.venv` 中的 Python 运行:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import rumen
|
|
34
|
+
|
|
35
|
+
fumen = rumen.parse("v115@ghE8JeAgHvhAAAA")
|
|
36
|
+
page = fumen[0]
|
|
37
|
+
matrix = page.to_matrix() # list[list[int]],24 行 × 10 列
|
|
38
|
+
|
|
39
|
+
for page in fumen:
|
|
40
|
+
print(page.notes, page.flag.positions())
|
|
41
|
+
|
|
42
|
+
matrices = fumen.to_matrices() # list[list[list[int]]],页 / 行 / 列
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`fumen` 支持长度、下标、负索引和迭代。模型属性只读,导出的矩阵是独立的可变列表。
|
|
46
|
+
|
|
47
|
+
## Rust:直接接上
|
|
48
|
+
|
|
49
|
+
在消费项目的 `Cargo.toml` 中添加本地依赖,把路径指向这个仓库:
|
|
50
|
+
|
|
51
|
+
```toml
|
|
52
|
+
[dependencies]
|
|
53
|
+
rumen = { path = "../rumen" }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```rust
|
|
57
|
+
use rumen::DecodeError;
|
|
58
|
+
|
|
59
|
+
fn main() -> Result<(), DecodeError> {
|
|
60
|
+
let fumen = rumen::parse("https://fumen.zui.jp/?v115@ghE8JeAgHvhAAAA")?;
|
|
61
|
+
|
|
62
|
+
for page in &fumen.pages {
|
|
63
|
+
println!("{} {:?}", page.notes, page.flag.positions());
|
|
64
|
+
}
|
|
65
|
+
Ok(())
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`Fumen` 也实现了 `FromStr`。原生 Rust 使用不需要开启 `python` feature。
|
|
70
|
+
|
|
71
|
+
### 命令行也能用
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
cargo run --release -- 'v115@ghE8JeAgHvhAAAA'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
输出完整的 Rust Debug 结构,方便检查谱面。
|
|
78
|
+
|
|
79
|
+
## 接口约定
|
|
80
|
+
|
|
81
|
+
- 盘面是当前页操作前的状态,按 `[y][x]` 访问;左上角为 `(0, 0)`,向右、向下递增,`y = 23` 为垃圾行。
|
|
82
|
+
- 矩阵编码 `0..8` 依次对应 `Empty, I, L, O, Z, T, J, S, Garbage`。
|
|
83
|
+
- `flag.location` 与 `flag.positions()` 使用同一套自上而下的坐标。`positions()` 返回方块占据的四个格子,无活动方块时为 `None`;坐标可能超出盘面。
|
|
84
|
+
- `page.notes` 是继承与 Quiz 推进后的可见注释,`page.flag.note` 表示该页是否显式更新了注释。
|
|
85
|
+
- 解码失败时,Rust 返回 `DecodeError`,Python 抛出 `rumen.DecodeError`(`ValueError` 子类)。
|
|
86
|
+
- 目前仅提供 v115 解码,不含编码器。
|
|
87
|
+
|
|
88
|
+
## 开发
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
cargo fmt --check
|
|
92
|
+
cargo test --all-features --locked
|
|
93
|
+
cargo clippy --all-targets --all-features --locked -- -D warnings
|
|
94
|
+
|
|
95
|
+
# 先按上文安装 Python 包;以下为 Linux / macOS 路径
|
|
96
|
+
.venv/bin/python -B -m unittest discover -s tests -p test_python.py
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 发布
|
|
100
|
+
|
|
101
|
+
[Release 工作流](https://github.com/A-Minos/rumen/actions/workflows/Release.yml) 构建 Linux x86_64 / aarch64、macOS x86_64 / arm64、Windows x86_64 的 ABI3 wheel,分别在 CPython 3.11–3.15 上测试,并验证源码包。
|
|
102
|
+
|
|
103
|
+
推送代码、标签或普通手动运行只做检查。发布 GitHub Release 时,标签须为 `v<Cargo.toml 中的版本>`;全部检查通过后,crates.io 与 PyPI 使用 OIDC 发布同一版本。两端 Trusted Publisher 均绑定 `A-Minos/rumen` 和 `Release.yml`,不设置 Environment 限制。
|
|
104
|
+
|
|
105
|
+
crates.io 首个版本需在本地 `cargo publish --locked`,之后才能配置 Trusted Publisher。首发时推送对应版本标签,不创建 GitHub Release;手动运行工作流,选择该标签并勾选 `publish_pypi`,只补发 PyPI。
|
|
106
|
+
|
|
107
|
+
项目采用 [MIT](LICENSE) 许可。协议行为参考 [tetris-fumen](https://github.com/knewjade/tetris-fumen),相关第三方许可见 [LICENSES/tetris-fumen.txt](LICENSES/tetris-fumen.txt)。
|
|
108
|
+
|
rumen-0.1.0/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# rumen
|
|
2
|
+
|
|
3
|
+
**小方块,大吞吐。Rust 干重活,Python 管好用。**
|
|
4
|
+
|
|
5
|
+
用 Rust 编写的 **fumen v115 解码器**,通过 PyO3 提供 Python 绑定。输入谱面字符串或查看器链接,得到盘面、操作和注释,用来分析谱面、处理训练数据,或者接进你的方块工具。
|
|
6
|
+
|
|
7
|
+
- 每页独立的 `24 × 10` 盘面快照、方块操作与事件标志,支持注释继承和 Quiz。
|
|
8
|
+
- 紧凑盘面存储、原地转义、Quiz 缓冲区复用,减少重复分配与搬运。
|
|
9
|
+
- Python 支持按页访问、批量整数矩阵和类型提示,解码时释放 GIL。
|
|
10
|
+
|
|
11
|
+
## Python:几行就够
|
|
12
|
+
|
|
13
|
+
需要 **Python 3.11+、Rust 工具链和 uv**。以下从源码安装,在仓库根目录执行:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
uv venv
|
|
17
|
+
uv pip install .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
用 `.venv` 中的 Python 运行:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import rumen
|
|
24
|
+
|
|
25
|
+
fumen = rumen.parse("v115@ghE8JeAgHvhAAAA")
|
|
26
|
+
page = fumen[0]
|
|
27
|
+
matrix = page.to_matrix() # list[list[int]],24 行 × 10 列
|
|
28
|
+
|
|
29
|
+
for page in fumen:
|
|
30
|
+
print(page.notes, page.flag.positions())
|
|
31
|
+
|
|
32
|
+
matrices = fumen.to_matrices() # list[list[list[int]]],页 / 行 / 列
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`fumen` 支持长度、下标、负索引和迭代。模型属性只读,导出的矩阵是独立的可变列表。
|
|
36
|
+
|
|
37
|
+
## Rust:直接接上
|
|
38
|
+
|
|
39
|
+
在消费项目的 `Cargo.toml` 中添加本地依赖,把路径指向这个仓库:
|
|
40
|
+
|
|
41
|
+
```toml
|
|
42
|
+
[dependencies]
|
|
43
|
+
rumen = { path = "../rumen" }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```rust
|
|
47
|
+
use rumen::DecodeError;
|
|
48
|
+
|
|
49
|
+
fn main() -> Result<(), DecodeError> {
|
|
50
|
+
let fumen = rumen::parse("https://fumen.zui.jp/?v115@ghE8JeAgHvhAAAA")?;
|
|
51
|
+
|
|
52
|
+
for page in &fumen.pages {
|
|
53
|
+
println!("{} {:?}", page.notes, page.flag.positions());
|
|
54
|
+
}
|
|
55
|
+
Ok(())
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`Fumen` 也实现了 `FromStr`。原生 Rust 使用不需要开启 `python` feature。
|
|
60
|
+
|
|
61
|
+
### 命令行也能用
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
cargo run --release -- 'v115@ghE8JeAgHvhAAAA'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
输出完整的 Rust Debug 结构,方便检查谱面。
|
|
68
|
+
|
|
69
|
+
## 接口约定
|
|
70
|
+
|
|
71
|
+
- 盘面是当前页操作前的状态,按 `[y][x]` 访问;左上角为 `(0, 0)`,向右、向下递增,`y = 23` 为垃圾行。
|
|
72
|
+
- 矩阵编码 `0..8` 依次对应 `Empty, I, L, O, Z, T, J, S, Garbage`。
|
|
73
|
+
- `flag.location` 与 `flag.positions()` 使用同一套自上而下的坐标。`positions()` 返回方块占据的四个格子,无活动方块时为 `None`;坐标可能超出盘面。
|
|
74
|
+
- `page.notes` 是继承与 Quiz 推进后的可见注释,`page.flag.note` 表示该页是否显式更新了注释。
|
|
75
|
+
- 解码失败时,Rust 返回 `DecodeError`,Python 抛出 `rumen.DecodeError`(`ValueError` 子类)。
|
|
76
|
+
- 目前仅提供 v115 解码,不含编码器。
|
|
77
|
+
|
|
78
|
+
## 开发
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
cargo fmt --check
|
|
82
|
+
cargo test --all-features --locked
|
|
83
|
+
cargo clippy --all-targets --all-features --locked -- -D warnings
|
|
84
|
+
|
|
85
|
+
# 先按上文安装 Python 包;以下为 Linux / macOS 路径
|
|
86
|
+
.venv/bin/python -B -m unittest discover -s tests -p test_python.py
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 发布
|
|
90
|
+
|
|
91
|
+
[Release 工作流](https://github.com/A-Minos/rumen/actions/workflows/Release.yml) 构建 Linux x86_64 / aarch64、macOS x86_64 / arm64、Windows x86_64 的 ABI3 wheel,分别在 CPython 3.11–3.15 上测试,并验证源码包。
|
|
92
|
+
|
|
93
|
+
推送代码、标签或普通手动运行只做检查。发布 GitHub Release 时,标签须为 `v<Cargo.toml 中的版本>`;全部检查通过后,crates.io 与 PyPI 使用 OIDC 发布同一版本。两端 Trusted Publisher 均绑定 `A-Minos/rumen` 和 `Release.yml`,不设置 Environment 限制。
|
|
94
|
+
|
|
95
|
+
crates.io 首个版本需在本地 `cargo publish --locked`,之后才能配置 Trusted Publisher。首发时推送对应版本标签,不创建 GitHub Release;手动运行工作流,选择该标签并勾选 `publish_pypi`,只补发 PyPI。
|
|
96
|
+
|
|
97
|
+
项目采用 [MIT](LICENSE) 许可。协议行为参考 [tetris-fumen](https://github.com/knewjade/tetris-fumen),相关第三方许可见 [LICENSES/tetris-fumen.txt](LICENSES/tetris-fumen.txt)。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["maturin>=1.9.4,<2"]
|
|
3
|
+
build-backend = "maturin"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rumen"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Fumen v115 decoder backed by Rust"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE", "LICENSES/*"]
|
|
13
|
+
|
|
14
|
+
[tool.maturin]
|
|
15
|
+
features = ["python"]
|
|
16
|
+
bindings = "pyo3"
|
rumen-0.1.0/rumen.pyi
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
from typing import ClassVar, SupportsIndex, final
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"Block",
|
|
6
|
+
"ColorScheme",
|
|
7
|
+
"DecodeError",
|
|
8
|
+
"Facing",
|
|
9
|
+
"Flag",
|
|
10
|
+
"Fumen",
|
|
11
|
+
"Location",
|
|
12
|
+
"Page",
|
|
13
|
+
"ViewMode",
|
|
14
|
+
"parse",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
class DecodeError(ValueError):
|
|
18
|
+
"""A fumen input could not be decoded; the message preserves the Rust error."""
|
|
19
|
+
|
|
20
|
+
@final
|
|
21
|
+
class Block:
|
|
22
|
+
Empty: ClassVar[Block]
|
|
23
|
+
I: ClassVar[Block]
|
|
24
|
+
L: ClassVar[Block]
|
|
25
|
+
O: ClassVar[Block]
|
|
26
|
+
Z: ClassVar[Block]
|
|
27
|
+
T: ClassVar[Block]
|
|
28
|
+
J: ClassVar[Block]
|
|
29
|
+
S: ClassVar[Block]
|
|
30
|
+
Garbage: ClassVar[Block]
|
|
31
|
+
def __int__(self) -> int: ...
|
|
32
|
+
def __eq__(self, other: object, /) -> bool: ...
|
|
33
|
+
def __hash__(self) -> int: ...
|
|
34
|
+
|
|
35
|
+
@final
|
|
36
|
+
class Facing:
|
|
37
|
+
Down: ClassVar[Facing]
|
|
38
|
+
Right: ClassVar[Facing]
|
|
39
|
+
Up: ClassVar[Facing]
|
|
40
|
+
Left: ClassVar[Facing]
|
|
41
|
+
def __int__(self) -> int: ...
|
|
42
|
+
def __eq__(self, other: object, /) -> bool: ...
|
|
43
|
+
def __hash__(self) -> int: ...
|
|
44
|
+
|
|
45
|
+
@final
|
|
46
|
+
class ColorScheme:
|
|
47
|
+
GuideLine: ClassVar[ColorScheme]
|
|
48
|
+
Sega: ClassVar[ColorScheme]
|
|
49
|
+
def __int__(self) -> int: ...
|
|
50
|
+
def __eq__(self, other: object, /) -> bool: ...
|
|
51
|
+
def __hash__(self) -> int: ...
|
|
52
|
+
|
|
53
|
+
@final
|
|
54
|
+
class ViewMode:
|
|
55
|
+
Normal: ClassVar[ViewMode]
|
|
56
|
+
Mini: ClassVar[ViewMode]
|
|
57
|
+
List: ClassVar[ViewMode]
|
|
58
|
+
MiniList: ClassVar[ViewMode]
|
|
59
|
+
def __int__(self) -> int: ...
|
|
60
|
+
def __eq__(self, other: object, /) -> bool: ...
|
|
61
|
+
def __hash__(self) -> int: ...
|
|
62
|
+
|
|
63
|
+
@final
|
|
64
|
+
class Location:
|
|
65
|
+
"""Wire coordinate: x=0..9, y=0..23, with (0, 0) at the top-left."""
|
|
66
|
+
@property
|
|
67
|
+
def x(self) -> int: ...
|
|
68
|
+
@property
|
|
69
|
+
def y(self) -> int: ...
|
|
70
|
+
def __eq__(self, other: object, /) -> bool: ...
|
|
71
|
+
def __hash__(self) -> int: ...
|
|
72
|
+
|
|
73
|
+
@final
|
|
74
|
+
class Flag:
|
|
75
|
+
@property
|
|
76
|
+
def mino(self) -> Block: ...
|
|
77
|
+
@property
|
|
78
|
+
def face(self) -> Facing | None: ...
|
|
79
|
+
@property
|
|
80
|
+
def location(self) -> Location | None: ...
|
|
81
|
+
@property
|
|
82
|
+
def up(self) -> bool: ...
|
|
83
|
+
@property
|
|
84
|
+
def mirror(self) -> bool: ...
|
|
85
|
+
@property
|
|
86
|
+
def color_scheme(self) -> ColorScheme: ...
|
|
87
|
+
@property
|
|
88
|
+
def note(self) -> bool: ...
|
|
89
|
+
@property
|
|
90
|
+
def lock(self) -> bool: ...
|
|
91
|
+
def positions(self) -> list[tuple[int, int]] | None:
|
|
92
|
+
"""The four occupied cells in top-down coordinates, or None for no mino."""
|
|
93
|
+
def __eq__(self, other: object, /) -> bool: ...
|
|
94
|
+
|
|
95
|
+
@final
|
|
96
|
+
class Page:
|
|
97
|
+
@property
|
|
98
|
+
def flag(self) -> Flag: ...
|
|
99
|
+
@property
|
|
100
|
+
def notes(self) -> str:
|
|
101
|
+
"""The resolved comment, including inheritance and quiz progression."""
|
|
102
|
+
def to_matrix(self) -> list[list[int]]:
|
|
103
|
+
"""Copy the 24x10 field into block codes 0..8, with garbage at row 23."""
|
|
104
|
+
def __eq__(self, other: object, /) -> bool: ...
|
|
105
|
+
|
|
106
|
+
@final
|
|
107
|
+
class Fumen:
|
|
108
|
+
def __len__(self) -> int: ...
|
|
109
|
+
def __iter__(self) -> Iterator[Page]: ...
|
|
110
|
+
def __getitem__(self, index: SupportsIndex, /) -> Page:
|
|
111
|
+
"""Copy one page; negative indices count from the end."""
|
|
112
|
+
def to_matrices(self) -> list[list[list[int]]]:
|
|
113
|
+
"""Copy all fields into page/row/column integer lists, without per-cell calls."""
|
|
114
|
+
@property
|
|
115
|
+
def mode(self) -> ViewMode: ...
|
|
116
|
+
@property
|
|
117
|
+
def version(self) -> int: ...
|
|
118
|
+
def __eq__(self, other: object, /) -> bool: ...
|
|
119
|
+
|
|
120
|
+
def parse(input: str) -> Fumen:
|
|
121
|
+
"""Decode a v115 string or viewer URL; invalid input raises DecodeError."""
|
rumen-0.1.0/src/block.rs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#[cfg_attr(feature = "python", pyo3::pyclass(frozen, eq, hash, module = "rumen"))]
|
|
2
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
3
|
+
#[repr(u8)]
|
|
4
|
+
pub enum Block {
|
|
5
|
+
Empty,
|
|
6
|
+
I,
|
|
7
|
+
L,
|
|
8
|
+
O,
|
|
9
|
+
Z,
|
|
10
|
+
T,
|
|
11
|
+
J,
|
|
12
|
+
S,
|
|
13
|
+
Garbage,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl TryFrom<u8> for Block {
|
|
17
|
+
type Error = &'static str;
|
|
18
|
+
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
|
19
|
+
Ok(match value {
|
|
20
|
+
0 => Block::Empty,
|
|
21
|
+
1 => Block::I,
|
|
22
|
+
2 => Block::L,
|
|
23
|
+
3 => Block::O,
|
|
24
|
+
4 => Block::Z,
|
|
25
|
+
5 => Block::T,
|
|
26
|
+
6 => Block::J,
|
|
27
|
+
7 => Block::S,
|
|
28
|
+
8 => Block::Garbage,
|
|
29
|
+
_ => return Err("Invalid Block"),
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const DECODE: [u8; 128] = {
|
|
2
|
+
let alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
3
|
+
let mut table = [u8::MAX; 128];
|
|
4
|
+
let mut digit = 0;
|
|
5
|
+
while digit < alphabet.len() {
|
|
6
|
+
table[alphabet[digit] as usize] = digit as u8;
|
|
7
|
+
digit += 1;
|
|
8
|
+
}
|
|
9
|
+
table
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub(crate) fn decode(ch: char) -> Result<u8, &'static str> {
|
|
13
|
+
DECODE
|
|
14
|
+
.get(ch as usize)
|
|
15
|
+
.copied()
|
|
16
|
+
.filter(|&digit| digit < 64)
|
|
17
|
+
.ok_or("Invalid base64 char")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub(crate) fn little_endian_value(data: &[u8]) -> u32 {
|
|
21
|
+
data.iter()
|
|
22
|
+
.enumerate()
|
|
23
|
+
.map(|(i, &n)| n as u32 * 64_u32.pow(i as u32))
|
|
24
|
+
.sum()
|
|
25
|
+
}
|