Skip to content

Repository files navigation

ournotes-deck

BanG Dream! Our Notes 的综合力、跳过分数与演出分数计算,以及精确的 Top-K 组卡搜索。

English

计算内容

  • 综合力:逐槽位各项(等级、突破次数与卡面等级带来的卡面属性;角色等级与总角色等级;乐队道具;歌曲属性与标签加成; snap 的加成与属性连携;队长技能;VIP;回忆;活动参数)与编成总值,按游戏自身的整数与 binary32 浮点运算,包括其舍入与取整方式。
  • 跳过分数:某张谱面跳过演出时的分数。
  • 演出分数:单个音符分数、帧、连击加成表、倍率指令,以及逐帧的整场演出(live::full):判定转换、连击、血量(回复、护盾、 归零)、分数计算器(包括迟到判定引起的回退重算),以及演出技能与 snap 技能的条件和效果。撃奏(区间状态、撃奏连击与 Just 计数、幸运抽签、排名加成,以及撃奏技能与撃奏 snap 技能)也已建模;搜索目前只支持撃奏关闭。
  • 活动点数:活动加成、评级、加成道具倍率,以及游戏客户端计算的活动点数。实际发放量由游戏服务器决定;本库复现的是客户端自身的计算。
  • 搜索:综合力(可带或不带歌曲、可带或不带活动参数)、跳过分数与演出分数的前 K 个编成,每组 5 张成员卡只保留一个结果。 演出分数是判定序列下含演出技能与 snap 技能的整场模拟(撃奏关闭);排除 snap 技能时,则是逐音符打法下只含演出技能的分数。 含 snap 技能时搜索要模拟候选编成,因此较慢;漏判或迟判较多的判定序列可能慢得多,可用时间上限约束这类请求。

正确性

正确性分三层,各层证明的内容不同。

与游戏一致。 各项计算按游戏客户端的代码逐函数实现,并逐个计算单元与客户端自身的实现对照:在模拟器中以相同输入执行 客户端的 arm64 原生函数,按位比较结果(浮点按位比较,整数溢出与抛出异常的路径也一并比较)。对照覆盖:

  • 综合力的数值原语与槽位计算;
  • 音符分数与帧、连击加成;
  • 倍率指令与分数计算器的回退重算;
  • 演出技能与 snap 技能的条件和效果;
  • 血量、连击与判定;
  • 随机数;
  • 评级与活动点数。

生成输入共数百万组,包括真实 master 行,差异为 0。对照时还运行了故意改错的版本,以确认比较确实能发现差异。编成五槽求和与 各项加成的构建是纯整数代码,没有单独执行,按代码逐函数移植。本库的实现再与这套经过对照的模型比较,差异同样为 0,其中整场 逐帧演出在撃奏关闭与开启时各比较了 5 万多个场景,包括 1,048 与 1,267 首真实谱面整首。

搜索精确。 Complete 的搜索结果恰为全部合法编成上的规范 Top-K。剪枝只使用在游戏运算下已证明可采纳的上界(证明见 docs/search.md)。搜索结果与独立的穷举实现逐项比较,穷举不共用任何上界、分解或 Top-K 代码:在真实卡牌与谱面上 比较了 25,600 组请求、共约 4.5 亿个编成,差异为 0。 含 snap 技能的演出分数,穷举会模拟每一组成员、队长、snap 配置与演出顺序:在真实卡牌与谱面上(默认与随机判定序列)比较了 1,280 组请求、共模拟约 220 万个编成与顺序,在 snap 技能会改变排名的合成卡池上比较了 3,200 组请求、共约 1.09 亿个,差异均为 0。 达到时间上限的搜索返回 TimedOut,其中的编成合法且数值精确,但不保证排名。 超出已证明范围的输入、未知卡牌、游戏会拒绝的规则以及尚未建模的部分,都以错误返回。

尚未验证的部分。 整场演出如何由各单元按帧拼成,没有与游戏整体对照:这需要一份真机录制的对局(随机种子、帧时间与逐音符 判定)。撃奏开启时分数取决于随机种子;搜索尚未提供撃奏目标。

数据

本库不含任何游戏数据。它读取一份 deck data 文件(nnnotes.deck-data/1,由 nnnotes deck-data 命令生成,含同一版本 master 数据的相关表与全部谱面),以及用户的卡牌持有情况:

{
  "player": { "characterRanks": { "1": 20 }, "bandItems": { "101": 10 }, "vipRank": 3, "events": [] },
  "members": [ { "id": 1, "level": 60, "awake": 3, "rank": 2, "liveSkillLevel": 3, "gekisouSkillLevel": 1 } ],
  "snaps": [ { "id": 1, "level": 20, "rank": 1 } ]
}

level 可以换成 exp。谱面按 score id(MasterLiveMusicScore._id)选择。

含 snap 技能的演出分数按判定序列模拟: {"frames": [0, 16, 33], "judged": [[frame, noteId, judgement, judgementTimeMs]], "baseSeed": 0, "assist": false}。 frames 是每一帧的乐曲时间(毫秒,不减);judged 的每一行表示某个音符在第 frames[frame] 帧判定,判定为转换前的判定 (1 Miss、2 Bad、3 Good、4 Great、5 Perfect、6 Just);血量、连击与技能由模拟得出。默认判定序列为理论最佳:以 60 fps 取帧(floor(i * 1000 / 60) 毫秒),直到最后一个判定音符或技能事件之后 2000 毫秒;每个判定音符在第一个到达其谱面时间的帧 判为 Perfect,判定时间取谱面时间(撃奏关闭时游戏不判 Just)。

