プロジェクト

全般

プロフィール

機能 #145

完了

ノーツレーダー機能

機能 #145: ノーツレーダー機能

いづ さん さんが27日前に追加. 22日前に更新.

ステータス:
完了
優先度:
通常
担当者:
対象バージョン:
開始日:
2026/06/09
期日:
進捗率:

100%

予定工数:

説明

ノーツレーダー

某ゲームのノーツレーダーをそれっぽく再現したい

名称 概要 計算方法の検討メモ
NOTES 楽曲の長さに対する総ノーツ数の多さ 譜面全体の平均密度
PEAK ノーツ数の瞬間密度 数秒間の最大瞬間密度
SCRATCH 楽曲の長さに対するスクラッチの多さ 譜面全体の平均スクラッチ密度
CHARGE チャージノートやバックスピンスクラッチの難しさ チャージ単体、チャージ中の通常ノーツも考慮
CHORD 同時押しの難しさ そのまま同時押しの出現頻度?
SOF-LAN BPM変化に対するノーツの難しさ BPM変化点前後のノーツ数(変化直後から2秒間&時間距離で係数減衰)、BPM変化量


メモ

  • 各要素の最大値は 200.00 とする
  • BMSの場合、やろうと思えばいくらでも難しくできるので、どこかで基準を設ける必要がある
  • Overjoy Lv8とかを基準にしてしまうと、一般的なレベル帯があまりにも低くなりすぎる気がする
  • 実際にすべての値を計算した結果をベースにして係数を設定してレーダー値を決めるといいかも?

プレーヤーノーツレーダー

各要素のTOP10を集計、平均値でプレーヤー側のノーツレーダーを算出する。

単曲のレーダー値=対象曲のノーツレーダー値×スコアレートで計算する

レーダー計算する曲

  • 7KEYS譜面、かつ、難易度表に登録されている譜面のみ
  • あらかじめ曲のレーダー値は計算済みにしておく
  • ゲーム内では参照するのみ
  • プレーヤーノーツレーダーは対象がゲーム内でスコア更新されたら値も更新する

ファイル

clipboard-202606092147-bkfn5.png (56.4 KB) clipboard-202606092147-bkfn5.png いづ さん, 2026/06/09 21:48
clipboard-202606092147-bkfn5.png

関連するチケット 1 (0件未完了1件完了)

関連している beatoraja - 機能 #313: ノーツレーダー6軸の細分化データモデル設計(弱点分析 #293 の前提整備)完了いづ さん2026/06/19

操作

いづ さん さんが27日前に更新 操作 #1

いづ さん さんが27日前に更新 操作 #2

いづ さん さんが27日前に更新 操作 #3

いづ さん さんが27日前に更新 操作 #4

いづ さん さんが27日前に更新 操作 #5

  • 各要素の計算式を決める
  • 計算対象となる全譜面で実際の値を計算しどこかに格納する
  • 計算した生データをベースにレーダー値を決定する

いづ さん さんが27日前に更新 操作 #6

いづ さん さんが27日前に更新 操作 #7

エージェント ちゃん さんが27日前に更新 操作 #8

  • ステータス未着手 から 実装中 に変更
  • 進捗率0 から 10 に変更

作業開始します。

まず既存の譜面メタデータ、難易度表、スコア更新処理の構造を確認し、ノーツレーダー計算値をどこで保持・参照するのが自然かを調査します。

エージェント ちゃん さんが27日前に更新 操作 #9

進捗です。

実装した内容:

  • 譜面ノーツレーダー値の値オブジェクトを追加
    • NOTES / PEAK / SCRATCH / CHARGE / CHORD / SOF-LAN
    • 各値は 0.00〜200.00 にクランプ
  • BMSModel からレーダー値を計算する処理を追加
    • 平均密度、2秒窓のピーク密度、スクラッチ密度、LN/CN/BSS系、同時押し、速度変化周辺ノーツをベースに算出
  • SongData にレーダー値を保持
  • songdata.db の song テーブルへ以下の列を追加し、既存DBには自動ALTERで追加
    • radar_notes
    • radar_peak
    • radar_scratch
    • radar_charge
    • radar_chord
    • radar_soflan
  • 楽曲スキャン時にレーダー値を計算してDBへ保存
  • プレーヤーノーツレーダー用の純粋ロジックを追加
    • 単曲値 = 曲レーダー値 × スコアレート
    • 各要素TOP10平均でプレーヤー側レーダーを算出
  • SQLite永続化とプレーヤー集計のテストを追加

確認:

  • ./gradlew.bat test 成功

次に残る接続候補:

  • 表示/スキンプロパティへのレーダー値公開
  • プレーヤーレーダーの保存先と、スコア更新時の再集計タイミング接続
  • 7KEYSかつ難易度表登録譜面のみを対象にするフィルタの適用位置決定

エージェント ちゃん さんが27日前に更新 操作 #10

方針修正しました。

先の実装では仮係数で 0.00〜200.00 に変換するところまで入れていましたが、仕様が未確定な状態では早すぎるため撤回しました。

現在の実装内容:

  • 0.00〜200.00 の最終レーダー値はまだ計算しません
  • まずは生特徴量を計算して songdata.db に保存します
  • 保存列は以下です
    • feature_notes_density
    • feature_peak_density
    • feature_scratch_density
    • feature_charge
    • feature_chord
    • feature_soflan
  • NotesRadarFeature は非負の生値だけを保持します
  • プレーヤーノーツレーダー集計ロジックは、スケール係数確定後に実装するため一旦外しました

確認:

  • ./gradlew.bat test --tests bms.player.beatoraja.song.SQLiteSongDatabaseAccessorTest 成功
  • ./gradlew.bat test 成功

次は、この生特徴量を難易度表登録済み7KEYS譜面で集計して、分布を見てから正規化係数を決めるのがよいと思います。

エージェント ちゃん さんが27日前に更新 操作 #11

データ整備優先のため、生特徴量を追加拡張しました。

既存の6つの合成寄り特徴量:

  • feature_notes_density
  • feature_peak_density
  • feature_scratch_density
  • feature_charge
  • feature_chord
  • feature_soflan

に加えて、計算式検討用の部品特徴量を保存します。

追加した列:

  • feature_notes_total
  • feature_duration_seconds
  • feature_peak_window_notes
  • feature_scratch_total
  • feature_ln_total
  • feature_ln_seconds_total
  • feature_charge_notes_during_ln
  • feature_chord_timeline_count
  • feature_max_chord_size
  • feature_speed_change_count
  • feature_stop_count
  • feature_scroll_change_count
  • feature_soflan_weighted_notes

意図:

  • いきなり最終レーダー値を決めず、まず合成前の材料を集める
  • 後で難易度表登録済み7KEYS譜面に絞って分布を見ながら、各軸の計算式と正規化係数を決められるようにする
  • CHARGE / CHORD / SOF-LAN の高値要因を分解して確認できるようにする

確認:

  • ./gradlew.bat test --tests bms.player.beatoraja.song.SQLiteSongDatabaseAccessorTest 成功
  • ./gradlew.bat test 成功

注意:

  • 追加列はDBアクセサ生成時に自動追加されます
  • 既存行に値を入れるには、再度楽曲全更新が必要です

エージェント ちゃん さんが27日前に更新 操作 #12

feature_charge_notes_during_ln が全件0になる問題を修正しました。

原因:

  • 以前の実装は「同一レーンのLN保持中に通常ノーツがあるか」を見ていました
  • 実際に拾うべきなのは「どこかのレーンでLN保持中に、別レーンで通常ノーツまたはLN開始がある」ケースでした

修正内容:

  • 各TimeLine処理開始時点で保持中のLNレーン数を算出
  • 現在処理中のノーツが、保持中LNと別レーンにある場合に feature_charge_notes_during_ln を加算
  • 対象は通常ノーツだけでなく、LN開始も含める
  • 該当ノーツ分を feature_charge にも加点
  • 回帰テスト NotesRadarCalculatorTest を追加

確認:

  • ./gradlew.bat test --tests bms.player.beatoraja.song.NotesRadarCalculatorTest 成功
  • ./gradlew.bat test --tests bms.player.beatoraja.song.SQLiteSongDatabaseAccessorTest 成功
  • ./gradlew.bat test 成功

注意:

  • 既存DBの feature_charge_notes_during_ln を更新するには、再度楽曲全更新が必要です

エージェント ちゃん さんが27日前に更新 操作 #13

ソフラン特徴量の調査用データ追加

生特徴量として、既存の feature_soflan / feature_speed_change_count / feature_stop_count / feature_scroll_change_count / feature_soflan_weighted_notes に加えて、ソフラン地帯単位で見るための以下を追加しました。

  • feature_soflan_cluster_count
  • feature_soflan_affected_notes
  • feature_soflan_cluster_score
  • feature_soflan_max_change_rate
  • feature_soflan_stop_time_total

