Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.85.0"
toolchain: "1.88.0"
- run: cargo check --all-targets
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "ournotes-deck"
version = "0.0.1"
edition = "2024"
rust-version = "1.85"
rust-version = "1.88"
description = "Deck power, skip score and live score for BanG Dream! Our Notes, with an exact Top-K deck search"
license = "MIT OR Apache-2.0"
repository = "https://github.com/empty-sekai/ournotes-deck"
Expand Down
93 changes: 83 additions & 10 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Deck power, skip score and live score for BanG Dream! Our Notes, and an exact To

## Correctness

Correctness has three layers, and each shows something different.
The checks below establish distinct contracts, each with its own scope and source identity.

**Agreement with the game.** Every calculation follows the game client's code function by function. Each unit is
then checked against the client's own implementation: the client's arm64 native functions run in an emulator on the
Expand Down Expand Up @@ -58,9 +58,11 @@ A search that reaches its time limit returns `TimedOut`, with legal and exactly
ranking claim. Inputs outside the proven range, unknown cards, rules the game would reject and parts of the game that
are not modelled are reported as errors.

**Not yet verified.** How the units combine, frame by frame, into a whole live has not been compared with the game as
a whole. That needs a recording of a play on a device: the random seed, frame times and each note's judgement.
With Gekisou on the score depends on the random seed; the search does not offer a Gekisou objective yet.
**Native update chains.** Offline Unicorn ARM64 executes real client scoring, skill and ranking chains, compared frame by frame at matching inputs and phases. Nine high/medium/low-accuracy × 30/60/120 fps cases have 57,750 frames and 18,826,500 core checks with zero differences. Seven long-chart cases separately have 70,747 frames and 19,971,785 checks with zero differences. Card Gekisou, probability triggers, life conditions and dynamic windows have their own contracts; see the [native validation table](docs/native-validation.en.md). Field counts and reused captures are not additional independent samples.

**Shared runtime.** Per-note replay uses the same Rust model. Across 340 charts × 2 judgement plans × 2 modes, 1,360 Rust / WASM runs match; see the [runtime identity](docs/validation/replay-2026-09-30.json). The checked scoring-model snapshot passes 272 release tests, clippy with denied warnings, fmt and Rust 1.88 checks; see the [production record](docs/validation/production-checks-2026-09-30.json).

**Applicability.** Results are limited to declared resources, inputs, fields and phases. A raw Touch sample using private resource adapters does not release complete physical-touch support; full device and server lifecycles require separate evidence. A per-play result uses explicit judgements, frame order, seed and ranking policy; statistical baselines and weights do not replace it.

## Data

Expand Down Expand Up @@ -131,18 +133,23 @@ Constraints: `--leader ID`, `--include ID,...`, `--exclude ID,...`, `--exclude-s
Chart statistics:

```sh
ournotes-deck chart-stats --data deck-data.json [--seeds 8] -o chart-stats.json
ournotes-deck chart-stats --data deck-data.json [--seeds 8] [--charts ID,...] [--jobs N] -o chart-stats.json
```

measures every chart's deck-independent numbers on the whole-live simulation (`ournotes-deck.chart-stats/2`) in
two scenarios: Gekisou on (`seeds`, as a Battle Live plays) and Gekisou off (`offSeeds`, as a solo live such as Free
Live or Challenge Live plays).
Live or Challenge Live plays). `--charts` measures the listed score ids only (in the file's order; ids the file does
not have are ignored); `--jobs N` measures N charts at once and writes the same document as one at a time. Without
`-o` the document goes to the standard output.

With Gekisou on, the play is the theoretical best play with Gekisou: every note judged at its time, Just inside the
Just-count ranges and Perfect elsewhere, rank 1 in every range. Per seed: the exact no-skill score and the Gekisou
ranges' results, and for every score-up kind of the master (2000 / 2002 / 2004 / 2005 rows grouped by type,
duration, targets and conditions, see `kinds`) at every performance position the score gained at factor 1 per unit
of deck power (`weights[kind][k]`). A deck scores about `P × (score / power + Σ factor_k × weights[kind_k][k])`;
Just-count ranges and Perfect elsewhere, rank 1 in every range. Per seed: the exact no-skill score; the Gekisou
ranges' results (`ranges`: the range score, the rank 1 bonus, the largest Gekisou combo `maxCombo`, the Just count
`justCount`, the luck points `luckPoints` and the lottery results `lotResults`; the combo, Just and luck missions
rank by `maxCombo`, `justCount` and `luckPoints`, and without skills the luck points come from the lottery of the
luck ranges alone, 0 elsewhere); and for every score-up kind of the master (2000 / 2002 / 2004 / 2005 rows grouped by
type, duration, targets and conditions, see `kinds`) at every performance position the score gained at factor 1 per
unit of deck power (`weights[kind][k]`). A deck scores about `P × (score / power + Σ factor_k × weights[kind_k][k])`;
every seed checks this on a random deck of the master's own values at another power and fails beyond the flooring
bound. Charts with a luck range are given on the first N published seeds (`--seeds`, default 8), which is not a
native expectation; a chart with more than three fevers, where the game fails when the fourth starts, is
Expand All @@ -162,6 +169,72 @@ With Gekisou off, the play is the theoretical best play (every note Perfect at i
Gekisou combo or rank bonus; `score`, `weights` and a check as above. A kind whose conditions read the Gekisou state
cannot play without Gekisou and has null weights.


### Gekisou skill aptitude

Statistics also include each skill's aptitude for a chart, without selecting a best formation or changing `seeds`
or `offSeeds`. The file-level `gekisouAptitude` holds shapes and measurement rules; each chart's
`charts[].gekisouAptitude` holds range `factors` and the `variants` of its missions. It is null for a chart with no
Gekisou range, one unplayable with Gekisou, or a master without measurable skills. These are additional fields;
the format remains `ournotes-deck.chart-stats/2`.

```sh
ournotes-deck chart-stats --data deck-data.json --aptitude-max-seeds 128 --aptitude-cross-seeds 32 -o stats.json
ournotes-deck chart-stats --data deck-data.json --no-gekisou-aptitude -o baseline.json
```

- `--aptitude-max-seeds N`: at most N seeds for a random increment, default 65536, N at least 2; stop earlier when both grade endpoints meet their SE targets.
- `--aptitude-cross-seeds N`: at most the first N seeds for ordinary skill cross terms, default 64, N at least 1.
- `--no-gekisou-aptitude`: skip aptitude measurement; both file-level and per-chart `gekisouAptitude` are null.
Existing statistics are still produced.

Shapes deduplicate by source, mission and effect parameters. Member skills use their highest level; support skills
use the level at the snap's highest rank, not necessarily the highest level in the effect table. Support skills
that differ only in their band targets share a shape, preserving `skills[].memberTargetIds` / `bandIds`, and are
measured both with `bandMatch: true` and `false`. Each support skill has a **synthetic, effect-free member Gekisou
skill** of its own mission as host, never a real card whose effects could contaminate the increment. Each run plays
one member or support skill alone through the whole-live engine.

`score`, `scorePerfect`, `tail` and range increments are `[mean, standard error of the mean]`: with-skill minus
without-skill on the same seed, at `model.power`. `tail = Δscore − Σ(ΔrangeScore + ΔrankBonus)` includes gains outside
the range score frames, such as effects persisting after the range ends. `factors` lists range note counts,
entering combos and baseline lottery counts. `weights` gives changes in the plain ordinary kind's weight at each
position; `rangeWeights` gives the corresponding range changes, not full formation weights. Both are null without
a plain kind. Each variant's `check` uses its first measured seed, random ranks and an ordinary-skill deck at
another power to validate the linear prediction; exceeding its flooring bound fails the measurement.

Random increments start at 32 seeds and double along the same seed prefix until the configured cap. Preset batches
extend to 65536; a cap outside those boundaries is included as the final batch.
`score` and `scorePerfect` each use their own increment mean and no-skill baseline. Both SEs must be at most
`max(1% × |mean increment|, 0.1% × mean no-skill score)` before stopping. At the cap, `seTargetMet` reports whether
both targets were met. Deterministic increments report one seed and zero SE; four identical samples alone cannot establish that
a random skill is deterministic. The seed mean is not the game's expectation: its seed law is unknown, and SE
does not measure model error. Cross terms can use fewer seeds, reported as `crossSeeds` per variant (0 without a
plain kind). Meeting the SE target may only satisfy the absolute baseline threshold, not 1% relative precision
on the increment; a small sample mean's sign alone does not establish that a skill helps or hurts.

**Model limits:**

- Only `battleLiveScore` changes, not the separately reported `soloScore`; Free Live has no aptitude gain.
- **Do not add increments of multiple skills.** Gekisou combo saturation, luck gauge/rush interactions and
Just-count-dependent support triggers can all break additivity.
- The theoretical best play has no Great or Miss: combo protection 12004, Great-to-Perfect 12006 and judgement
window extension 4004 have zero effect here. Just-count effects 13000/13002 and luck-point effect 11002 can change
range indicators without increasing score. Shapes of other missions are gated off and omitted on this chart.
- Ordinary-skill factors and rank changes use the linear formula, with rank rounding differences checked by
`check`. Below 100% Just, interpolation of the no-ordinary-skill increment between Just and Perfect plays is
approximate: conversion 13005, per-Just support 2001 and Just-count effect 13002 are nonlinear. Perfect-play
cross weights are not measured, so full aptitude with nonzero ordinary skills is unavailable below 100% Just.
Scaling by `1 − 0.2q` for Great proportion q is also approximate.

These interpolation and scaling limits concern statistical summaries. A declared per-note play uses the shared
`replay` API with its actual frame order, skills and seed; see the [model contract](docs/native-validation.en.md#shared-model-contract).

Library callers can use `chart_stats_with` / `document_with` with
`Options { seeds, aptitude: Some(AptitudeOptions { max_seeds, cross_seeds }) }`; `aptitude: None` disables it.
A `DeckData` without charts still produces the shape header, also available through
`aptitude_header(master, kinds, options)`.

## Tests

`cargo test` runs the unit tests, reads synthetic deck data files and compares the search with exhaustive
Expand Down
Loading
Loading