English | 简体中文
Project Sekai 组卡推荐引擎的 Rust 实现,专攻 DFS / 分支限界(B&B)精确搜索。
给定玩家卡组、活动加成与目标(综合力 / 技能 / 活动点数 / MySekai 等),在巨大的组合空间里搜出最优的 5 张卡编成。核心数据结构用 SoA(结构体数组)+ 位运算组织,配合角色感知的后缀上界与支配剪枝。耗时取决于卡池、规则和 Top-K,测量范围见下文。
部分游戏内数值与逻辑(综合力、技能加成、活动点数、支援卡组、WL3 模拟终章等)来自下列开源实现,源码注释保留对照出处:
- https://github.com/Team-Haruki/sekai-deck-recommend-cpp
- https://github.com/StarMoe-org/sekai-deck-recommend-cpp
具体移植与修正内容见各 commit 说明。
在此基础上,本实现并非逐行翻译,而是对底层热路径与搜索剪枝做了彻底的 Rust 重构,核心数据结构全部按 cache line 对齐:
- 建池(pool building):对 masterdata 一次性建立 by-id 索引,把逐卡 O(N) 的线性扫描降为 O(1) 查表。
- 搜索(search):SoA 卡池 + 小池使用的 512-bit 元数据位图、综合力压成 u18×8 槽位 + 查找表、逐角色支配裁剪、角色感知后缀上界、贪心 + 1-swap warm start 下界,使分支限界尽早剪枝。
CardPool 采用列式 SoA 布局,每列 64 字节对齐。典型候选池(130–260 张卡)整体 ~7–12 KB,加上 SearchContext、SuffixBound 等搜索期结构,热路径数据适合驻留在现代服务器 CPU 的 L1 data cache 内(EPYC 9K85 每核 L1d = 48 KiB)。叶子评估遍历卡组时按列顺序访问,尽量减少无关 cache line 和 TLB 压力。
耗时取决于候选池大小、活动规则、Top-K 和硬件。请在 release profile 下分别记录建池与搜索耗时,同时报告完成状态;超时结果不代表完整 Top-K。
验证指南 提供独立小池穷举、完整结果对拍和可复现的性能测量方法。
主入口是 engine::recommend_json——纯 JSON 进、JSON 出:
use allium_deck::engine::recommend_json;
let result_json = recommend_json(
masterdata_json, // 游戏 masterdata
music_metas_json, // 歌曲元数据
user_data_json, // 玩家卡组(camelCase)
params_json, // 组卡参数(target / event / card_configs 等)
)?;内部走两阶段:handler::build_card_pool(建池)→ search::search(搜索)。结构体入口 engine::recommend 可绕过 JSON 序列化。
返回 {"decks": [...], "completion": "complete", "stats": {...}}。cards 是游戏卡 ID,按站位顺序、队长在前;score 是搜索排序键。综合力、live 分数等面板明细不在其中,需要时用 handler::build_card_pool + search::summarize_deck(src/bin/recommend_cli.rs 是完整示例)。
完整参数与模式说明见 docs/parameters.md。搜索完整结束时,各受支持模式都返回精确 Top-K;超时会显式返回 timed_out,不会伪装成完整结果。整体正确性说明见 docs/exactness-proof.md,每一种剪枝为什么不会漏解的形式化证明见 docs/pruning-proof.md。
| 模块 | 职责 |
|---|---|
engine |
对外入口(recommend_json / recommend)、masterdata 加载(OwnedGameData)、JSON 参数解析 |
types |
公共标识符与枚举(Unit / Attr / LiveType / ScoreTarget),以及逐卡解析后的综合力与技能数值 |
handler |
建池层:候选裁剪、综合力/技能/活动加成预计算、WL 支援卡组、构建搜索上下文 |
pool |
SoA 卡池:列式存储、位图、对齐布局、冻结后只读 |
search |
搜索层:支配剪枝、后缀上界、warm start、按目标/场景分派 B&B / DP / 专用求解器、叶子精确评估 |
auxiliary |
非搜索路径的辅助计算:区域道具推荐、曲目推荐、精确打歌分(wasm 直接导出) |
masterdata JSON ─┐
├─→ OwnedGameData::load // 一次性加载,可缓存
music_metas ─────┘ │ as_ref()
▼
GameData (只读借用视图)
user JSON ──→ parse_user_profile_json ──→ UserProfile
params JSON ─→ parse_build_params_json ──→ BuildParams
│
▼
build_card_pool (handler) // 建池
├─ 每张用户卡:综合力 / 技能 / 活动加成预计算
├─ 硬约束过滤与安全预处理(不做质量前缀截断)
├─ 排序灌入 SoA CardPool
└─ 构建 SearchContext(含 WL 支援卡组)
│
▼ (CardPool, SearchContext)
search (search) // 搜索
├─ 逐角色支配裁剪
├─ 角色感知后缀上界(B&B 剪枝核心)
├─ warm start 下界(贪心 + 1-swap)
└─ 按 target / 场景分派:
Score / MySekai B&B、Power DP / B&B、Skill B&B、Challenge、终章
│
▼
Vec<DeckResult> → JSON
原生核心只依赖 serde / serde_json / thiserror;wasm32 额外使用 web-time 提供单调时钟。没有图形、异步或系统库依赖,可独立编译:
cargo build --release性能数字应在 release profile 下测量。src/bin/recommend_cli.rs 提供 standalone CLI(cargo install allium-deck 出来后命令名 recommend_cli),打印分阶段耗时(建池 vs 搜索),方便快速迭代验证。
| 语言 | 位置 | 说明 |
|---|---|---|
| Rust | 本仓库(crates.io allium-deck) |
引擎本体 |
| JavaScript / 浏览器 | wasm/(npm @empty-sekai/allium-deck-wasm) |
WASM 绑定,见下方导出表 |
| Python | allium-deck-python(PyPI allium-sekai-deck) |
预编译 abi3 wheel,含 allium_deck API 与 LunaBot 兼容门面,无需本地 Rust 工具链 |
外置 masterdata 模式(推荐,浏览器侧复用已有的 masterdata JSON):
| 导出 | 说明 |
|---|---|
load_masterdata(map, metas) |
一次扁平化并缓存;辅助表(areas/areaItems/shopItems/ingameNotes/ingameCombos)可选 |
recommend(user, params) |
字符串入 / JSON 字符串出 |
createUserData(user, region) + recommendWithUserData(options, handle) |
解析一次用户数据多次复用(region 词表 jp/tw/en/kr/cn) |
recommend_area_items(options) |
固定卡组的区域道具升级建议 |
recommendMusic(options) |
已定卡组的全曲目/难度打分排序 |
calculate_exact_live(options) |
逐 note 精确打歌分 |
get_world_bloom_support_cards(options) |
WL 支援卡逐卡加成(按 bonus 降序、card_id 升序) |
recommendBatch 系列暂未提供。options 键名为 snake_case(兼容 camelCase 别名),
输出键名为 snake_case。recommend_embedded 仍保留在 embedded feature 下。
recommend_cli 是可独立运行的组卡推荐命令行工具,可从 GitHub Releases 下载预编译二进制,或从源码安装。
从命令行跑一次完整推荐,打印建池/搜索耗时和 Top-K 卡组:
# 方式1: 下载预编译二进制 (以 linux-x86_64 为例)
curl -L -o recommend_cli \
https://github.com/empty-sekai/allium-deck/releases/download/v0.0.15/recommend_cli-v0.0.15-linux-x86_64
chmod +x recommend_cli
./recommend_cli [OPTIONS]
# 方式2: 从 git 安装 (无需 clone)
cargo install --git https://github.com/empty-sekai/allium-deck --bin recommend_cli
recommend_cli [OPTIONS]
# 方式3: Clone 后本地编译
git clone https://github.com/empty-sekai/allium-deck.git
cd allium-deck
cargo build --release --bin recommend_cli
./target/release/recommend_cli [OPTIONS]使用方法:
recommend_cli \
--masterdata <masterdata-dir> \
--music-metas <music_metas.json> \
--user <user.json> \
--target score \
--live-type multi \
--event-id 170 \
--music-id 74 \
--music-diff expert \
--boost 10 \
--event-unit ln \
--event-attr cool \
--unit-filter ln \
--multi-teammate-power 250000 \
--multi-teammate-score-up 200 \
--top-k 5参数:
| 参数 | 类型 | 说明 |
|---|---|---|
--masterdata |
目录 | 游戏 masterdata 目录,内含 cards.json、events.json、skills.json、cardRarities.json、gameCharacterUnits.json 等文件。 |
--music-metas |
文件 | 歌曲元数据 JSON 文件。 |
--user |
文件 | 玩家数据 JSON,至少包含 userCards;区域道具、角色等级、称号、MySekai 等字段会参与评分。 |
--params |
文件 | 兼容入口:读取推荐参数 JSON;直接 flags 会覆盖同名 JSON 字段。 |
--target |
枚举 | score / power / skill / mysekai。 |
--live-type |
枚举 | solo / multi / cheerful / auto / challenge / challenge_auto / mysekai。 |
--event-id / --music-id / --music-diff |
值 | 活动、歌曲和难度;难度为 easy / normal / hard / expert / master / append。 |
--boost |
整数 | 火数 0..10:0 为无火,1..5 为 5/10/15/20/25x,6..10 为 27/29/31/33/35x。 |
--fixed-cards / --fixed-characters / --excluded-cards |
列表 | 逗号分隔的卡 ID / 角色 ID 约束。 |
--event-unit / --event-attr |
枚举 | 模拟活动团和属性;团可用 ln/mmj/vbs/wxs/25ji/vs,属性可用 cool/cute/happy/pure/mysterious。 |
--unit-filter / --attr-filter |
枚举 | 硬过滤候选池;VS 双团卡按 support_unit 参与对应团过滤。 |
--world-bloom-character-id / --world-bloom-event-turn / --challenge-live-character-id |
值 | WL / Challenge Live 特殊参数。 |
--mode area-items / --mode music / --mode exact-live |
模式 | 辅助计算(不组卡):area-items 需 --card-ids;music 需 --deck;exact-live 需 --power/--skills/--music-score。 |
--skill-reference-strategy / --live-skill-order / --specific-skill-order |
值 | 技能参考与发动顺序;指定顺序使用 0,1,2,3,4。 |
--multi-teammate-power / --multi-teammate-score-up / --multi-live-score-up-lower-bound |
值 | 协力和 Cheerful 队友综合力、技能实效、技能总下限。 |
--other-score / --life |
值 | Cheerful 对手分数和体力。 |
--rarity4-config / --single-card-config |
值 | 养成配置,如 level_max,skill_max,master_max,episode_read,canvas 和 123:level_max,skill_max。 |
输出示例:
stderr 只输出进度和耗时,stdout 固定输出结构化 JSON,便于回归和性能对比:
[load] masterdata+music_metas: 135.0ms
[build_pool] 1.4ms pool=78 effective_live=Multi
[search] 0.4ms leaf=84 ub_prunes=278 ep_explored=18 mono_break=12
[total] 136.8ms
{
"completion": "complete",
"timed_out": false,
"effective_params": { "target": "Score", "live_type": "Multi", "boost": 10 },
"diagnostics": { "pool_size": 78, "effective_live_type": "Multi" },
"timing": { "build_pool_ms": 1.4, "search_ms": 0.4 },
"decks": [
{
"rank": 1,
"event_point": 1234567,
"cards": [
{ "card_id": 111, "power_total": 35210, "skill_score_up": 120.0, "has_canvas_bonus": true, "canvas_power": 600 }
]
}
]
}server/ 是引擎的 HTTP 服务,独立 crate,不发布到 crates.io。masterdata 常驻内存,
搜索跑在固定数量的专用线程上,队列有界——满了直接回 503 而不是把请求堆进无界积压。
这是一个直接的实现:它让引擎可以通过 HTTP 用起来,并带上共享服务需要的那几道护栏,
但不是调优过的架构,也不是完整的部署方案。没有 TLS、没有鉴权与配额、没有缓存、
没有请求取消、不跨实例协调,单次搜索也不并行。完整的边界清单见
server/README.md。
# 本仓不携带游戏数据;先导出一份合成 masterdata 就能把服务跑起来
cargo run --manifest-path server/Cargo.toml --release --bin export-synth-masterdata -- ./synth
cd server
cargo run --release -- \
--masterdata synth=../synth/masterdata \
--music-metas synth=../synth/music_metas.jsoncurl localhost:8080/v1/recommend -H 'content-type: application/json' -d "{
\"user\": $(cat ../synth/user.json),
\"params\": {\"liveType\": \"multi\", \"target\": \"score\", \"eventId\": 1, \"limit\": 5}
}"params 就是引擎自己的参数契约(见 docs/parameters.md),服务不另造一套方言。
| 方法 | 路径 | 用途 |
|---|---|---|
| POST | /v1/recommend |
组卡;覆盖全部 target 与 live type,含 WL 章节与终章 |
| POST | /v1/recommend/challenge-all |
26 个角色各自的最优挑战卡组,带排名 |
| POST | /v1/world-bloom/support-cards |
WL 章节的逐卡支援加成 |
| POST | /v1/music/recommend |
对已定卡组给全部曲目/难度打分排序 |
| POST | /v1/live/exact-score |
按谱面逐 note 计算打歌分 |
| POST | /v1/area-items/recommend |
区域道具升级的性价比排序 |
| GET | /v1/regions /healthz /readyz /metrics /openapi.json |
区服清单与运维端点 |
每个响应的 timing 会给出 queue / 建池 / 搜索的分段耗时,/metrics 里是同一组分段的
直方图——决定 --workers 和 --max-queue 该设多少时看这个。
# 构建上下文是仓库根目录(服务按 path 依赖引擎 crate)
docker build -f server/Dockerfile -t allium-deck-server .
docker run --rm -p 8080:8080 -v /path/to/data:/data:ro allium-deck-server --masterdata cn=/data/masterdata --music-metas cn=/data/music_metas.json完整配置项、背压与超时语义、错误码见 server/README.md;
镜像的 libc / 分配器 / 运行时基底组合与实测见 docker/README.md。
data/ 内嵌 3 张世界开花(World Bloom)支援卡组加成表。这些表在参考实现中作为仓库静态资源随包携带、不随 masterdata 更新,因此这里用 include_str! 内嵌,masterdata 缺失对应文件时回退使用。
这里的“精确”指的是:只要搜索以 Complete 结束,返回的就是完整可行集合按统一排序规则得到的真正 Top-K,不是依赖随机种子或经验阈值的近似答案。Score、活动分、MySekai、World Bloom、终章、Challenge、Power、Skill 和精确加成档位都遵守这条规则。
实现里仍然有 warm start、beam、one-swap 等启发式,但它们只用于更早找到好解、提高分支限界阈值或调整访问顺序;不会拿来删除尚未被数学上界否定的候选。无约束 Power 使用按共同组合与共同属性划分场景的精确分支限界,其余 Power / Skill 走完整候选集上的有界搜索。
搜索有显式 deadline。命中 deadline 时返回 TimedOut,已经找到的卡组仍是合法且精确评分的,但这时不声称 Top-K 已证明完整。512-bit 元数据位图保持固定宽度,大池仍在 SoA 列中保留所有卡;候选超过稠密 CardIdx 的 65,535 张容量,或紧凑元数据无法无损编码时,才返回容量错误,不会静默删卡。
形式化正确性分成两份文档:
- Exactness proof:定义可行集合、统一 Top-K 排序、各 solver 的完整性与 timeout / capacity 边界。
- Pruning proof:逐项证明每一种会真正删除搜索空间的规则,并给出对应代码位置;只负责排序或提供初始解的启发式会被明确排除在“剪枝”之外。
验证层用于找反例和防回归,而不是代替证明。目前固定门禁包括 256 × 15 = 3840 条跨场景独立对照、针对历史反例和边界条件的专项测试,以及 native / server / Rust 1.89 / WASM / Node / Chrome 的跨运行时一致性检查。
MIT OR Apache-2.0。