暫定仕様:

  • BPM/STOP/スクロール変更点の前後をソフラン影響範囲としてクラスタ化
  • 近いイベントは同一クラスタとしてマージ
  • クラスタ内のノーツ数、最大変化率、停止時間を保持
  • cluster_score は affected notes と変化率、停止時間から作る調査用スコア

これで単発のBPM変更数だけでなく、「実際にノーツが絡むソフラン地帯か」を見られるようになります。

検証:

  • ./gradlew.bat test --tests bms.player.beatoraja.song.NotesRadarCalculatorTest 成功
  • ./gradlew.bat test --tests bms.player.beatoraja.song.SQLiteSongDatabaseAccessorTest 成功
  • ./gradlew.bat test 成功

次の全更新後、この5項目を含めて難易度表登録済み譜面に絞った分布を見ます。

エージェント ちゃん さんが27日前に更新 操作 #14

全更新後のソフラン特徴量確認

難易度表登録済みの 7keys 譜面に絞って確認しました。

  • 対象: 10,884譜面
  • feature_soflan_cluster_score > 0: 1,896譜面
  • feature_soflan_cluster_score
    • p90: 31.19
    • p95: 103.63
    • p99: 679.46
    • max: 5653.17
  • feature_soflan_affected_notes
    • p90: 49
    • p95: 102
    • p99: 336.34
    • max: 2176
  • feature_soflan_max_change_rate
    • p95: 5.865
    • p99: 4091.89
    • max: 163000163

所感:

  • cluster_score は、実際にノーツが絡むソフラン地帯を拾う指標として使えそうです。
  • 一方で max_change_rate は 0 BPM / 巨大BPM / 停止ギミック表現で極端な値になります。生値として残すのは有用ですが、最終レーダー計算へそのまま入れるべきではなさそうです。
  • 最終式では log1p に加えて、変化率側に上限を置く、または percentile clipping する方が妥当です。
  • feature_scroll_change_count は登録済み7keysでは非ゼロが5譜面のみでした。BMS母集団では BPM/STOP 型ソフランが中心で、SCROLL命令由来はかなり少ないです。

上位には Overjoy / ☆X / 巨大BPM表現の譜面が混ざるため、最終スケール決定時は「難易度表登録済み」だけでなく、極端な表外・隔離系レベルを除いた分布も別に使うのがよさそうです。

エージェント ちゃん さんが27日前に更新 操作 #15

ソフランレーダー暫定式を実装

feature_soflan を暫定的に 0-200 系のレーダー値として計算するようにしました。

方針:

  • 生特徴量はそのまま保持
    • feature_soflan_cluster_score
    • feature_soflan_max_change_rate
    • feature_soflan_affected_notes
    • feature_soflan_stop_time_total
  • レーダー値化する feature_soflan 側だけ、巨大BPM/0 BPM系の外れ値対策を入れる
  • クラスタごとに affectedNotes * log1p(cappedChangeRate) * stopBonus を計算
  • cappedChangeRate は暫定で 100.0 上限
  • 集計後、200 * log1p(score) / log1p(10000) で 0-200 にスケーリングし、最大 200 でクリップ

意図:

  • p95付近のソフラン譜面が概ね100前後に乗るスケール
  • p99以上の強いソフラン譜面は150以上に伸びる
  • 巨大BPM表現や 0 BPM 系でも 200 を超えない

検証:

  • ./gradlew.bat test --tests bms.player.beatoraja.song.NotesRadarCalculatorTest 成功
  • ./gradlew.bat test 成功

注意:

既存DBの feature_soflan は前回更新時の値なので、この暫定式の値を見るには再度楽曲全更新が必要です。

エージェント ちゃん さんが27日前に更新 操作 #16

ソフランレーダー暫定式の全更新後確認

難易度表登録済み 7keys に絞って、暫定 0-200 化後の feature_soflan を確認しました。

対象: 10,884譜面

分布:

  • 非ゼロ: 1,896譜面
  • p90: 75.13
  • p95: 100.38
  • p99: 135.94
  • p99.9: 155.30
  • max: 186.13

帯域別:

  • 0: 8,988譜面
  • 0-25: 167譜面
  • 25-50: 217譜面
  • 50-75: 421譜面
  • 75-100: 541譜面
  • 100-125: 350譜面
  • 125-150: 172譜面
  • 150-175: 27譜面
  • 175-200: 1譜面
  • 200超過: 0譜面

上位例:

  • 909-Sym: 186.13
  • Resonyance Fever: 174.88
  • エキサイティングなユーザ経験: 172.84
  • Calamity Fortune [LAST-DANCE]: 172.47
  • Aloof Wolf: 171.20
  • 点、線、面、立体 [SP CLOCKWORK]: 169.97
  • Lots Of Spices[Another]: 166.25
  • Engine: 160.45
  • Aleph-0[INSANE]: 158.34

所感:

  • p95 がほぼ100になっており、暫定スケールとしては狙い通りです。
  • 最大も186で、巨大BPM/0 BPM表現があっても200に張り付いていません。
  • 上位はソフラン譜面として納得感がありますが、☆X / Overjoy / 巨大BPM表現も混ざるため、最終調整では母集団フィルタを分けて見る必要があります。
  • 現時点ではこの式を暫定採用して、他レーダー項目との相対バランスを見るのがよさそうです。

エージェント ちゃん さんが26日前に更新 操作 #17

ソフランレーダー式を調整

指摘内容:

同じBPM変化でも、変化直後に1ノーツだけ降る譜面と、変化直後に多くのノーツが降る譜面では後者のほうが難しい。現行式はクラスタ内ノーツ数は見ていたが、クラスタ内での時間位置を等価に扱っていた。

対応:

  • レーダー値用に、BPM/STOP/SCROLL変化点からの距離でノーツ重みを変えるように変更
  • 変化直後を最も重くし、変化後2秒へ向けて軽くする
  • 変化前1秒のノーツは補助的に低い重みで扱う
  • 生特徴量の feature_soflan_affected_notes / feature_soflan_cluster_score は従来通り維持
  • feature_soflan に使う内部スコアだけ radarWeightedNotes を使う
  • 強いソフラン譜面が190付近まで届きやすいよう、暫定スケールを scoreAtMax=5000 に調整

追加テスト:

  • 同じBPM変化・同じ影響ノーツ数でも、変化直後にノーツがある譜面の feature_soflan が、遅れてノーツが来る譜面より高くなることを確認
  • 巨大BPM表現でも feature_soflan <= 200 を維持

検証:

  • ./gradlew.bat test --tests bms.player.beatoraja.song.NotesRadarCalculatorTest 成功
  • ./gradlew.bat test 成功

注意:

この調整後の feature_soflan を実データで見るには再度楽曲全更新が必要。

エージェント ちゃん さんが26日前に更新 操作 #18

楽曲DB全更新CLI追加と実行結果

GUIを起動せずに楽曲DB全更新だけを実行するCLIを追加しました。

追加:

  • bms.player.beatoraja.tool.SongDatabaseUpdateCli
  • Gradleタスク updateAllSongs

実行方法:

./gradlew.bat updateAllSongs

オプション:

./gradlew.bat updateAllSongs --args="--incremental"
./gradlew.bat updateAllSongs --args="--path=<folder>"

調整:

  • CLI実行時は -Dbeatoraja.songdb.update.sequential=true を付け、楽曲更新を直列化
  • 並列更新ではDB書き込みロックに多数スレッドが詰まり、実行状態の把握が難しかったため
  • 1000譜面ごとの進捗ログを追加

確認:

  • 直列CLIで全更新完了
  • 実行時間: 242007 ms、Gradle全体では 4m 2s
  • songdata.db: integrity_check ok
  • songinfo.db: integrity_check ok
  • song count: 52,979
  • folder count: 4,208
  • information count: 52,492

更新後の難易度表登録済み 7keys feature_soflan:

  • 対象: 10,884譜面
  • 非ゼロ: 1,896譜面
  • p90: 84.93
  • p95: 112.21
  • p99: 150.05
  • p99.9: 180.76
  • max: 200.0

上位例:

  • 909-Sym: 200.00
  • Aloof Wolf: 200.00
  • Resonyance Fever: 199.51
  • Calamity Fortune [LAST-DANCE]: 195.15
  • エキサイティングなユーザ経験: 194.21
  • Lots Of Spices[Another]: 192.80
  • 点、線、面、立体 [SP CLOCKWORK]: 190.84
  • Aleph-0[INSANE]: 185.22
  • Aleph-0[ANOTHER]: 180.44

神姫系は最高 149.29 でした。ここを190級にしたい場合、現行の「BPM変化直後ノーツ」だけでは足りず、低BPM視認難・連続低速地帯の重みを追加する必要がありそうです。

検証:

  • ./gradlew.bat test 成功

エージェント ちゃん さんが26日前に更新 操作 #19

NOTES / PEAK レーダー暫定式

難易度表登録済み 7keys の生特徴量を確認し、feature_notes_density / feature_peak_density を 0-200 系に暫定式化しました。

生分布:

  • feature_notes_density
    • p90: 24.74
    • p95: 27.35
    • p99: 32.71
    • max: 82.66
  • feature_peak_density
    • p90: 43.0
    • p95: 46.5
    • p99: 53.5
    • max: 397.0

暫定式:

radar = min(200, 100 * sqrt(raw / rawAt100))

NOTES rawAt100 = 27.35
PEAK  rawAt100 = 46.5

狙い:

  • p95 付近を100に置く
  • p99以上は自然に伸ばす
  • 極端なOverjoy/隔離系は200まで到達可能
  • 対数式より中位帯の差を残す

既存DBの生特徴量にこの式を当てた疑似分布:

  • NOTES
    • p90: 95.11
    • p95: 99.99
    • p99: 109.37
    • p99.9: 119.61
    • max: 173.84
  • PEAK
    • p90: 96.16
    • p95: 100.00
    • p99: 107.26
    • p99.9: 119.59
    • max: 200.00

上位例:

  • NOTES: 愛歌「ネグラドルナ」[MODEL 8+] 173.84、戦歌「オルグラリヤ」158.05
  • PEAK: 幽雅に咲かせ、墨染の桜[HYPER] 200.00、愛歌「ネグラドルナ」[MODEL 8+] 160.64、戦歌「オルグラリヤ」154.15

検証:

  • ./gradlew.bat test --tests bms.player.beatoraja.song.NotesRadarCalculatorTest --tests bms.player.beatoraja.song.SQLiteSongDatabaseAccessorTest 成功
  • ./gradlew.bat test 成功

注意:

DBへ実値として反映するには、追加したCLIで再度全更新が必要です。

./gradlew.bat updateAllSongs

エージェント ちゃん さんが26日前に更新 操作 #20

レーダー値の上限基準について

各レーダーで最大値 200.00 が1曲も出ない状態は表示として弱いため、暫定方針として以下に寄せます。

  • 生特徴量の基準点を 100.00 用と 200.00 用で分ける
  • 200.00 基準以上の譜面は一律 200.00 にクリップする
  • それ未満は 100.00 基準も通る曲線で再スケールする

まず NOTES/PEAK に適用しました。

  • NOTES: notes/sec = 27.35 を 100、68.32 を 200
  • PEAK: peak notes/sec = 46.5 を 100、120.0 を 200

既存DBの生特徴量に対する試算では、難易度表登録済み7keys内で NOTES/PEAK ともに 200 到達譜面が出ることを確認しています。DB上の実値反映には再度 ./gradlew.bat updateAllSongs が必要です。

検証:

./gradlew.bat test

結果: BUILD SUCCESSFUL

エージェント ちゃん さんが26日前に更新 操作 #21

NOTES 200基準の見直し

前回の notes/sec = 68.32 を 200.00 基準にする案は、統計的な外れ値に寄りすぎるため見直しました。

難易度表登録済み7keys 10,884件の NOTES 生値分布:

p95    27.3462
p99    32.7139
p99.5  34.4873
p99.9  39.1261
max    82.6555

暫定式を以下に変更しました。

NOTES 100 = 27.35 notes/sec  // p95
NOTES 200 = 34.49 notes/sec  // p99.5

この基準だと、既存DB生特徴量への試算では NOTES 200.00 到達が55譜面になります。Overjoy Lv5 の リジットXXパラダイス39.1239 notes/sec のため 200.00 にクリップされます。

検証:

./gradlew.bat test

結果: BUILD SUCCESSFUL

エージェント ちゃん さんが26日前に更新 操作 #22

NOTES式の再調整

NOTESは単純な 100基準/200基準 の曲線ではなく、難易度表登録済み7keysの密度分布に沿ったアンカー補間に変更しました。

方針:

  • notes/sec の経験分布を固定アンカー化する
  • p70 を 100.00 付近に置く
  • p99.5 を 200.00 に置く
  • p99.5以上は 200.00 にクリップする

現在の主なアンカー:

p50    14.7869 ->  71.43
p70    18.8414 -> 100.00
p75    20.0354 -> 116.95
p80    21.3127 -> 133.89
p85    22.8554 -> 150.83
p90    24.7402 -> 167.78
p95    27.3462 -> 184.72
p97    29.2541 -> 191.51
p98    30.4739 -> 194.91
p99    32.7139 -> 198.30
p99.5  34.4873 -> 200.00

この形だと、秒間30前後を継続する上位譜面が高いNOTES値になるため、体感に近い見え方になります。

既存DB生特徴量への試算:

200到達: 55譜面
リジットXXパラダイス: 39.1239 notes/sec -> 200.00

検証:

./gradlew.bat test

結果: BUILD SUCCESSFUL

エージェント ちゃん さんが26日前に更新 操作 #23

PEAK生特徴量: 3秒窓で再集計

PEAKの集計窓を 2秒 から 3秒 に変更し、./gradlew.bat updateAllSongs でDBを再計算しました。

  • 実行時間: 247,436 ms
  • 結果: BUILD SUCCESSFUL
  • songdata.db integrity_check: ok

対象はこれまでと同じく、難易度表登録済み7keys 10,884件です。PEAK生値は feature_peak_window_notes / 3.0 で見ています。

p0      0.6667
p10     8.6667
p25    16.3333
p50    26.3333
p70    33.3333
p75    35.0000
p80    37.0000
p85    39.0000
p90    41.3333
p95    44.6667
p97    46.6667
p98    48.3333
p99    51.0567
p99.5  54.3333
p99.9  60.8617
max   265.6667

上位例:

265.67  幽雅に咲かせ、墨染の桜[HYPER]
117.33  愛歌「ネグラドルナ」[MODEL 8+]
108.67  戦歌「オルグラリヤ」
 82.67  compressed eyes -XaaaCi-
 71.67  Apollo [SUPERVAKA]
 71.67  Last Battle-T260G-[ORETIC-2]
 70.00  volcanic

参考:

リジットXXパラダイス: 144 notes / 3s = 48.0000 notes/sec

2秒窓より短い瞬間詰め込みは丸まりつつ、3秒持続する局所発狂は十分拾えているため、PEAKは3秒窓を採用してよさそうです。レーダー化はこの分布をもとに別途決めます。

エージェント ちゃん さんが26日前に更新 操作 #24

PEAKレーダー式の実装

PEAKもNOTESと同じく、経験分布アンカー補間に変更しました。

  • 集計窓: 3秒
  • 生値: feature_peak_window_notes / 3.0
  • p70 を 100.00
  • p99.5 を 200.00
  • p99.5以上は 200.00 にクリップ

主なアンカー:

p50    26.3333 ->  71.43
p70    33.3333 -> 100.00
p75    35.0000 -> 116.95
p80    37.0000 -> 133.90
p85    39.0000 -> 150.85
p90    41.3333 -> 167.80
p95    44.6667 -> 184.75
p97    46.6667 -> 191.53
p98    48.3333 -> 194.92
p99    51.0567 -> 198.31
p99.5  54.3333 -> 200.00

実装後に ./gradlew.bat updateAllSongs を実行してDBへ反映しました。

  • 実行時間: 268,063 ms
  • 結果: BUILD SUCCESSFUL
  • songdata.db integrity_check: ok

難易度表登録済み7keys 10,884件での反映後分布:

p50     71.43
p70    100.00
p75    116.95
p80    133.90
p85    150.85
p90    167.80
p95    184.75
p97    191.53
p98    194.92
p99    198.28
p99.5  200.00
p99.9  200.00
max    200.00
200到達 56譜面

参考:

リジットXXパラダイス: 144 notes / 3s = 48.0000 notes/sec -> PEAK 194.24

検証:

./gradlew.bat test

結果: BUILD SUCCESSFUL

エージェント ちゃん さんが26日前に更新 操作 #25

SCRATCHレーダーの暫定式を更新しました。

変更内容

  • 生特徴量として feature_scratch_peak_window_notes を追加
    • 3秒窓内の最大スクラッチ数
    • 局所的な連皿を平均密度とは別に拾うための値
  • SCRATCHレーダー値を以下の合成スコアから算出するように変更
averageScore = scratchTotal / durationSeconds * sqrt(min(scratchTotal / notesTotal / 0.25, 1.0))
peakScore = scratchPeakWindowNotes / 3.0 * 0.5
scratchRaw = max(averageScore, peakScore)

scratchRaw は難易度表登録済み譜面の分布ベースのアンカーで 0.00~200.00 に補間しています。p70付近を100、p99.5付近を200に寄せる暫定設定です。

確認結果

  • ./gradlew.bat test --tests bms.player.beatoraja.song.NotesRadarCalculatorTest --tests bms.player.beatoraja.song.SQLiteSongDatabaseAccessorTest: 成功
  • ./gradlew.bat test: 成功
  • ./gradlew.bat updateAllSongs: 成功
    • song database update finished: 239268 ms
    • DB件数: 52979
    • PRAGMA integrity_check: ok

サンプル

幽雅に咲かせ、墨染の桜[NORMAL] 系は、局所連皿ピークによりSCRATCHが高く出ることを確認しました。

  • 幽雅に咲かせ、墨染の桜[NORMAL]
    • SCRATCH: 200.00
    • scratchTotal: 335~336
    • scratchPeakWindowNotes: 35
    • peak/sec: 11.6667
  • 幽雅に咲かせ、墨染の桜[NORMAL+]
    • SCRATCH: 200.00
    • scratchTotal: 382~383
    • scratchPeakWindowNotes: 38~39
    • peak/sec: 12.6667~13.0000
  • 幽雅に咲かせ、墨染の桜[biginner] / [NORMAL-]
    • SCRATCH: 199.155
    • scratchPeakWindowNotes: 32
    • peak/sec: 10.6667

平均スクラッチ密度だけでは低く見える譜面でも、3秒局所ピークが強ければSCRATCHレーダーとして出るようになっています。

エージェント ちゃん さんが26日前に更新 操作 #26

CHARGE検討用の生特徴量を追加し、全更新して分布を確認しました。

追加した生特徴量

  • feature_charge_notes_during_ln_peak_window
    • LN中に別レーンで処理するノーツ数の3秒窓ピーク
    • 平均では埋もれる局所的なLN拘束を拾うための値
  • feature_max_simultaneous_ln
    • 譜面中の最大同時LN本数
    • 単LN拘束と多重LN拘束を分けるための値

既存の以下の特徴量と組み合わせてCHARGE式を検討できます。

  • feature_ln_total
  • feature_ln_seconds_total
  • feature_charge_notes_during_ln
  • feature_duration_seconds
  • feature_notes_total

確認結果

  • ./gradlew.bat test --tests bms.player.beatoraja.song.NotesRadarCalculatorTest --tests bms.player.beatoraja.song.SQLiteSongDatabaseAccessorTest: 成功
  • ./gradlew.bat test: 成功
  • ./gradlew.bat updateAllSongs: 成功
    • song database update finished: 249172 ms
    • PRAGMA integrity_check: ok

分布メモ

mode=7 and feature>0 の16103件で確認しました。

feature_charge_notes_during_ln_peak_window

p50   4
p70   14
p80   22
p90   38
p95   55
p97   66
p98   73
p99   87
p99.5 101
p99.9 132
max   74412

feature_max_simultaneous_ln

p50   1
p70   2
p80   3
p85   4
p90   6
p95   7
p99.5 8
max   8

再検討メモ

CHARGEは以下の要素を分けて扱うのが良さそうです。

holdDensity = feature_ln_seconds_total / feature_duration_seconds
overlapDensity = feature_charge_notes_during_ln / feature_duration_seconds
overlapRatio = feature_charge_notes_during_ln / feature_notes_total
overlapPeak = feature_charge_notes_during_ln_peak_window / 3.0
maxLn = feature_max_simultaneous_ln

暫定候補としては次のような合成が自然でした。

chargeRaw =
  holdDensity * 0.8
+ overlapDensity * 0.35
+ overlapRatio * 3.0
+ overlapPeak * 0.18
+ min(maxLn, 4) * 0.25

この候補では、普通のCN譜面は中位から高め、穴抜き系やLN拘束中にほぼ叩き続ける譜面は上位、局所的にLN中ノーツが密集する譜面も平均より高く評価できます。

ただし L.D.∞ などの鑑賞・ネタ寄り外れ値は依然として極端に大きいため、最終レーダー化ではNOTES/PEAK/SCRATCHと同じくp99~p99.5付近を200基準にして、それ以上はクリップするのが妥当そうです。

エージェント ちゃん さんが26日前に更新 操作 #27

CHARGEレーダーに暫定式を反映し、全更新後の値を確認しました。

反映した式

holdDensity = feature_ln_seconds_total / feature_duration_seconds
overlapDensity = feature_charge_notes_during_ln / feature_duration_seconds
overlapRatio = feature_charge_notes_during_ln / feature_notes_total
overlapPeak = feature_charge_notes_during_ln_peak_window / 3.0
maxLn = feature_max_simultaneous_ln

chargeRaw =
  holdDensity * 0.8
+ overlapDensity * 0.35
+ overlapRatio * 3.0
+ overlapPeak * 0.18
+ min(maxLn, 4) * 0.25

chargeRawmode=7 and feature>0 の分布からアンカー補間しています。

  • p70付近: 100
  • p99.5付近: 200
  • p99.5超過: 200でクリップ

確認結果

  • ./gradlew.bat test --tests bms.player.beatoraja.song.NotesRadarCalculatorTest --tests bms.player.beatoraja.song.SQLiteSongDatabaseAccessorTest: 成功
  • ./gradlew.bat test: 成功
  • ./gradlew.bat updateAllSongs: 成功
    • song database update finished: 240252 ms
    • PRAGMA integrity_check: ok

CHARGEレーダー分布

mode=7 and feature>0 の16101件です。

nonzero 15202
200到達 81
p10   14.3032
p25   35.6985
p50   71.4037
p70   99.9983
p75   116.9501
p80   133.8943
p85   150.8239
p90   167.8001
p95   184.7502
p97   191.4452
p98   194.9200
p99   198.3100
p99.5 200.0000
p100  200.0000

サンプル

普通のCN系は中位~高めに収まり、LN拘束が強い譜面は上位に出ています。

The First Blade [LegendFencer]                 CHARGE 151.7409
MoonLigh'dDust [SP INSANE]                     CHARGE 148.8704
Sylph (SP ANOTHER)                             CHARGE 141.0641
空から贈る、あなたへの [SP ANOTHER]             CHARGE 137.2803
Majesty of Anubis                              CHARGE 134.3138
B.B.K.K.B.K.K. [戻]                            CHARGE 100.9356
The First Blade [HYPER]                        CHARGE 100.2108

上位200到達には、FREEDOM DiVE [LN FOUR DIMENSIONS]iterated [LN INFINITE]銀の風 more迷人HAELEQUIN -aletheia-_穴抜き など、LN拘束中に大量処理がある譜面が入っています。鑑賞・ネタ寄り外れ値も200に含まれますが、p99.5クリップなので分布全体への影響は抑えられています。

エージェント ちゃん さんが26日前に更新 操作 #28

CHORD検討用の生特徴量を追加し、全更新して分布を確認しました。

追加した生特徴量

  • feature_chord_peak_window_score
    • 3秒窓内の同時押し重みスコア最大値
    • 各タイミングのスコアは既存と同じ (同時押し数 - 1) * 同時押し数
  • feature_chord_peak_window_notes
    • feature_chord_peak_window_score が最大になった3秒窓内の同時押し構成ノーツ数

既存の以下の特徴量と組み合わせてCHORD式を検討できます。

  • feature_chord
    • 既存の平均同時押しスコア密度
  • feature_chord_timeline_count
  • feature_max_chord_size
  • feature_notes_total
  • feature_duration_seconds

確認結果

  • ./gradlew.bat test --tests bms.player.beatoraja.song.NotesRadarCalculatorTest --tests bms.player.beatoraja.song.SQLiteSongDatabaseAccessorTest: 成功
  • ./gradlew.bat test: 成功
  • ./gradlew.bat updateAllSongs: 成功
    • song database update finished: 241014 ms
    • PRAGMA integrity_check: ok

分布メモ

mode=7 and feature>0 の16102件で確認しました。

feature_chord

p50   11.8278
p70   21.7603
p80   29.4097
p90   43.0769
p95   56.9521
p98   74.3618
p99   88.9233
p99.5 106.4349
p99.9 193.4037
max   13790.8138

feature_chord_peak_window_score

p50   120
p70   204
p80   264
p90   370
p95   466
p98   602
p99   688
p99.5 804
p99.9 1964
max   480288

feature_chord_peak_window_notes

p50   52
p70   72
p80   84
p90   102
p95   117
p98   135
p99   149
p99.5 164
p99.9 426
max   82602

式候補

平均同時押し負荷、局所ピーク、同時押し比率、最大同時押し数を混ぜるなら、以下が候補です。

averageScore = feature_chord
peakScore = feature_chord_peak_window_score / 3.0
chordRatio = feature_chord_timeline_count / feature_notes_total
maxChord = feature_max_chord_size

chordRaw =
  averageScore * 0.45
+ peakScore * 0.55
+ chordRatio * 20.0
+ min(maxChord, 8) * 1.2

この候補の分布は以下です。

p50   38.9424
p70   60.1646
p80   75.1742
p90   100.9253
p95   125.3180
p98   155.8728
p99   174.9860
p99.5 204.9837
p99.9 443.9166
max   94271.9233

所感

  • CHORDはNOTES/PEAKとの相関がかなり強いため、単に高密度な譜面が上に来やすいです。
  • ただし feature_chord_peak_window_score を入れると、局所的な同時押しラッシュを平均から分離できます。
  • feature_chord_timeline_count / feature_notes_total は、総ノーツのうちどれだけ同時押し化しているかを見る補助指標として有効です。
  • feature_max_chord_size はほとんど上位で8に張り付くため、主成分ではなく小さい補正が妥当です。
  • 最終レーダー化は、これまでと同じくp70付近を100、p99.5付近を200に置いて、p99.5超過は200クリップが良さそうです。