排除 snap 技能的演出分数默认使用逐音符形式的理论最佳打法:每个判定音符都是 Perfect,全连且不掉血。也可以另给一份打法 {"notes": [{"noteId", "timeMs", "noteType", "scoreType", "life", "combo"}], "lifeAtEvent": [...], "assist"} (判定类型:1 Just、2 Perfect、3 Great、4 Good、5 Bad、6 Miss)。

作为库使用

use ournotes_deck::cards::Roster;
use ournotes_deck::data::DeckData;
use ournotes_deck::search::{Constraints, Objective, Pool, SearchRequest, search};

let data = DeckData::from_path("deck-data.json").unwrap();
let roster = Roster::from_json(&std::fs::read_to_string("box.json").unwrap()).unwrap();
let pool = Pool::new(&data.master, &roster).unwrap();
let out = search(&pool, &SearchRequest {
    objective: Objective::SkipScore { score_id: 10000103, chart: data.chart(10000103).unwrap() },
    k: 10,
    constraints: Constraints::default(),
    time_limit: None,
})
.unwrap();
for deck in &out.results {
    println!("{:?} {} {:?} {:?}", deck.score, deck.power, deck.members, deck.snaps);
}

命令行

ournotes-deck power --data deck-data.json --roster box.json -k 10 [--music ID] [--event]
ournotes-deck skip  --data deck-data.json --roster box.json --score SCORE_ID -k 10
ournotes-deck live  --data deck-data.json --roster box.json --score SCORE_ID [--play stream.json] -k 10
ournotes-deck live  --data deck-data.json --roster box.json --score SCORE_ID --exclude-snap-skills [--play play.json] -k 10

live 默认计入 snap 技能,--play 为判定序列;加 --exclude-snap-skills 时只计演出技能,--play 为逐音符打法。

约束:--leader ID、--include ID,...、--exclude ID,...、--exclude-snaps ID,...、--no-snaps、 --time-limit-ms N。输出为 JSON。

谱面统计:

ournotes-deck chart-stats --data deck-data.json [--seeds 8] -o chart-stats.json

在整场模拟上实测每张谱面与卡组无关的量(ournotes-deck.chart-stats/2),分两种场景:激走开启(seeds,撃奏ライブ 的打法)与激走关闭(offSeeds,自由 Live、挑战 Live 等单人 Live 的打法)。

激走开启时打法为激走理论最佳:每个音符按准点判定,Just 任务区间内为 Just,其余为 Perfect,每个区间取名次 1。每个 种子给出:无技能的精确得分与各激走区间结果;master 中每种加分效果(2000 / 2002 / 2004 / 2005,按类型、时长、目标、 条件分组,见 kinds)在每个演出位上因子为 1 时的得分增量除以综合力(weights[kind][k])。卡组得分约为 P × (score / power + Σ factor_k × weights[kind_k][k]),每个种子都用 master 真实数值的随机卡组在另一综合力下实跑校验, 偏差超出取整上限即报错。有幸运区间的谱面按前 N 个发布种子给出(--seeds,默认 8),这不是原生期望;超过三段 fever 的谱面游戏会在第四段开始时出错,记为 unplayable(激走关闭时照常可玩)。

其他名次不重跑:名次加成为 trunc(区间得分 × 百分比 / 100),记在区间结束帧的固定分上,不改因子也不改音符得分,所以 区间 i 取名次 r_i 时无技能得分精确为 score − Σ rankBonus_i + Σ trunc(rangeScore_i × rankBonusPercents_i[r_i − 1] / 100), 权重为 weights[kind][k] + Σ (rankBonusPercents_i[r_i − 1] − rankBonusPercents_i[0]) / 100 × rangeWeights[kind][k][i] (rangeWeights 是该效果带来的区间得分增量除以综合力)。每个种子另用同一校验卡组在随机名次下走显式名次确认实跑校验 (rankCheck)。条件读取确认名次(7012)的效果种类没有 rangeWeights,名次加成可能落进另一区间得分帧的谱面整张没有。 每个种子还给出把 Just 全部改判 Perfect 的同一打法的无技能得分与各区间得分(scorePerfect、rangeScorePerfect)。

激走关闭时打法为理论最佳(每个音符准点 Perfect),种子 0,没有 Just、幸运、激走连击和名次加成;给出同形的 score、 weights 与校验。条件读取激走状态的效果种类在激走关闭时无法演出,其权重为 null。

测试

cargo test 运行单元测试,读取合成的 deck data 文件,并在小规模合成卡池上将搜索结果与穷举结果逐项比较。 可用 OURNOTES_DECK_ORACLE_CASES、OURNOTES_DECK_ORACLE_SEED0、OURNOTES_DECK_ORACLE_MEMBERS、 OURNOTES_DECK_ORACLE_SNAPS 扩大比较规模。含 snap 技能的演出分数可用 OURNOTES_DECK_SNAPS_CASES、 OURNOTES_DECK_SNAPS_SEED0、OURNOTES_DECK_SNAPS_MEMBERS、OURNOTES_DECK_SNAPS_SNAPS、OURNOTES_DECK_SNAPS_NOTES、 OURNOTES_DECK_SNAPS_VARIANTS 扩大比较规模。

许可证

MIT OR Apache-2.0。

About

Deck power, skip score and live score for BanG Dream! Our Notes, with an exact Top-K deck search

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages