Skip to content

Feat: Normalize Scoring - #132

Draft
oldcrate wants to merge 4 commits into
Rhythia:indevfrom
oldcrate:indev
Draft

Feat: Normalize Scoring#132
oldcrate wants to merge 4 commits into
Rhythia:indevfrom
oldcrate:indev

Conversation

@oldcrate

@oldcrate oldcrate commented Sep 3, 2026

Copy link
Copy Markdown

Score is capped to 1 milion for all levels.
Modifiers and speed modifiers are applied to the max score making it higher or lower.

I also had to fix Attempt.ModsMultiplier since it was not being assigned and would always be equal to 1 no matter what.

@itronite

itronite commented Sep 3, 2026

Copy link
Copy Markdown

overall checks out, but i feel it'd be better to just round on hit rather than on total score, that way you can avoid rounding everywhere
then don't forget to clamp to max score so it doesnt exceed it

just keep the int cast that was here

 double hitScore = Attempt.UnitHitScore * Attempt.ComboMultiplier * factor;

@oldcrate

oldcrate commented Sep 3, 2026

Copy link
Copy Markdown
Author

Rounding on hit makes a bigger error then just rounding the total score.
I tried and with a perfect run I get a score of 999,995 instead of the full milion.

@itronite

itronite commented Sep 3, 2026

Copy link
Copy Markdown

right, my mistake, if you round down once the score can be too low. Math.ceil should do it then, instead of Math.round

btw, i'm against having a double score counter because the game doesn't support it visually, and afaik no other rg i've seen uses decimal places

@oldcrate

oldcrate commented Sep 3, 2026

Copy link
Copy Markdown
Author

Again, it's off by 40 points.

To not round it every time you could store the score as a double in RawScore and write the getter of Score to round RawScore and convert it to uint (thinking about it, it's still rounding everytime but it's written somewhere else).

@itronite

itronite commented Sep 3, 2026

Copy link
Copy Markdown

aaaaalright, i've been testing around for a bit and the decimal part does matter, as the formula stops working properly at around 1k notes. the score shown in game should regardless be rounded to not show any decimal places, so you do need to round on every note.

on another note, i checked this loop again and it looks quite naive

 for (uint i = 1; i <= 7; i++)
        {
            NoteWeight += ComboMultiplierIncrement * i;
        }

        NoteWeight += ((uint)Map.Notes.Length - ComboMultiplierIncrement * 7) * 8;

suppose there is a map with 1 note; obviously that note should award 1M points by itself, however:

  1. the minimum combo increment is 2 notes/combo
  2. this loop always builds up combo, even after running out of notes

therefore you'll get the wrong weight, even negative (e.g. w/ 1 note, increment of 2):

2 * (1 + 2 + 3 ... + 7) (loop) = 56
then add (1 - 2 * 7) * 8 = -104 + 56 = -48

@senternall

Copy link
Copy Markdown

quick question but how would normalization work with the ingame score multiplier? cuz i feel like if normilization gets added thatd either have to be reworked or removed completely.

@oldcrate

oldcrate commented Sep 3, 2026

Copy link
Copy Markdown
Author

quick question but how would normalization work with the ingame score multiplier? cuz i feel like if normilization gets added thatd either have to be reworked or removed completely.

The multiplier applies to the max score. So a 1.1x multiplier means that the maximum score is no longer 1,000,000 but 1,100,000.

suppose there is a map with 1 note; obviously that note should award 1M points by itself, however:

  1. the minimum combo increment is 2
  2. this loop always builds up combo, even after running out of notes

therefore you'll get the wrong weight, even negative (e.g. w/ 1 note, increment of 2):

You are right to mention that, I did not account for maps under 8 notes.
A fix I have found is to allow for the increment to be 1 when the map has less then 9 notes and to not hardcode the number of iterations in the loop and a couple other things.

I will make another comment/commit as soon as I get it working.

@oldcrate

oldcrate commented Sep 3, 2026

Copy link
Copy Markdown
Author

Now the normalization accounts for levels under 9 notes by calculating the max combo you reach in a level, instead of having it hardcoded to 7.

@itronite

itronite commented Sep 3, 2026

Copy link
Copy Markdown

i feel like this line

ComboMultiplierIncrement = Map.Notes.Length <= 8 ? 1 : Math.Max(2, (uint)Map.Notes.Length / 200);

causes some weird behavior now. the combo increment shouldn't really change even if the map is under 8 notes.

a map with 9 notes should reach combo 4, same as 8 notes, given the code that was previously there. with that new expression, a map with 8 notes will get up to combo 7 (clamped by 1-7 range), which is quite unbalanced.
the maximum combo is calculated correctly, just avoid that weird ternary

also maybe for the sake of clarity, stick with maximum possible combo being 8 instead of 7.

edit: in fact, this sum