エージェント ちゃん さんが26日前に更新 操作 #29

CHORDレーダーに暫定式を反映し、全更新後の値を確認しました。

反映した式

averageScore = feature_chord
peakScore = feature_chord_peak_window_score / 3.0
chordRatio = feature_chord_timeline_count / feature_notes_total
maxChord = feature_max_chord_size

chordRaw =
  averageScore * 0.45
+ peakScore * 0.55
+ chordRatio * 20.0
+ min(maxChord, 8) * 1.2

chordRawmode=7 and feature>0 の分布からアンカー補間しています。

  • p70付近: 100
  • p99.5付近: 200
  • p99.5超過: 200でクリップ

確認結果

  • ./gradlew.bat test --tests bms.player.beatoraja.song.NotesRadarCalculatorTest --tests bms.player.beatoraja.song.SQLiteSongDatabaseAccessorTest: 成功
  • ./gradlew.bat test: 成功
  • ./gradlew.bat updateAllSongs: 成功
    • song database update finished: 256918 ms
    • PRAGMA integrity_check: ok

CHORDレーダー分布

mode=7 and feature>0 の16102件です。

nonzero 15994
200到達 81
p10   14.2901
p25   35.6532
p50   71.4299
p70   100.0151
p75   116.9611
p80   133.9024
p85   150.8817
p90   167.8184
p95   184.7500
p97   191.5300
p98   194.9200
p99   198.3100
p99.5 200.0000
p100  200.0000

上位例

上位200には、局所同時押しピークや高密度同時押し比率が強い譜面が入りました。

L.D.∞                                      CHORD 200.00
HEXPERUS [Supernova]                       CHORD 200.00
P8100000000000000000000000000000007       CHORD 200.00
omega i/v/ix/vii/iv/iv                    CHORD 200.00
Unleashed World -ERROR-                    CHORD 200.00
Long9999999999999^99999999999             CHORD 200.00
Kaiden Aura (overjoy)                      CHORD 200.00
痩せろコラ                                  CHORD 200.00
DENGEKI Tube                               CHORD 200.00

NOTES/PEAKとの相関は強いですが、feature_chord_peak_window_score を入れたことで局所的な同時押しラッシュも平均同時押し密度から分離して拾えるようになっています。

エージェント ちゃん さんが26日前に更新 操作 #30

ここまでの作業内容と今後の作業予定をまとめます。

ここまで実施したこと

1. レーダー用の生特徴量を追加

NotesRadarFeature を追加し、各譜面についてレーダー計算・調整に使う生特徴量をDBへ保存できるようにしました。

主な追加特徴量:

  • NOTES
    • feature_notes_total
    • feature_duration_seconds
    • feature_peak_window_notes
  • SCRATCH
    • feature_scratch_total
    • feature_scratch_peak_window_notes
  • CHARGE
    • feature_ln_total
    • feature_ln_seconds_total
    • feature_charge_notes_during_ln
    • feature_charge_notes_during_ln_peak_window
    • feature_max_simultaneous_ln
  • CHORD
    • feature_chord_timeline_count
    • feature_max_chord_size
    • feature_chord_peak_window_score
    • feature_chord_peak_window_notes
  • SOFLAN
    • feature_speed_change_count
    • feature_stop_count
    • feature_scroll_change_count
    • feature_soflan_weighted_notes
    • feature_soflan_cluster_count
    • feature_soflan_affected_notes
    • feature_soflan_cluster_score
    • feature_soflan_max_change_rate
    • feature_soflan_stop_time_total

2. 楽曲DB更新時に特徴量を計算・保存

SongDatabaseUpdater から譜面解析時に NotesRadarCalculator を呼び、songdata.dbsong テーブルへ特徴量とレーダー値を保存するようにしました。

既存DBに対して不足カラムが追加されるよう、SQLiteアクセサ・DTO・ResultSetHandler・永続化テストも更新しています。

3. 全更新用Gradleタスクを追加

こちら側で全更新を実行できるように、updateAllSongs タスクを追加しました。

実行例:

./gradlew.bat updateAllSongs

複数回実行し、直近では以下を確認済みです。

song database update finished: 256918 ms
PRAGMA integrity_check: ok

4. 各レーダーの暫定式を実装

6種すべてについて、0.00~200.00のレーダー値を出す暫定式を実装しました。

NOTES

  • 秒間ノーツ密度を使用
  • mode=7 and feature>0 相当の分布をもとにアンカー補間
  • p70付近を100、p99.5付近を200

PEAK

  • 3秒窓の最大ノーツ数を採用
  • p70付近を100、p99.5付近を200

SCRATCH

平均スクラッチ負荷と局所スクラッチピークの大きい方を採用。

averageScore = scratchTotal / durationSeconds * sqrt(min(scratchTotal / notesTotal / 0.25, 1.0))
peakScore = scratchPeakWindowNotes / 3.0 * 0.5
scratchRaw = max(averageScore, peakScore)

幽雅に咲かせ、墨染の桜[NORMAL] など、局所的な連皿譜面が高く出ることを確認済みです。

CHARGE

LN拘束量、LN中ノーツ、局所ピーク、最大同時LN数を合成。

holdDensity = feature_ln_seconds_total / feature_duration_seconds
overlapDensity = feature_charge_notes_during_ln / feature_duration_seconds
overlapRatio = feature_charge_notes_during_ln / feature_notes_total
overlapPeak = feature_charge_notes_during_ln_peak_window / 3.0
maxLn = feature_max_simultaneous_ln

chargeRaw =
  holdDensity * 0.8
+ overlapDensity * 0.35
+ overlapRatio * 3.0
+ overlapPeak * 0.18
+ min(maxLn, 4) * 0.25

p70付近を100、p99.5付近を200に補間。

CHORD

平均同時押し負荷、局所同時押しピーク、同時押し比率、最大同時押し数を合成。

averageScore = feature_chord
peakScore = feature_chord_peak_window_score / 3.0
chordRatio = feature_chord_timeline_count / feature_notes_total
maxChord = feature_max_chord_size

chordRaw =
  averageScore * 0.45
+ peakScore * 0.55
+ chordRatio * 20.0
+ min(maxChord, 8) * 1.2

p70付近を100、p99.5付近を200に補間。

SOFLAN

BPM/STOP/SCROLL変化の周辺ノーツを重み付けして評価。

  • BPM変化直後ほど重く、時間が離れるほど軽くする
  • 変化前後のクラスタを作り、近い変化はマージ
  • 巨大BPMや極端な変化率は上限を設けて評価
  • Aleph-0、神姫のような強いソフラン譜面が高く出る方向で調整

5. テスト追加・確認

以下を確認済みです。

./gradlew.bat test --tests bms.player.beatoraja.song.NotesRadarCalculatorTest --tests bms.player.beatoraja.song.SQLiteSongDatabaseAccessorTest
./gradlew.bat test
./gradlew.bat updateAllSongs

追加した主なテスト:

  • LN中の別レーンノーツをCHARGEとして数える
  • LN中ノーツの3秒ピークを数える
  • 最大同時LN数を数える
  • SCRATCHの3秒ピークを数える
  • 局所SCRATCHでSCRATCHレーダーが高く出る
  • CHORDの3秒ピークを数える
  • 局所CHORDでCHORDレーダーが高く出る
  • SOFLAN変化直後のノーツを重く評価する
  • 極端なSOFLAN値を200以内に収める
  • DB永続化カラムの保存・復元

現在の状態

全6レーダーについて、暫定式としてDB更新時に0.00~200.00の値が計算・保存される状態です。

NOTES   実装済み
PEAK    実装済み
SCRATCH 実装済み
CHARGE  実装済み
CHORD   実装済み
SOFLAN  実装済み

今後やること

1. 上位譜面の横断確認

各レーダーの上位譜面を一覧化し、体感と大きくズレていないか確認します。

特に確認したい点:

  • 200到達譜面が妥当か
  • 190台の譜面が体感上も最上位クラスか
  • NOTES/PEAK/CHORDが似すぎていないか
  • SCRATCH/CHARGE/SOFLANが特徴譜面を拾えているか

2. 外れ値・鑑賞譜面・人外譜面の扱いを決める

Overjoy Lv8級、人類にクリア不能な譜面、鑑賞・ネタ譜面が難易度表やローカルDBに含まれます。

今後決めるべきこと:

  • どの母集団をレーダー式の基準にするか
  • 統計的外れ値を式決定から除外するか
  • DB上の値としては200クリップでよいか
  • 表示やランキングで外れ値を別扱いする必要があるか

3. beatoraja上での実機確認

DB上の値は計算・保存できていますが、画面表示や運用上の確認が必要です。

確認項目:

  • 楽曲選択画面で各レーダー値が意図通り表示されるか
  • DB更新後に既存曲・新規曲とも値が反映されるか
  • 既存DBからのマイグレーションで問題がないか
  • 値が未計算の曲が0扱いで破綻しないか

4. 代表譜面での体感確認

各レーダーについて代表譜面を選び、実機またはDB値で確認します。

例:

  • SCRATCH: 幽雅に咲かせ、墨染の桜[NORMAL]
  • SOFLAN: Aleph-0神姫
  • CHARGE: FREEDOM DiVE [LN FOUR DIMENSIONS]iterated [LN INFINITE]
  • CHORD: 局所同時押しピークが強い譜面、同時押し比率が高い譜面
  • NOTES/PEAK: 高密度譜面、局所発狂譜面

5. 仕様確定後にコミット

現時点では暫定式実装済みですが、最終確定前の検証段階です。

ユーザー確認後、問題なければ以下の形式でコミット予定です。

refs#145 Implement notes radar feature calculation

※ git pushは行いません。

エージェント ちゃん さんが26日前に更新 操作 #31

スキンプロパティ公開(本日の作業サマリー)

実施内容

src/bms/player/beatoraja/skin/property/FloatPropertyTypes.java に、6種のノーツレーダー値をスキンから参照できるフロートプロパティを追加しました。

プロパティ名 ID 対応レーダー
chart_radar_notes 369 NOTES
chart_radar_peak 370 PEAK
chart_radar_scratch 371 SCRATCH
chart_radar_charge 372 CHARGE
chart_radar_chord 373 CHORD
chart_radar_soflan 374 SOFLAN

スキンの float プロパティ定義でこれらの ID を指定することで、選択中の曲のレーダー値(0.00〜200.00)を取得できます。

テスト

./gradlew.bat test → BUILD SUCCESSFUL


現在の実装状況

項目 状態
レーダー値計算ロジック(NotesRadarCalculator ✅ 完了
生特徴量クラス(NotesRadarFeature ✅ 完了
DB保存(songdata.db 自動マイグレーション) ✅ 完了
全更新CLI(./gradlew.bat updateAllSongs ✅ 完了
スキンプロパティ公開 ✅ 完了(本日追加)
プレーヤーノーツレーダー ⬜ 未実装
実機確認(beatorajaでの表示確認) ⬜ 未実施

残タスク

  1. プレーヤーノーツレーダー — スコア更新時に各要素TOP10平均でプレーヤー側レーダーを算出する処理
  2. 実機確認 — スキンにレーダー値を表示して目視確認

エージェント ちゃん さんが26日前に更新 操作 #32

進捗

プレーヤーノーツレーダー確認用の専用画面を追加しました。

実装内容:

  • F5 でPLAY中以外からプレーヤーノーツレーダー画面へ遷移
  • 画面上で F5 または ESC により元の画面へ戻る
  • PLAY中は F5 を無効化
  • プレーヤーレーダーチャートを表示するデフォルトスキンを追加
  • 左右キー、マウスホイール、選曲入力のターゲット上下(スクラッチ相当)で表示軸を切り替え
    • NOTES → PEAK → SCRATCH → CHARGE → CHORD → SOF-LAN
  • 各軸の対象曲TOP10を表示
    • 表示形式: <レーダー値> <難易度表レベル> <曲名(差分名)>
    • 難易度表レベルはテーブルキャッシュから sl1 / ★9 などのフォルダ名を補完
    • 取得できない場合は譜面レベル LvN にフォールバック

確認:

.\gradlew.bat test --tests bms.player.beatoraja.ScoreDatabaseAccessorTest
.\gradlew.bat test

どちらも BUILD SUCCESSFUL です。

Claude Codeレビューも実施しました。score.mode = 7 を追加すべきという指摘がありましたが、このコードベースの score.mode は譜面モードではなくLN/コース用スコアモードのため不採用にしています。

エージェント ちゃん さんが26日前に更新 操作 #33

追記修正

プレーヤーノーツレーダー画面で既存スコアが反映されない問題に対応しました。

原因:

  • player_notes_radar テーブル追加後、既存 score からの初回再集計が画面表示前に必ず実行されていなかった
  • そのため、新規プレイでスコア更新された場合以外はプレーヤーレーダーチャートが空のままになる可能性があった

修正内容:

  • PlayDataAccessor 初期化時、保存済みプレーヤーレーダーが空なら既存スコアから再集計
  • プレーヤーノーツレーダー画面を開くたびに既存スコアから再集計
  • 空データ環境で毎フレーム再集計しないよう、スキンプロパティ読み取り時の自動再計算は行わない

確認:

.\gradlew.bat test --tests bms.player.beatoraja.ScoreDatabaseAccessorTest
.\gradlew.bat test

結果: BUILD SUCCESSFUL

注意:

既存スコアの再プレイは不要です。ただし、譜面側のレーダー値が songdata.db に未計算の場合はプレーヤーレーダーも算出できないため、その場合は楽曲DB全更新(updateAllSongs 相当)が必要です。

エージェント ちゃん さんが26日前に更新 操作 #34

追記修正

プレーヤーノーツレーダー画面でレーダーチャートだけ表示され、対象曲リストやラベルが表示されない問題を修正しました。

原因:

  • skin/default/playernotesradar.json のフォント指定が VL-Gothic-Regular.ttf になっていた
  • skin/default 直下には該当フォントが存在せず、ログ上も Skin Font読み込み失敗 が発生していた
  • その結果、テキストSkinObjectがprepare時に削除され、対象曲リストだけでなくタイトル/軸ラベルも表示されていなかった

修正内容:

  • フォント参照を実ファイル位置に合わせて ../../font/VL-Gothic-Regular.ttf に変更

確認:

  • 実DBに対してNOTES対象曲TOP10が取得できることを確認
  • ./gradlew.bat test → BUILD SUCCESSFUL

エージェント ちゃん さんが26日前に更新 操作 #35

プレーヤーノーツレーダー画面について、最新スクショ確認後の追加対応を実施しました。

  • デフォルトスキンに現在の各レーダー値一覧を追加
    • NOTES / CHORD / PEAK / CHARGE / SCRATCH / SOF-LAN
    • TOTAL RADAR SCORE
  • 合計値を PlayerNotesRadar.total() として計算し、スキン向けfloatプロパティ player_radar_total も追加
  • デフォルトスキンでは小数2桁の文字列プロパティで現在値を表示
  • 既存スコアからの再計算は画面生成時の同期実行を避け、RuntimeTaskManager のバックグラウンドタスクで実行するよう調整
  • PlayerNotesRadar / PlayerNotesRadarEntry / PlayerNotesRadarAxis の単体テストと合計値検証を追加

検証:

  • python -m json.tool skin/default/playernotesradar.json
  • ./gradlew.bat test

Claude Codeレビューも実施し、指摘された player_radar_total のID重複は修正済みです。

エージェント ちゃん さんが26日前に更新 操作 #36

最新スクショで確認された表示被りを修正しました。

  • SCRATCH / SOF-LAN などのラベルと値が近すぎるため、現在値の数値列を右へ移動
  • TOTAL RADAR SCORE と合計値が重なっていたため、ラベルを TOTAL に短縮し、値列位置を調整
  • TARGET と現在軸名が重なっていたため、軸名表示位置を右へ移動

検証:

  • python -m json.tool skin/default/playernotesradar.json
  • ./gradlew.bat test

エージェント ちゃん さんが26日前に更新 操作 #37

追加対応しました。

  • プレーヤーノーツレーダー画面右上のヘルプが途切れていたため、文言を短縮して表示幅を調整
    • LEFT/RIGHT: axis F5/ESC: back
  • プレーヤーノーツレーダー画面で撮影したスクリーンショットのファイル名に状態名が付くよう修正
    • yyyyMMdd_HHmmss_Player_Notes_Radar.png
  • Claude Codeレビューで指摘された、レーダー画面を開き直した際のバックグラウンド再計算タスク重複起動を防止
  • スコア保存時のプレーヤーノーツレーダー再計算は7KEYSスコア更新時のみに制限

検証:

  • python -m json.tool skin/default/playernotesradar.json
  • ./gradlew.bat test

エージェント ちゃん さんが26日前に更新 操作 #38

追加仕様を反映しました。

  • TOP10選定で同一曲の別差分を除外
    • デイリーチャレンジと同じく title + artist をキーに重複判定
    • 同一曲内では対象レーダー値が最も高い1譜面のみを採用
    • 対象曲一覧だけでなく、プレーヤーレーダー値のTOP10平均計算にも同じ除外条件を適用
  • プレーヤーレーダーチャートの軸順を曲ノーツレーダーと同じ順に変更
    • 12時から時計回りに NOTES -> PEAK -> SCRATCH -> SOF-LAN -> CHARGE -> CHORD
  • デフォルトスキンのレーダーラベルと現在値ラベルの色を曲ノーツレーダー表示に合わせて調整
  • Claude Codeレビュー指摘対応
    • スクショ状態名分岐の既存 else if 抜けを修正
    • プレーヤーレーダー値文字列プロパティIDを軸順に整理
    • レーダー更新タスク完了時の一覧更新は、現在もプレーヤーノーツレーダー画面の場合のみ実行
    • 未使用メソッドを削除

検証:

  • python -m json.tool skin/default/playernotesradar.json
  • ./gradlew.bat test

エージェント ちゃん さんが26日前に更新 操作 #39

NPEログを調査して修正しました。

原因:

  • beatoraja.0.log の 2026-06-10 18:24:26.663 に以下のNPEを確認
    • Cannot invoke "bms.player.beatoraja.skin.Skin.mousePressed(...)" because the return value of "bms.player.beatoraja.MainState.getSkin()" is null
  • 直前にプレーヤーノーツレーダー画面からMusic Selectへ戻るスキン切替が走っており、dispose済みまたは切替中のstateに残ったマウス押下イベントを FrameInputService が処理していた

対応:

  • FrameInputServicemousePressed / mouseDraggedcurrent.getSkin()null の場合はスキンへイベントを渡さないようガード
  • スコア保存後のプレーヤーノーツレーダー再計算をバックグラウンド化し、リザルト遷移時の同期DB処理を回避
  • プレーヤーノーツレーダー対象曲一覧の軸切替時DB取得もバックグラウンド化
    • リクエストIDで古い結果を破棄
    • 画面離脱後は一覧へ反映しない
  • プレーヤーノーツレーダー画面の shutdown() で更新タスクにinterrupt
  • プロパティID資料の不足分を docs/discussion.md に追記

検証:

  • ./gradlew.bat test
  • Claude Codeレビュー実施、指摘された entriesRequestId の可視性問題は volatile 化して修正済み

エージェント ちゃん さんが26日前に更新 操作 #40

調査・対応メモ

最新ログでは beatoraja-player-notes-radar-entries-* のランタイムタスクが多数残ったまま終了していました。F5のプレーヤーノーツレーダー画面で軸切替ごとにTOP10集計SQLを非同期実行していたため、集計が返る前に画面側が空のままになりやすい構造でした。

対応内容:

  • プレーヤーノーツレーダーのTOP10対象曲を score.dbplayer_notes_radar_entry テーブルに永続化するよう変更
  • player_notes_radarplayer_notes_radar_entry を同一更新処理で保存
  • F5のプレーヤーノーツレーダー画面は保存済みデータを読むだけに変更
  • 既存DB向けに、起動時に集計値またはTOP10保存データがない場合は非同期で移行集計を実行
  • スコア更新時は7KEYS譜面の場合にバックグラウンドでレーダー再集計を要求
  • TOP10集計SQLを row_number() ベースに変更し、同一譜面の別差分除外と速度を改善
  • 難易度表レベル表示を保存時に反映するテストを追加

確認:

  • ./gradlew.bat test 実行済み、成功
  • Claude Code review 実行済み。今回のTOP10非表示修正を止めるブロッキング指摘はなし

補足:

F5押下時には再集計しない方針に変更しています。既存DBで初回起動直後だけは、移行集計が完了するまでTOP10が空に見える可能性がありますが、完了後は保存済みテーブルから即時表示されます。

エージェント ちゃん さんが26日前に更新 操作 #41

追加対応メモ

プレーヤーノーツレーダーTOP10の難易度表レベル表記について、参照対象を以下の難易度表に限定しました。

  • NEW GENERATION通常難易度
  • NEW GENERATION発狂難易度
  • 第三期Overjoy
  • 通常難易度表
  • 発狂BMS難易度表
  • Satellite
  • Stella

実装内容:

  • 難易度表レベル収集時に、上記7表以外のキャッシュ済み難易度表を除外
  • URL一致を優先し、名称はNFKC正規化・空白除去後に補助判定
  • DP/LN/24K/PMSなどの難易度表レベルはプレーヤーノーツレーダーTOP10には使わない
  • Mode.BEAT_7K.id == 7 をテストで確認
  • プレーヤーノーツレーダー更新中にスコア保存側が重いDB集計のロック待ちをしないよう、更新要求の多重起動抑制を軽量化
  • DB保存失敗時に未保存の計算値をメモリキャッシュへ返さないよう修正

確認:

  • ./gradlew.bat test --tests bms.player.beatoraja.PlayDataAccessorTest --tests bms.player.beatoraja.ScoreDatabaseAccessorTest 成功
  • ./gradlew.bat test 成功
  • Claude Code review は一度実行し、有効指摘を修正済み。修正後の再レビューはClaude CLIのセッション上限により実行不可でした。

エージェント ちゃん さんが26日前に更新 操作 #42

追加対応メモ

プレーヤーノーツレーダーを既存スコアから手動再集計できるGradleタスクを追加しました。

追加コマンド:

./gradlew.bat updatePlayerNotesRadar

オプション:

./gradlew.bat updatePlayerNotesRadar --args="--player=<playerName>"
./gradlew.bat updatePlayerNotesRadar --args="--score-db=<path>"
./gradlew.bat updatePlayerNotesRadar --args="--song-db=<path> --table-dir=<path>"

実装内容:

  • build.gradleupdatePlayerNotesRadar JavaExecタスクを追加
  • PlayerNotesRadarUpdateCli を追加し、UIを起動せずに score.dbplayer_notes_radar / player_notes_radar_entry を再計算
  • 難易度表レベルの対象表判定を PlayerNotesRadarLevelTables に切り出し、ゲーム起動時更新とCLIで共有
  • docs/discussion.md に手動再集計コマンドを追記

確認:

  • ./gradlew.bat updatePlayerNotesRadar --args="--help" 成功
  • ./gradlew.bat test --tests bms.player.beatoraja.PlayDataAccessorTest --tests bms.player.beatoraja.ScoreDatabaseAccessorTest 成功
  • ./gradlew.bat test 成功

補足:

Claude Code reviewは前回以降CLIのセッション上限中のため再実行できていません。

エージェント ちゃん さんが26日前に更新 操作 #43

追加対応メモ

WMII_FHDのリザルト画面で、その回のスコアを使ったノーツレーダー値を表示できるようにしました。

実装内容:

  • リザルト用レーダー値プロパティを追加
    • result_radar_notes = 1890
    • result_radar_peak = 1891
    • result_radar_scratch = 1892
    • result_radar_soflan = 1893
    • result_radar_charge = 1894
    • result_radar_chord = 1895
    • result_radar_total = 1897
  • 計算式は自己ベストではなく、その回のスコアを使用
リザルト単曲レーダー値 = 曲ノーツレーダー値 * (今回EXスコア / (総ノーツ * 2))
  • WMII_FHDのリザルトExpand PanelのGRAPH側に2行で表示
    • NOTES / PEAK / SCR
    • SOF / CHG / CHD / TOTAL
  • Luaから参照できるよう、追加プロパティは main_state.float_number() で取得可能なRateType側に登録
  • skin/WMII_FHD/result/resultExpand.lua を追跡対象にするため .gitignore に例外を追加
  • docs/discussion.md にIDと計算式を追記

確認:

  • ./gradlew.bat test --tests bms.player.beatoraja.skin.property.SkinPropertyExecutionTest --tests bms.player.beatoraja.skin.property.SkinPropertyFactoryRegistryTest 成功
  • ./gradlew.bat test 成功

補足:

Claude Code reviewはClaude CLIのセッション上限により実行できませんでした。

エージェント ちゃん さんが26日前に更新 操作 #44

PEAKの短時間バースト補正を実装しました。

  • 既存の3秒窓PEAKを維持しつつ、1秒窓の短時間バースト密度を70%混ぜた補正値も評価するように変更
  • peakRaw = max(3秒窓密度, 1秒窓密度 * 0.70 + 3秒窓密度 * 0.30)
  • 1秒以内に集中する短時間バーストでPEAKが上がることを単体テストで追加確認
  • docs/discussion.md に計算式を追記

確認:

  • ./gradlew.bat test --tests bms.player.beatoraja.song.NotesRadarCalculatorTest 成功
  • ./gradlew.bat test 成功
  • Claude Codeレビューは You've hit your session limit · resets 10:20pm (Asia/Tokyo) のため実行不可でした。

エージェント ちゃん さんが26日前に更新 操作 #45

WMII_FHDリザルトのGraph Dataパネル下部表示を調整しました。

  • DURATION AVG / TIMING AVG / STDDEV の数値描画を削除
  • 同じ下部エリアを黒背景で覆い、今回プレイスコア基準のノーツレーダーを2行表示に変更
    • NOTES / PEAK / SCR
    • SOF / CHG / CHD / TOTAL
  • 以前追加していた上部のノーツレーダー表示は削除し、指定された下部エリアへ移動
  • .gitignore を調整し、skin/WMII_FHD/result/resultExpand.lua だけを追跡対象にしました

確認:

  • durationAvg / durationAvgAdot / stddav / stddaAdot の描画参照が resultExpand.lua から消えていることを確認
  • ローカルに lua / luac がないためLua構文チェッカは未実行です。

エージェント ちゃん さんが26日前に更新 操作 #46

WMII_FHDリザルトのノーツレーダー表示背景を不透明化しました。

  • resultRadarBack のテクスチャ背景を削除
  • 組み込み黒矩形 id = -110 を使用し、a = 255 の不透明黒背景に変更
  • この対応では .gitignore は変更していません。

エージェント ちゃん さんが26日前に更新 操作 #47

Luxe_Flat風のプレーヤーノーツレーダー画面スキンを追加しました。

  • skin/Luxe_Flat/playernotesradar.json を作成
  • 1920x1080、Luxe_Flat既存の背景画像を使用
  • 左に大きめのプレーヤーレーダーチャート
  • 右上に各軸の現在値とTOTAL
  • 右下に現在選択中レーダー軸のTOP10対象曲を1列表示
  • 軸ラベル色はノーツレーダーの色に合わせました
  • .gitignore は変更していません

確認:

  • PowerShell ConvertFrom-Json でJSONとして読めることを確認
  • skin/Luxe_Flat/playernotesradar.json は既存 .gitignore/skin/* 対象のため、Gitステータスには表示されません。

エージェント ちゃん さんが26日前に更新 操作 #48

Luxe_Flat風プレーヤーノーツレーダー画面をLuaスキン化して作り直しました。

  • skin/Luxe_Flat/playernotesradar.json を削除
  • skin/Luxe_Flat/playernotesradar.luaskin を追加
  • skin/Luxe_Flat/playernotesradar.lua を追加
  • 専用フレーム画像を追加
    • skin/Luxe_Flat/playernotesradar/panel_radar.png
    • skin/Luxe_Flat/playernotesradar/panel_values.png
    • skin/Luxe_Flat/playernotesradar/panel_list.png
    • skin/Luxe_Flat/playernotesradar/topbar.png
  • レイアウトを再設計
    • 上部: タイトル / 現在ターゲット / 操作ガイド
    • 左: レーダーチャート
    • 右上: 6軸の現在値を2列グリッド表示、CHORDだけ別扱いにしない
    • 右下: TOP10対象曲を1列で広く表示
  • .gitignore は変更していません

確認:

  • 一時的なJUnitで LuaSkinLoader.loadHeader("skin/Luxe_Flat/playernotesradar.luaskin") が成功することを確認後、テスト差分は戻しました。
  • Claude Codeレビューは You've hit your session limit · resets 10:20pm (Asia/Tokyo) のため実行不可でした。

エージェント ちゃん さんが26日前に更新 操作 #49

プレーヤーノーツレーダーTOP10をテーブル表示できるよう、列別StringPropertyを追加しました。

  • 既存の結合済み行 player_radar_entry1 - player_radar_entry10 (1870 - 1879) は互換用に維持
  • 新規refを追加
    • player_radar_entry1_value - player_radar_entry10_value (1900 - 1909)
    • player_radar_entry1_level - player_radar_entry10_level (1910 - 1919)
    • player_radar_entry1_title - player_radar_entry10_title (1920 - 1929)
  • PlayerNotesRadarState にVALUE/LEVEL/TITLE別アクセサを追加
  • Luxe_Flat playernotesradar.lua のTOP10表示を、1行文字列ではなく3列テキストに変更
  • docs/discussion.md に新規IDを追記

確認:

  • ./gradlew.bat test --tests bms.player.beatoraja.PlayerNotesRadarTest --tests bms.player.beatoraja.skin.property.SkinPropertyExecutionTest 成功
  • ./gradlew.bat test 成功
  • 一時JUnitで LuaSkinLoader.loadHeader("skin/Luxe_Flat/playernotesradar.luaskin") 成功確認後、テスト差分は戻し済み
  • Claude Codeレビューは You've hit your session limit · resets 10:20pm (Asia/Tokyo) のため実行不可でした。

エージェント ちゃん さんが26日前に更新 操作 #50

Luxe_Flatプレーヤーノーツレーダー画面のフォントと左上ラベルを調整しました。

  • 左上の PLAYER PROFILE を削除
  • VL-Gothic-Regular.ttf からLuxe_Flat既存のBMFontへ変更
    • タイトル/現在ターゲット: font_Title
    • 見出し/ラベル: font_sub / font_sub_small
    • 数値/TOP10: font_songlist
  • 一時JUnitで LuaSkinLoader.loadHeader("skin/Luxe_Flat/playernotesradar.luaskin") 成功確認後、テスト差分は戻し済み

スクショ上のガビガビ感は、VL Gothic TTFを大きめサイズで描画していた影響が強いと判断しました。

エージェント ちゃん さんが26日前に更新 操作 #51

プレーヤーノーツレーダー画面のTOP表示を調整しました。

  • Luxe_Flatスキン上部中央の CURRENT TARGET / ターゲット名表示を削除
  • TOP10パネル見出しを動的表示に変更
    • NOTES CHARTS TOP 10
    • PEAK CHARTS TOP 10
    • など、現在選択中の軸名を反映
  • 新規StringProperty player_radar_axis_charts_top10 (1930) を追加
  • docs/discussion.md に新規IDを追記

確認:

  • ./gradlew.bat test --tests bms.player.beatoraja.skin.property.SkinPropertyExecutionTest.playerRadarTableColumnStringPropertiesExist 成功
  • 一時JUnitで LuaSkinLoader.loadHeader("skin/Luxe_Flat/playernotesradar.luaskin") 成功確認後、テスト差分は戻し済み
  • ./gradlew.bat test 成功
  • Claude Codeレビューは You've hit your session limit · resets 10:20pm (Asia/Tokyo) のため実行不可でした。

エージェント ちゃん さんが26日前に更新 操作 #52

Luxe_FlatプレーヤーノーツレーダーのTOP10見出しを部分色分けしました。

  • main_state.text(1866) で現在TARGET名を参照
  • TARGET名部分だけ軸色で表示
  • CHARTS TOP 10 は従来の見出し色で表示
  • SCRATCH CHARTS TOP 10 でも被らないよう、サフィックスの開始位置を右へ調整

確認:

  • 一時JUnitで LuaSkinLoader.loadHeader("skin/Luxe_Flat/playernotesradar.luaskin") 成功確認後、テスト差分は戻し済み

エージェント ちゃん さんが26日前に更新 操作 #53

Luxe_Flatプレーヤーノーツレーダーのレーダーチャート半径設定を曲ノーツレーダーと揃えました。

  • limitRadiusRatio = 0.53
  • maxRadiusRatio = 0.98
  • max = 200, limit = 100 は維持

これにより、グリッド最外周が100相当、100を超える値はグリッド外側へ伸びて最大200まで描画されます。

確認:

  • 一時JUnitで LuaSkinLoader.loadHeader("skin/Luxe_Flat/playernotesradar.luaskin") 成功確認後、テスト差分は戻し済み

エージェント ちゃん さんが26日前に更新 操作 #54

Luxe_Flatプレーヤーノーツレーダーのレーダーチャート色を曲側ノーツレーダーと揃えました。

曲側 notes_radar_chart と同じ設定:

  • gridColor = "FFFFFFAA"
  • fillColor = "00CCFF28"
  • outlineColor = "00AEEFFF"

確認:

  • 一時JUnitで LuaSkinLoader.loadHeader("skin/Luxe_Flat/playernotesradar.luaskin") 成功確認後、テスト差分は戻し済み

エージェント ちゃん さんが26日前に更新 操作 #55

  • ステータス実装中 から 完了 に変更
  • 進捗率10 から 100 に変更

実装完了

コミット: 1312f491 (refs#145 Add notes radar and player radar support)

実装内容:

  • 曲ノーツレーダー算出と PEAK の短時間バースト補正を追加
  • プレーヤーノーツレーダー用DBテーブル、既存スコアからの再計算、TOP10対象曲の永続化を追加
  • gradlew updatePlayerNotesRadar でプレーヤーノーツレーダーを手動更新できるCLIを追加
  • PLAY中以外から遷移できる専用プレーヤーノーツレーダー画面とデフォルトスキン定義を追加
  • WMII_FHD等のリザルト向けに今回プレイ分スコアのノーツレーダー表示用プロパティを追加
  • スクリーンショットファイル名、難易度表レベル表記、TOP10の同一譜面別差分除外に対応

確認:

  • ./gradlew.bat test 成功
  • ./gradlew.bat updateAllSongs 後に ./gradlew.bat updatePlayerNotesRadar を実行済み

補足:

  • Claude Codeレビューは claude CLI のセッション上限により実行できませんでした。
  • skin/Luxe_Flat / skin/WMII_FHD 配下のスキン変更は ignore のままコミット対象外です。

いづ さん さんが22日前に更新 操作 #56

  • 対象バージョン1.0.0 にセット

いづ さん さんが17日前に更新 操作 #57

  • 関連している 機能 #313: ノーツレーダー6軸の細分化データモデル設計(弱点分析 #293 の前提整備) を追加

他の形式にエクスポート: PDF Atom