Skip to content

Commit 42109e9

Browse files
committed
Rework bucket formation logic
1 parent b0b6e67 commit 42109e9

File tree

3 files changed

+115
-24
lines changed

3 files changed

+115
-24
lines changed

Diff for: src/card-draw.ts

+41-16
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { nanoid } from "nanoid";
22
import { GameData, Song, Chart } from "./models/SongData";
3-
import { chunkInPieces, pickRandomItem, shuffle, times } from "./utils";
3+
import { pickRandomItem, shuffle, times } from "./utils";
44
import { CountingSet } from "./utils/counting-set";
55
import { DefaultingMap } from "./utils/defaulting-set";
6+
import { Fraction } from "./utils/fraction";
67
import { DrawnChart, EligibleChart, Drawing } from "./models/Drawing";
78
import { ConfigState } from "./config-state";
89
import { getDifficultyColor } from "./hooks/useDifficultyColor";
@@ -12,6 +13,15 @@ import {
1213
getDiffAbbr,
1314
} from "./game-data-utils";
1415

16+
function clampToNearest(incr: number, n: number, clamp: (n: number) => number) {
17+
const multor = Math.round(1 / incr);
18+
let ret = clamp(n * multor) / multor;
19+
if (Number.isInteger(n) && clamp === Math.floor) {
20+
ret -= incr;
21+
}
22+
return ret;
23+
}
24+
1525
export function getDrawnChart(
1626
gameData: GameData,
1727
currentSong: Song,
@@ -67,9 +77,8 @@ export function chartIsValid(
6777
}
6878

6979
export function* eligibleCharts(config: ConfigState, gameData: GameData) {
70-
const buckets = getBuckets(
71-
config,
72-
getAvailableLevels(gameData, config.useGranularLevels),
80+
const buckets = Array.from(
81+
getBuckets(config, getAvailableLevels(gameData, config.useGranularLevels)),
7382
);
7483
for (const currentSong of gameData.songs) {
7584
if (!songIsValid(config, currentSong)) {
@@ -105,37 +114,50 @@ export function* eligibleCharts(config: ConfigState, gameData: GameData) {
105114

106115
export type LevelRangeBucket = [low: number, high: number];
107116
export type BucketLvlRanges = Array<LevelRangeBucket>;
108-
export type LvlRanges = Array<number> | BucketLvlRanges;
117+
export type LvlRanges = Array<number | LevelRangeBucket>;
109118

110119
/**
111120
*
112121
* @param cfg
113122
* @param availableLvls prefer granular
114123
* @returns
115124
*/
116-
export function getBuckets(
125+
export function* getBuckets(
117126
cfg: Pick<
118127
ConfigState,
119128
"useWeights" | "probabilityBucketCount" | "upperBound" | "lowerBound"
120129
>,
121130
availableLvls: Array<number>,
122-
): LvlRanges {
131+
): Generator<LevelRangeBucket | number> {
123132
const { useWeights, probabilityBucketCount, upperBound, lowerBound } = cfg;
124133
const absoluteRangeSize = upperBound - lowerBound + 1;
125134
if (!useWeights || !probabilityBucketCount) {
126-
return times(absoluteRangeSize, (n) => n - 1 + lowerBound);
135+
for (let n = lowerBound; n <= upperBound; n++) {
136+
yield n;
137+
}
138+
return;
127139
}
128-
const lowerIndex = availableLvls.indexOf(lowerBound);
140+
const bucketWidth = new Fraction(absoluteRangeSize, probabilityBucketCount);
129141
let upperIndex: number | undefined = availableLvls.indexOf(upperBound + 1);
130142
if (upperIndex === -1) {
131143
upperIndex = undefined;
132144
}
133-
const levelsInRange = availableLvls.slice(lowerIndex, upperIndex);
134-
return Array.from(chunkInPieces(probabilityBucketCount, levelsInRange)).map(
135-
(levels): LevelRangeBucket => {
136-
return [levels[0], levels[levels.length - 1]];
137-
},
138-
);
145+
// TODO add this to the data file spec
146+
const incrementGuess = availableLvls[1] - availableLvls[0];
147+
const lowerBoundF = new Fraction(lowerBound);
148+
const nudge = new Fraction(1, 1000);
149+
for (let i = 0; i < probabilityBucketCount; i++) {
150+
const bucketBottom = bucketWidth.mult(new Fraction(i)).add(lowerBoundF);
151+
const bucketTop = bucketBottom.add(bucketWidth);
152+
yield [
153+
clampToNearest(incrementGuess, bucketBottom.valueOf(), Math.ceil),
154+
clampToNearest(
155+
incrementGuess,
156+
bucketTop.sub(nudge).valueOf(),
157+
Math.floor,
158+
),
159+
];
160+
}
139161
}
140162

141163
/**
@@ -184,7 +206,10 @@ export function draw(gameData: GameData, configData: ConfigState): Drawing {
184206

185207
for (const chart of eligibleCharts(configData, gameData)) {
186208
const bucketIdx = useWeights
187-
? bucketIndexForLvl(chartLevelOrTier(chart, useGranularLevels), buckets)
209+
? bucketIndexForLvl(
210+
chartLevelOrTier(chart, useGranularLevels),
211+
Array.from(buckets),
212+
)
188213
: 0; // outside of weights mode we just put all songs into one shared bucket
189214
if (bucketIdx === null) continue;
190215
validCharts.get(bucketIdx).push(chart);

Diff for: src/controls/controls-weights.tsx

+10-8
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,16 @@ export function WeightsControls({ usesTiers, high, low }: Props) {
5252
const gameData = useDrawState((s) => s.gameData);
5353
const groups = useMemo(() => {
5454
const availableLevels = getAvailableLevels(gameData, useGranularLevels);
55-
return getBuckets(
56-
{
57-
lowerBound: low,
58-
upperBound: high,
59-
useWeights,
60-
probabilityBucketCount: bucketCount,
61-
},
62-
availableLevels,
55+
return Array.from(
56+
getBuckets(
57+
{
58+
lowerBound: low,
59+
upperBound: high,
60+
useWeights,
61+
probabilityBucketCount: bucketCount,
62+
},
63+
availableLevels,
64+
),
6365
);
6466
}, [gameData, useGranularLevels, low, high, useWeights, bucketCount]);
6567

Diff for: src/utils/fraction.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
function gcd(a: number, b: number) {
2+
if (!a) return b;
3+
if (!b) return a;
4+
while (true) {
5+
a %= b;
6+
if (!a) return b;
7+
b %= a;
8+
if (!b) return a;
9+
}
10+
}
11+
12+
/*********
13+
* This is a port of the bare minimum functionality from the 'fraction.js'
14+
* npm package to suit the needs of dealing with fractional beat offsets.
15+
* The original package has types written in a strange way that doesn't make
16+
* typescript happy when in pure ESM mode.
17+
*
18+
* This file and the original implementation are available under the MIT license.
19+
* See: https://github.com/infusion/Fraction.js/blob/master/fraction.js
20+
*********/
21+
export class Fraction {
22+
constructor(
23+
public n: number,
24+
public d: number = 1,
25+
) {}
26+
27+
add(f: Fraction) {
28+
if (f.d === this.d) {
29+
return new Fraction(this.n + f.n, this.d);
30+
}
31+
return new Fraction(this.n * f.d + this.d * f.n, this.d * f.d).simplify();
32+
}
33+
34+
sub(f: Fraction) {
35+
return this.add(f.mult(negativeOne));
36+
}
37+
38+
mult(f: Fraction) {
39+
return new Fraction(this.n * f.n, this.d * f.d).simplify();
40+
}
41+
42+
mod(f: Fraction) {
43+
return new Fraction((f.d * this.n) % (f.n * this.d), f.d * this.d);
44+
}
45+
46+
simplify() {
47+
const reduceBy = gcd(this.n, this.d);
48+
return new Fraction(this.n / reduceBy, this.d / reduceBy);
49+
}
50+
51+
valueOf() {
52+
return this.n / this.d;
53+
}
54+
55+
toFixed(digits?: number) {
56+
return this.valueOf().toFixed(digits);
57+
}
58+
59+
toString() {
60+
return this.valueOf().toString();
61+
}
62+
}
63+
64+
const negativeOne = new Fraction(-1);

0 commit comments

Comments
 (0)