for (uint i = 1; i <= MaxComboMultiplier; i++)
        {
            NoteWeight += ComboMultiplierIncrement * i;
        }
        
        NoteWeight += ((uint)Map.Notes.Length - ComboMultiplierIncrement * MaxComboMultiplier) * (MaxComboMultiplier + 1);

runs once more than needed on combos below 7 (because the last sentence always runs)

... atp if you wanna just replicate this code then that also works

@oldcrate

oldcrate commented Sep 3, 2026

Copy link
Copy Markdown
Author

Yeah, I named the variable wrong.

It isn't really the max combo rather the iterations for the loop and since I used it more times I decided to put it in a variable.

        ComboMultiplierIncrement = Math.Max(2, (uint)Map.Notes.Length / 200);
        // ...

        uint noteWeightIterations = (uint)Math.Clamp(Math.Floor((double)Map.Notes.Length / ComboMultiplierIncrement), 1, 7);

        for (uint i = 1; i <= noteWeightIterations; i++)
        {
            NoteWeight += Math.Min(ComboMultiplierIncrement, (uint)Map.Notes.Length) * i;
        }

        NoteWeight += ((uint)Map.Notes.Length - Math.Min(ComboMultiplierIncrement, (uint)Map.Notes.Length) * noteWeightIterations) * (noteWeightIterations + 1);

This should be clearer (also removed the ternary operator as you suggested).

runs once more than needed on combos below 7 (because the last sentence always runs)

If we have 7 notes that would mean a max combo of 3. The for loop adds 2 + 4 + 6 = 12 and the sum after the for loop adds an additional 4 (notes left * next combo which is 1 * 4 = 4 in this case).

So this brings the note weight to 16 notes.

During gameplay you hit the first 2 notes that weigh 1, 2 notes that weigh 2, 2 that weigh 3 and 1 that weighs 4. So the weight of the notes hit during gameplay is 2 + 4 + 6 + 4, the same as the one calculated before.

I don't see where it runs an additional time.

@senternall

Copy link
Copy Markdown

quick question but how would normalization work with the ingame score multiplier? cuz i feel like if normilization gets added thatd either have to be reworked or removed completely.

The multiplier applies to the max score. So a 1.1x multiplier means that the maximum score is no longer 1,000,000 but 1,100,000.

suppose there is a map with 1 note; obviously that note should award 1M points by itself, however:

  1. the minimum combo increment is 2
  2. this loop always builds up combo, even after running out of notes

therefore you'll get the wrong weight, even negative (e.g. w/ 1 note, increment of 2):

You are right to mention that, I did not account for maps under 8 notes. A fix I have found is to allow for the increment to be 1 when the map has less then 9 notes and to not hardcode the number of iterations in the loop and a couple other things.

I will make another comment/commit as soon as I get it working.

what i meant is the 8x score multiplier u get for hitting notes without missing. you could theoretically include that but it seems unviable in a way

@itronite

itronite commented Sep 4, 2026

Copy link
Copy Markdown

what i meant is the 8x score multiplier u get for hitting notes without missing. you could theoretically include that but it seems unviable in a way

this is what we're trying to do, it's perfectly possible. most similar example i can think of is djmax which goes up to x5 over time, but the scoring is normalized to 1M

If we have 7 notes that would mean a max combo of 3. The for loop adds 2 + 4 + 6 = 12 and the sum after the for loop adds an additional 4 (notes left * next combo which is 1 * 4 = 4 in this case).

So this brings the note weight to 16 notes.

During gameplay you hit the first 2 notes that weigh 1, 2 notes that weigh 2, 2 that weigh 3 and 1 that weighs 4. So the weight of the notes hit during gameplay is 2 + 4 + 6 + 4, the same as the one calculated before.

I don't see where it runs an additional time.

ok right, mb, didn't account for the case where the map length == combo increment * n.º of increments correctly
this should be working correctly now

edit: since combo increment is a constant you can inline the loop, probably the compiler does this internally

@oldcrate

oldcrate commented Sep 4, 2026

Copy link
Copy Markdown
Author

edit: since combo increment is a constant you can inline the loop, probably the compiler does this internally

I didn't think about squashing the for loop into one computation. That would in fact be more optimal.

uint noteWeightIterations = (uint)Math.Clamp(Math.Floor((double)Map.Notes.Length / ComboMultiplierIncrement), 1, 7);
NoteWeight += Math.Min(ComboMultiplierIncrement, (uint)Map.Notes.Length) * (1 + noteWeightIterations) * noteWeightIterations / 2;
NoteWeight += ((uint)Map.Notes.Length - Math.Min(ComboMultiplierIncrement, (uint)Map.Notes.Length) * noteWeightIterations) * (noteWeightIterations + 1);

This code sums up the range of i from 1 to noteWeightIterations and calculates the weight of the notes during ramp up only once.

Now the variable name noteWeightIterations is misleading though because there are no iterations. Do you have any suggestions on how to name it?

@oldcrate
oldcrate marked this pull request as draft September 4, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants