Merge pull request #23 from MaxOhn/improve-hitresults
feat: improve generate hitresults
This commit is contained in:
+10
@@ -21,8 +21,18 @@ async-std = { version = "1.9", optional = true }
|
||||
tokio = { version = "1.2", optional = true, default-features = false, features = ["fs", "io-util"] }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1.3.1"
|
||||
tokio = { version = "1.2", default-features = false, features = ["fs", "rt"] }
|
||||
|
||||
[profile.test]
|
||||
opt-level = 2
|
||||
|
||||
[profile.test.package.proptest]
|
||||
opt-level = 3
|
||||
|
||||
[profile.test.package.rand_chacha]
|
||||
opt-level = 3
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
# document these features
|
||||
features = ["gradual"]
|
||||
|
||||
@@ -0,0 +1,649 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
AudioFilename: audio.mp3
|
||||
AudioLeadIn: 0
|
||||
PreviewTime: 9356
|
||||
Countdown: 0
|
||||
SampleSet: Soft
|
||||
StackLeniency: 0.7
|
||||
Mode: 3
|
||||
LetterboxInBreaks: 0
|
||||
SpecialStyle: 0
|
||||
WidescreenStoryboard: 1
|
||||
|
||||
[Editor]
|
||||
DistanceSpacing: 0.9
|
||||
BeatDivisor: 12
|
||||
GridSize: 4
|
||||
TimelineZoom: 2.899999
|
||||
|
||||
[Metadata]
|
||||
Title:Future Son
|
||||
TitleUnicode:Future Son
|
||||
Artist:ARCIEN
|
||||
ArtistUnicode:ARCIEN
|
||||
Creator:AncuL
|
||||
Version:Hard
|
||||
Source:
|
||||
Tags:edm electronic dance music bass
|
||||
BeatmapID:1638954
|
||||
BeatmapSetID:777881
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:8
|
||||
CircleSize:4
|
||||
OverallDifficulty:8
|
||||
ApproachRate:5
|
||||
SliderMultiplier:1.4
|
||||
SliderTickRate:1
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
0,0,"Konachan.com_-_198887_animal_barefoot_blonde_hair_bloodborne_doll_fish_flowers_goth-loli_hat_headdre (2).jpg",0,0
|
||||
//Break Periods
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Sound Samples
|
||||
|
||||
[TimingPoints]
|
||||
23,400,4,2,1,75,1,0
|
||||
|
||||
|
||||
[HitObjects]
|
||||
320,192,23,5,4,0:0:0:95:
|
||||
64,192,23,1,0,0:0:0:0:
|
||||
448,192,23,1,0,0:0:0:0:
|
||||
192,192,148,1,0,0:0:0:0:
|
||||
448,192,198,128,0,1223:0:0:0:0:
|
||||
192,192,1223,128,0,3223:0:0:0:0:
|
||||
320,192,3223,1,0,0:0:0:0:
|
||||
64,192,3289,128,0,4423:0:0:0:0:
|
||||
320,192,4423,128,0,6423:0:0:0:0:
|
||||
192,192,6423,1,0,0:0:0:0:
|
||||
64,192,6548,1,0,0:0:0:0:
|
||||
448,192,6598,128,0,7623:0:0:0:0:
|
||||
320,192,7623,128,0,9623:0:0:0:0:
|
||||
448,192,9623,1,0,0:0:0:0:
|
||||
64,192,9689,128,0,10823:0:0:0:0:
|
||||
192,192,10823,128,0,11623:0:0:0:0:
|
||||
320,192,11623,128,0,11823:0:0:0:0:
|
||||
192,192,11823,128,0,12023:0:0:0:0:
|
||||
448,192,12023,128,0,12223:0:0:0:0:
|
||||
64,192,12223,128,0,12423:0:0:0:0:
|
||||
320,192,12423,128,0,12623:0:0:0:0:
|
||||
192,192,12623,128,0,12823:0:0:0:0:
|
||||
64,192,12823,1,0,0:0:0:95:Kick.wav
|
||||
320,192,12823,1,4,0:0:0:95:
|
||||
448,192,12823,1,0,0:0:0:0:
|
||||
192,192,12923,1,0,0:0:0:0:
|
||||
320,192,13023,1,0,0:0:0:0:
|
||||
64,192,13023,1,0,0:0:0:0:
|
||||
448,192,13123,1,0,0:0:0:0:
|
||||
192,192,13223,1,0,0:0:0:0:
|
||||
64,192,13223,1,0,0:0:0:0:
|
||||
448,192,13323,1,0,0:0:0:0:
|
||||
192,192,13423,1,0,0:0:0:0:
|
||||
320,192,13423,1,0,0:0:0:0:
|
||||
64,192,13523,1,0,0:0:0:0:
|
||||
320,192,13623,1,0,0:0:0:85:Snare.wav
|
||||
192,192,13623,1,0,0:0:0:0:
|
||||
448,192,13623,128,0,14023:0:0:0:0:
|
||||
192,192,14023,1,4,0:0:0:95:
|
||||
64,192,14023,1,0,0:0:0:95:Kick.wav
|
||||
320,192,14023,1,0,0:0:0:0:
|
||||
448,192,14123,1,0,0:0:0:0:
|
||||
320,192,14223,1,0,0:0:0:0:
|
||||
64,192,14223,1,0,0:0:0:0:
|
||||
448,192,14323,1,0,0:0:0:0:
|
||||
192,192,14423,1,0,0:0:0:0:
|
||||
64,192,14423,1,0,0:0:0:0:
|
||||
320,192,14523,1,0,0:0:0:0:
|
||||
448,192,14623,1,0,0:0:0:0:
|
||||
192,192,14623,1,0,0:0:0:0:
|
||||
64,192,14723,1,0,0:0:0:0:
|
||||
320,192,14823,1,0,0:0:0:0:
|
||||
448,192,14823,1,0,0:0:0:0:
|
||||
64,192,14923,1,0,0:0:0:0:
|
||||
192,192,15023,1,0,0:0:0:0:
|
||||
320,192,15023,1,0,0:0:0:0:
|
||||
448,192,15123,1,0,0:0:0:0:
|
||||
320,192,15223,1,0,0:0:0:85:Snare.wav
|
||||
64,192,15223,1,0,0:0:0:0:
|
||||
192,192,15223,128,0,15623:0:0:0:0:
|
||||
64,192,15623,128,0,15723:0:0:0:0:
|
||||
320,192,15623,128,0,15723:0:0:0:0:
|
||||
64,192,15823,128,0,15923:0:0:0:0:
|
||||
448,192,15823,128,0,15923:0:0:0:0:
|
||||
448,192,16023,1,4,0:0:0:95:
|
||||
192,192,16023,1,0,0:0:0:95:Kick.wav
|
||||
320,192,16023,1,0,0:0:0:0:
|
||||
64,192,16123,1,0,0:0:0:0:
|
||||
192,192,16223,1,0,0:0:0:0:
|
||||
448,192,16223,1,0,0:0:0:0:
|
||||
320,192,16323,1,0,0:0:0:0:
|
||||
192,192,16423,1,0,0:0:0:0:
|
||||
64,192,16423,1,0,0:0:0:0:
|
||||
448,192,16523,1,0,0:0:0:0:
|
||||
320,192,16623,1,0,0:0:0:0:
|
||||
64,192,16623,1,0,0:0:0:0:
|
||||
192,192,16723,1,0,0:0:0:0:
|
||||
320,192,16823,1,0,0:0:0:85:Snare.wav
|
||||
448,192,16823,1,0,0:0:0:0:
|
||||
64,192,16823,1,0,0:0:0:0:
|
||||
320,192,17223,1,4,0:0:0:95:
|
||||
192,192,17223,1,0,0:0:0:95:Kick.wav
|
||||
64,192,17223,1,0,0:0:0:0:
|
||||
448,192,17323,1,0,0:0:0:0:
|
||||
320,192,17423,1,0,0:0:0:0:
|
||||
64,192,17423,1,0,0:0:0:0:
|
||||
448,192,17523,1,0,0:0:0:0:
|
||||
192,192,17623,1,0,0:0:0:0:
|
||||
64,192,17623,1,0,0:0:0:0:
|
||||
320,192,17723,1,0,0:0:0:0:
|
||||
448,192,17823,1,0,0:0:0:0:
|
||||
192,192,17823,1,0,0:0:0:0:
|
||||
64,192,17923,1,0,0:0:0:0:
|
||||
448,192,18023,1,0,0:0:0:0:
|
||||
320,192,18023,1,0,0:0:0:0:
|
||||
64,192,18123,1,0,0:0:0:0:
|
||||
192,192,18223,1,0,0:0:0:0:
|
||||
448,192,18223,1,0,0:0:0:0:
|
||||
64,192,18323,1,0,0:0:0:0:
|
||||
448,192,18423,1,0,0:0:0:85:Snare.wav
|
||||
192,192,18423,1,0,0:0:0:0:
|
||||
320,192,18423,128,0,18623:0:0:0:0:
|
||||
192,192,18623,128,0,18823:0:0:0:0:
|
||||
448,192,18823,128,0,18923:0:0:0:0:
|
||||
64,192,18823,128,0,18923:0:0:0:0:
|
||||
64,192,19023,128,0,19123:0:0:0:0:
|
||||
192,192,19023,128,0,19123:0:0:0:0:
|
||||
448,192,19223,1,4,0:0:0:95:
|
||||
320,192,19223,1,0,0:0:0:95:Kick.wav
|
||||
64,192,19223,1,0,0:0:0:0:
|
||||
192,192,19323,1,0,0:0:0:0:
|
||||
448,192,19423,1,0,0:0:0:0:
|
||||
64,192,19423,1,0,0:0:0:0:
|
||||
320,192,19523,1,0,0:0:0:0:
|
||||
192,192,19623,1,0,0:0:0:0:
|
||||
64,192,19623,1,0,0:0:0:0:
|
||||
448,192,19723,1,0,0:0:0:0:
|
||||
320,192,19823,1,0,0:0:0:0:
|
||||
192,192,19823,1,0,0:0:0:0:
|
||||
448,192,19923,1,0,0:0:0:0:
|
||||
320,192,20023,1,0,0:0:0:85:Snare.wav
|
||||
192,192,20023,1,0,0:0:0:0:
|
||||
64,192,20023,1,0,0:0:0:0:
|
||||
448,192,20223,1,0,0:0:0:95:Kick.wav
|
||||
192,192,20223,1,0,0:0:0:0:
|
||||
448,192,20423,1,4,0:0:0:95:
|
||||
320,192,20423,1,0,0:0:0:95:Kick.wav
|
||||
64,192,20423,1,0,0:0:0:0:
|
||||
192,192,20523,1,0,0:0:0:0:
|
||||
64,192,20623,1,0,0:0:0:0:
|
||||
320,192,20623,1,0,0:0:0:0:
|
||||
448,192,20723,1,0,0:0:0:0:
|
||||
320,192,20823,1,0,0:0:0:0:
|
||||
192,192,20823,1,0,0:0:0:0:
|
||||
448,192,20923,1,0,0:0:0:0:
|
||||
320,192,21023,1,0,0:0:0:0:
|
||||
64,192,21023,1,0,0:0:0:0:
|
||||
192,192,21123,1,0,0:0:0:0:
|
||||
448,192,21223,1,0,0:0:0:0:
|
||||
64,192,21223,128,0,21623:0:0:0:0:
|
||||
320,192,21423,1,0,0:0:0:0:
|
||||
320,192,21623,1,0,0:0:0:85:Snare.wav
|
||||
448,192,21623,1,0,0:0:0:0:
|
||||
192,192,21623,1,0,0:0:0:0:
|
||||
192,192,22023,128,0,22123:0:0:0:0:
|
||||
64,192,22023,128,0,22123:0:0:0:0:
|
||||
192,192,22223,128,0,22323:0:0:0:0:
|
||||
448,192,22223,128,0,22323:0:0:0:0:
|
||||
448,192,22423,1,4,0:0:0:95:
|
||||
320,192,22423,128,0,23023:0:0:0:95:Kick.wav
|
||||
64,192,22423,1,0,0:0:0:0:
|
||||
192,192,22623,1,0,0:0:0:0:
|
||||
64,192,22823,1,0,0:0:0:0:
|
||||
448,192,23023,128,0,23623:0:0:0:0:
|
||||
192,192,23023,1,0,0:0:0:0:
|
||||
192,192,23223,1,0,0:0:0:85:Snare.wav
|
||||
64,192,23223,1,0,0:0:0:0:
|
||||
320,192,23223,1,0,0:0:0:0:
|
||||
64,192,23623,128,4,24823:0:0:0:95:
|
||||
192,192,23623,1,0,0:0:0:95:Kick.wav
|
||||
320,192,23623,1,0,0:0:0:0:
|
||||
448,192,23823,1,0,0:0:0:0:
|
||||
320,192,24023,1,0,0:0:0:0:
|
||||
448,192,24223,1,0,0:0:0:0:
|
||||
192,192,24423,1,0,0:0:0:0:
|
||||
320,192,24623,1,0,0:0:0:0:
|
||||
320,192,24823,1,0,0:0:0:85:Snare.wav
|
||||
192,192,24823,1,0,0:0:0:0:
|
||||
448,192,24823,128,0,25023:0:0:0:0:
|
||||
320,192,25023,128,0,25223:0:0:0:0:
|
||||
192,192,25223,128,0,25323:0:0:0:0:
|
||||
448,192,25223,128,0,25323:0:0:0:0:
|
||||
448,192,25423,128,0,25523:0:0:0:0:
|
||||
64,192,25423,128,0,25523:0:0:0:0:
|
||||
64,192,25623,1,0,0:0:0:95:Kick.wav
|
||||
192,192,25623,1,4,0:0:0:95:
|
||||
448,192,25623,1,0,0:0:0:0:
|
||||
320,192,25723,1,0,0:0:0:0:
|
||||
448,192,25823,1,0,0:0:0:0:
|
||||
192,192,25823,1,0,0:0:0:0:
|
||||
64,192,25923,1,0,0:0:0:0:
|
||||
320,192,26023,1,0,0:0:0:0:
|
||||
192,192,26023,1,0,0:0:0:0:
|
||||
448,192,26123,1,0,0:0:0:0:
|
||||
192,192,26223,1,0,0:0:0:0:
|
||||
64,192,26223,1,0,0:0:0:0:
|
||||
320,192,26323,1,0,0:0:0:0:
|
||||
448,192,26423,1,0,0:0:0:85:Snare.wav
|
||||
64,192,26423,1,0,0:0:0:0:
|
||||
192,192,26423,128,0,26823:0:0:0:0:
|
||||
320,192,26823,1,4,0:0:0:95:
|
||||
64,192,26823,1,0,0:0:0:95:Kick.wav
|
||||
448,192,26823,1,0,0:0:0:0:
|
||||
192,192,26923,1,0,0:0:0:0:
|
||||
448,192,27023,1,0,0:0:0:0:
|
||||
320,192,27023,1,0,0:0:0:0:
|
||||
64,192,27123,1,0,0:0:0:0:
|
||||
192,192,27223,1,0,0:0:0:0:
|
||||
320,192,27223,1,0,0:0:0:0:
|
||||
448,192,27323,1,0,0:0:0:0:
|
||||
320,192,27423,1,0,0:0:0:0:
|
||||
192,192,27423,1,0,0:0:0:0:
|
||||
64,192,27523,1,0,0:0:0:0:
|
||||
192,192,27623,1,0,0:0:0:0:
|
||||
448,192,27623,1,0,0:0:0:0:
|
||||
320,192,27723,1,0,0:0:0:0:
|
||||
192,192,27823,1,0,0:0:0:0:
|
||||
64,192,27823,1,0,0:0:0:0:
|
||||
320,192,27923,1,0,0:0:0:0:
|
||||
192,192,28023,1,0,0:0:0:85:Snare.wav
|
||||
64,192,28023,1,0,0:0:0:0:
|
||||
448,192,28023,128,0,28423:0:0:0:0:
|
||||
64,192,28423,128,0,28523:0:0:0:0:
|
||||
320,192,28423,128,0,28523:0:0:0:0:
|
||||
448,192,28623,128,0,28723:0:0:0:0:
|
||||
64,192,28623,128,0,28723:0:0:0:0:
|
||||
448,192,28823,1,4,0:0:0:95:
|
||||
64,192,28823,1,0,0:0:0:95:Kick.wav
|
||||
192,192,28823,1,0,0:0:0:0:
|
||||
320,192,28923,1,0,0:0:0:0:
|
||||
192,192,29023,1,0,0:0:0:0:
|
||||
64,192,29023,1,0,0:0:0:0:
|
||||
448,192,29123,1,0,0:0:0:0:
|
||||
320,192,29223,1,0,0:0:0:0:
|
||||
64,192,29223,1,0,0:0:0:0:
|
||||
192,192,29323,1,0,0:0:0:0:
|
||||
320,192,29423,1,0,0:0:0:0:
|
||||
448,192,29423,1,0,0:0:0:0:
|
||||
64,192,29523,1,0,0:0:0:0:
|
||||
192,192,29623,1,0,0:0:0:85:Snare.wav
|
||||
448,192,29623,1,0,0:0:0:0:
|
||||
320,192,29623,1,0,0:0:0:0:
|
||||
320,192,30023,1,4,0:0:0:95:
|
||||
448,192,30023,1,0,0:0:0:95:Kick.wav
|
||||
64,192,30023,1,0,0:0:0:0:
|
||||
192,192,30123,1,0,0:0:0:0:
|
||||
448,192,30223,1,0,0:0:0:0:
|
||||
64,192,30223,1,0,0:0:0:0:
|
||||
192,192,30323,1,0,0:0:0:0:
|
||||
320,192,30423,1,0,0:0:0:0:
|
||||
448,192,30423,1,0,0:0:0:0:
|
||||
64,192,30523,1,0,0:0:0:0:
|
||||
448,192,30623,1,0,0:0:0:0:
|
||||
192,192,30623,1,0,0:0:0:0:
|
||||
320,192,30723,1,0,0:0:0:0:
|
||||
192,192,30823,1,0,0:0:0:0:
|
||||
64,192,30823,1,0,0:0:0:0:
|
||||
320,192,30923,1,0,0:0:0:0:
|
||||
192,192,31023,1,0,0:0:0:0:
|
||||
448,192,31023,1,0,0:0:0:0:
|
||||
64,192,31123,1,0,0:0:0:0:
|
||||
448,192,31223,1,0,0:0:0:85:Snare.wav
|
||||
192,192,31223,1,0,0:0:0:0:
|
||||
320,192,31223,128,0,31423:0:0:0:0:
|
||||
192,192,31423,128,0,31623:0:0:0:0:
|
||||
64,192,31623,128,0,31723:0:0:0:0:
|
||||
320,192,31623,128,0,31723:0:0:0:0:
|
||||
320,192,31823,128,0,31923:0:0:0:0:
|
||||
192,192,31823,128,0,31923:0:0:0:0:
|
||||
448,192,32023,1,4,0:0:0:95:
|
||||
320,192,32023,1,0,0:0:0:95:Kick.wav
|
||||
64,192,32023,1,0,0:0:0:0:
|
||||
192,192,32123,1,0,0:0:0:0:
|
||||
448,192,32223,1,0,0:0:0:0:
|
||||
64,192,32223,1,0,0:0:0:0:
|
||||
320,192,32323,1,0,0:0:0:0:
|
||||
192,192,32423,1,0,0:0:0:0:
|
||||
64,192,32423,1,0,0:0:0:0:
|
||||
448,192,32523,1,0,0:0:0:0:
|
||||
320,192,32623,1,0,0:0:0:0:
|
||||
192,192,32623,1,0,0:0:0:0:
|
||||
64,192,32723,1,0,0:0:0:0:
|
||||
320,192,32823,1,0,0:0:0:85:Snare.wav
|
||||
448,192,32823,1,0,0:0:0:0:
|
||||
192,192,32823,1,0,0:0:0:0:
|
||||
192,192,33023,1,0,0:0:0:95:Kick.wav
|
||||
64,192,33023,1,0,0:0:0:0:
|
||||
448,192,33223,1,4,0:0:0:95:
|
||||
320,192,33223,1,0,0:0:0:95:Kick.wav
|
||||
64,192,33223,1,0,0:0:0:0:
|
||||
192,192,33323,1,0,0:0:0:0:
|
||||
320,192,33423,1,0,0:0:0:0:
|
||||
64,192,33423,1,0,0:0:0:0:
|
||||
448,192,33523,1,0,0:0:0:0:
|
||||
64,192,33623,1,0,0:0:0:0:
|
||||
320,192,33623,1,0,0:0:0:0:
|
||||
192,192,33723,1,0,0:0:0:0:
|
||||
320,192,33823,1,0,0:0:0:0:
|
||||
448,192,33823,1,0,0:0:0:0:
|
||||
64,192,33923,1,0,0:0:0:0:
|
||||
320,192,34023,128,0,34423:0:0:0:0:
|
||||
192,192,34023,1,0,0:0:0:0:
|
||||
448,192,34223,1,0,0:0:0:0:
|
||||
448,192,34423,1,0,0:0:0:85:Snare.wav
|
||||
192,192,34423,1,0,0:0:0:0:
|
||||
64,192,34423,1,0,0:0:0:0:
|
||||
192,192,34823,128,0,34923:0:0:0:0:
|
||||
64,192,34823,128,0,34923:0:0:0:0:
|
||||
448,192,34923,1,0,0:0:0:0:
|
||||
64,192,35023,128,0,35123:0:0:0:0:
|
||||
320,192,35023,128,0,35123:0:0:0:0:
|
||||
192,192,35223,128,4,35823:0:0:0:95:
|
||||
320,192,35223,1,0,0:0:0:95:Kick.wav
|
||||
64,192,35223,1,0,0:0:0:0:
|
||||
448,192,35423,1,0,0:0:0:0:
|
||||
320,192,35623,1,0,0:0:0:0:
|
||||
448,192,35823,128,0,36423:0:0:0:0:
|
||||
64,192,35823,1,0,0:0:0:0:
|
||||
64,192,36023,1,0,0:0:0:85:Snare.wav
|
||||
192,192,36023,1,0,0:0:0:0:
|
||||
320,192,36023,1,0,0:0:0:0:
|
||||
64,192,36423,128,4,37623:0:0:0:95:
|
||||
192,192,36423,1,0,0:0:0:95:Kick.wav
|
||||
320,192,36423,1,0,0:0:0:0:
|
||||
448,192,36623,1,0,0:0:0:0:
|
||||
192,192,36823,1,0,0:0:0:0:
|
||||
320,192,37023,1,0,0:0:0:0:
|
||||
192,192,37223,1,0,0:0:0:0:
|
||||
448,192,37423,1,0,0:0:0:0:
|
||||
192,192,37623,1,0,0:0:0:85:Snare.wav
|
||||
320,192,37623,1,0,0:0:0:0:
|
||||
448,192,37623,128,0,37823:0:0:0:0:
|
||||
320,192,37823,128,0,38023:0:0:0:0:
|
||||
192,192,38023,128,0,38123:0:0:0:0:
|
||||
448,192,38023,128,0,38123:0:0:0:0:
|
||||
192,192,38223,128,0,38323:0:0:0:0:
|
||||
64,192,38223,128,0,38323:0:0:0:0:
|
||||
64,192,38423,1,0,0:0:0:95:Kick.wav
|
||||
320,192,38423,1,4,0:0:0:95:
|
||||
448,192,38423,1,0,0:0:0:0:
|
||||
192,192,38523,1,0,0:0:0:0:
|
||||
320,192,38623,1,0,0:0:0:0:
|
||||
64,192,38623,1,0,0:0:0:0:
|
||||
64,192,38823,1,0,0:0:0:0:
|
||||
320,192,38823,1,0,0:0:0:0:
|
||||
64,192,39023,1,0,0:0:0:0:
|
||||
320,192,39023,1,0,0:0:0:0:
|
||||
64,192,39223,1,0,0:0:0:85:Snare.wav
|
||||
192,192,39223,1,0,0:0:0:0:
|
||||
448,192,39223,128,0,39623:0:0:0:0:
|
||||
320,192,39623,1,4,0:0:0:95:
|
||||
192,192,39623,1,0,0:0:0:95:Kick.wav
|
||||
64,192,39623,1,0,0:0:0:0:
|
||||
448,192,39723,1,0,0:0:0:0:
|
||||
320,192,39823,1,0,0:0:0:0:
|
||||
192,192,39823,1,0,0:0:0:0:
|
||||
192,192,40023,1,0,0:0:0:0:
|
||||
320,192,40023,1,0,0:0:0:0:
|
||||
192,192,40223,1,0,0:0:0:0:
|
||||
320,192,40223,1,0,0:0:0:0:
|
||||
192,192,40423,1,0,0:0:0:0:
|
||||
320,192,40423,1,0,0:0:0:0:
|
||||
192,192,40623,1,0,0:0:0:0:
|
||||
320,192,40623,1,0,0:0:0:0:
|
||||
448,192,40823,1,0,0:0:0:85:Snare.wav
|
||||
192,192,40823,1,0,0:0:0:0:
|
||||
64,192,40823,128,0,41223:0:0:0:0:
|
||||
192,192,41223,128,0,41323:0:0:0:0:
|
||||
320,192,41223,128,0,41323:0:0:0:0:
|
||||
320,192,41423,128,0,41523:0:0:0:0:
|
||||
448,192,41423,128,0,41523:0:0:0:0:
|
||||
192,192,41623,1,4,0:0:0:95:
|
||||
448,192,41623,1,0,0:0:0:95:Kick.wav
|
||||
320,192,41623,1,0,0:0:0:0:
|
||||
64,192,41723,1,0,0:0:0:0:
|
||||
192,192,41823,1,0,0:0:0:0:
|
||||
448,192,41823,1,0,0:0:0:0:
|
||||
192,192,42023,1,0,0:0:0:0:
|
||||
448,192,42023,1,0,0:0:0:0:
|
||||
192,192,42223,1,0,0:0:0:0:
|
||||
448,192,42223,1,0,0:0:0:0:
|
||||
448,192,42423,1,0,0:0:0:85:Snare.wav
|
||||
64,192,42423,1,0,0:0:0:0:
|
||||
320,192,42423,1,0,0:0:0:0:
|
||||
448,192,42823,1,4,0:0:0:95:
|
||||
192,192,42823,1,0,0:0:0:95:Kick.wav
|
||||
64,192,42823,1,0,0:0:0:0:
|
||||
320,192,42923,1,0,0:0:0:0:
|
||||
64,192,43023,1,0,0:0:0:0:
|
||||
448,192,43023,1,0,0:0:0:0:
|
||||
448,192,43223,1,0,0:0:0:0:
|
||||
64,192,43223,1,0,0:0:0:0:
|
||||
64,192,43423,1,0,0:0:0:0:
|
||||
448,192,43423,1,0,0:0:0:0:
|
||||
448,192,43623,1,0,0:0:0:0:
|
||||
64,192,43623,1,0,0:0:0:0:
|
||||
64,192,43823,1,0,0:0:0:0:
|
||||
448,192,43823,1,0,0:0:0:0:
|
||||
64,192,44023,1,0,0:0:0:85:Snare.wav
|
||||
192,192,44023,1,0,0:0:0:0:
|
||||
320,192,44023,128,0,44223:0:0:0:0:
|
||||
192,192,44223,128,0,44423:0:0:0:0:
|
||||
64,192,44423,128,0,44523:0:0:0:0:
|
||||
320,192,44423,128,0,44523:0:0:0:0:
|
||||
448,192,44623,128,0,44723:0:0:0:0:
|
||||
320,192,44623,128,0,44723:0:0:0:0:
|
||||
448,192,44823,1,4,0:0:0:95:
|
||||
64,192,44823,1,0,0:0:0:95:Kick.wav
|
||||
320,192,44823,1,0,0:0:0:0:
|
||||
192,192,44923,1,0,0:0:0:0:
|
||||
448,192,45023,1,0,0:0:0:0:
|
||||
64,192,45023,1,0,0:0:0:0:
|
||||
64,192,45223,1,0,0:0:0:0:
|
||||
448,192,45223,1,0,0:0:0:0:
|
||||
448,192,45423,1,0,0:0:0:0:
|
||||
64,192,45423,1,0,0:0:0:0:
|
||||
64,192,45623,1,0,0:0:0:85:Snare.wav
|
||||
320,192,45623,1,0,0:0:0:0:
|
||||
192,192,45623,1,0,0:0:0:0:
|
||||
448,192,45823,1,0,0:0:0:95:Kick.wav
|
||||
192,192,45823,1,0,0:0:0:0:
|
||||
448,192,46023,1,4,0:0:0:95:
|
||||
320,192,46023,1,0,0:0:0:95:Kick.wav
|
||||
64,192,46023,1,0,0:0:0:0:
|
||||
192,192,46123,1,0,0:0:0:0:
|
||||
320,192,46223,1,0,0:0:0:0:
|
||||
64,192,46223,1,0,0:0:0:0:
|
||||
64,192,46423,1,0,0:0:0:0:
|
||||
320,192,46423,1,0,0:0:0:0:
|
||||
64,192,46623,1,0,0:0:0:0:
|
||||
320,192,46623,1,0,0:0:0:0:
|
||||
320,192,46823,1,0,0:0:0:0:
|
||||
64,192,46823,1,0,0:0:0:0:
|
||||
64,192,47023,1,0,0:0:0:0:
|
||||
320,192,47023,1,0,0:0:0:0:
|
||||
320,192,47223,1,0,0:0:0:85:Snare.wav
|
||||
192,192,47223,1,0,0:0:0:0:
|
||||
448,192,47223,1,0,0:0:0:0:
|
||||
192,192,47623,128,0,47723:0:0:0:0:
|
||||
448,192,47623,128,0,47723:0:0:0:0:
|
||||
192,192,47823,128,0,47923:0:0:0:0:
|
||||
320,192,47823,128,0,47923:0:0:0:0:
|
||||
320,192,48023,1,4,0:0:0:95:
|
||||
448,192,48023,1,0,0:0:0:95:Kick.wav
|
||||
64,192,48023,128,0,48623:0:0:0:0:
|
||||
192,192,48223,1,0,0:0:0:0:
|
||||
192,192,48423,1,0,0:0:0:0:
|
||||
448,192,48623,128,0,49223:0:0:0:0:
|
||||
320,192,48623,1,0,0:0:0:0:
|
||||
320,192,48823,1,0,0:0:0:85:Snare.wav
|
||||
192,192,48823,1,0,0:0:0:0:
|
||||
64,192,48823,1,0,0:0:0:0:
|
||||
192,192,49223,128,4,50423:0:0:0:95:
|
||||
320,192,49223,1,0,0:0:0:95:Kick.wav
|
||||
64,192,49223,1,0,0:0:0:0:
|
||||
448,192,49423,1,0,0:0:0:0:
|
||||
448,192,49623,1,0,0:0:0:0:
|
||||
320,192,49823,1,0,0:0:0:0:
|
||||
320,192,50023,1,0,0:0:0:0:
|
||||
64,192,50223,1,0,0:0:0:0:
|
||||
64,192,50423,1,0,0:0:0:85:Snare.wav
|
||||
320,192,50423,1,0,0:0:0:0:
|
||||
448,192,50423,128,0,50623:0:0:0:0:
|
||||
320,192,50623,128,0,50823:0:0:0:0:
|
||||
192,192,50823,128,0,50923:0:0:0:0:
|
||||
448,192,50823,128,0,50923:0:0:0:0:
|
||||
64,192,51023,128,0,51123:0:0:0:0:
|
||||
192,192,51023,128,0,51123:0:0:0:0:
|
||||
64,192,51223,1,0,0:0:0:95:Kick.wav
|
||||
320,192,51223,1,4,0:0:0:95:
|
||||
448,192,51223,1,0,0:0:0:0:
|
||||
192,192,51323,1,0,0:0:0:0:
|
||||
448,192,51423,1,0,0:0:0:0:
|
||||
64,192,51423,1,0,0:0:0:0:
|
||||
320,192,51623,1,0,0:0:0:0:
|
||||
64,192,51623,128,0,51823:0:0:0:0:
|
||||
320,192,51823,1,0,0:0:0:0:
|
||||
448,192,51823,1,0,0:0:0:0:
|
||||
192,192,52023,1,0,0:0:0:85:Snare.wav
|
||||
64,192,52023,1,0,0:0:0:0:
|
||||
448,192,52023,128,0,52423:0:0:0:0:
|
||||
320,192,52423,1,4,0:0:0:95:
|
||||
192,192,52423,1,0,0:0:0:95:Kick.wav
|
||||
64,192,52423,1,0,0:0:0:0:
|
||||
448,192,52523,1,0,0:0:0:0:
|
||||
320,192,52623,1,0,0:0:0:0:
|
||||
64,192,52623,1,0,0:0:0:0:
|
||||
64,192,52823,1,0,0:0:0:0:
|
||||
448,192,52823,1,0,0:0:0:0:
|
||||
448,192,53023,1,0,0:0:0:0:
|
||||
192,192,53023,1,0,0:0:0:0:
|
||||
320,192,53223,1,0,0:0:0:0:
|
||||
192,192,53223,128,0,53423:0:0:0:0:
|
||||
320,192,53423,1,0,0:0:0:0:
|
||||
448,192,53423,1,0,0:0:0:0:
|
||||
448,192,53623,1,0,0:0:0:85:Snare.wav
|
||||
192,192,53623,1,0,0:0:0:0:
|
||||
64,192,53623,1,0,0:0:0:0:
|
||||
320,192,53756,1,0,0:0:0:0:
|
||||
192,192,53889,1,0,0:0:0:0:
|
||||
320,192,54023,128,0,54123:0:0:0:0:
|
||||
448,192,54023,128,0,54123:0:0:0:0:
|
||||
448,192,54223,128,0,54323:0:0:0:0:
|
||||
64,192,54223,128,0,54323:0:0:0:0:
|
||||
448,192,54423,1,4,0:0:0:95:
|
||||
320,192,54423,1,0,0:0:0:95:Kick.wav
|
||||
64,192,54423,1,0,0:0:0:0:
|
||||
192,192,54523,1,0,0:0:0:0:
|
||||
448,192,54623,1,0,0:0:0:0:
|
||||
320,192,54623,1,0,0:0:0:0:
|
||||
64,192,54823,1,0,0:0:0:0:
|
||||
448,192,54823,128,0,55023:0:0:0:0:
|
||||
64,192,55023,1,0,0:0:0:0:
|
||||
320,192,55023,1,0,0:0:0:0:
|
||||
192,192,55223,1,0,0:0:0:85:Snare.wav
|
||||
320,192,55223,1,0,0:0:0:0:
|
||||
448,192,55223,1,0,0:0:0:0:
|
||||
448,192,55623,1,4,0:0:0:95:
|
||||
192,192,55623,1,0,0:0:0:95:Kick.wav
|
||||
64,192,55623,1,0,0:0:0:0:
|
||||
320,192,55723,1,0,0:0:0:0:
|
||||
64,192,55823,1,0,0:0:0:0:
|
||||
448,192,55823,1,0,0:0:0:0:
|
||||
448,192,56023,1,0,0:0:0:0:
|
||||
192,192,56023,1,0,0:0:0:0:
|
||||
192,192,56223,1,0,0:0:0:0:
|
||||
64,192,56223,1,0,0:0:0:0:
|
||||
320,192,56423,1,0,0:0:0:0:
|
||||
64,192,56423,128,0,56623:0:0:0:0:
|
||||
320,192,56623,1,0,0:0:0:0:
|
||||
448,192,56623,1,0,0:0:0:0:
|
||||
192,192,56823,1,0,0:0:0:85:Snare.wav
|
||||
64,192,56823,1,0,0:0:0:0:
|
||||
448,192,56823,128,0,57023:0:0:0:0:
|
||||
320,192,57023,128,0,57223:0:0:0:0:
|
||||
192,192,57223,128,0,57323:0:0:0:0:
|
||||
64,192,57223,128,0,57323:0:0:0:0:
|
||||
64,192,57423,128,0,57523:0:0:0:0:
|
||||
448,192,57423,128,0,57523:0:0:0:0:
|
||||
64,192,57623,1,4,0:0:0:95:
|
||||
320,192,57623,1,0,0:0:0:95:Kick.wav
|
||||
192,192,57623,1,0,0:0:0:0:
|
||||
448,192,57723,1,0,0:0:0:0:
|
||||
64,192,57823,1,0,0:0:0:0:
|
||||
192,192,57823,1,0,0:0:0:0:
|
||||
192,192,58023,1,0,0:0:0:0:
|
||||
320,192,58023,128,0,58223:0:0:0:0:
|
||||
64,192,58223,1,0,0:0:0:0:
|
||||
448,192,58223,1,0,0:0:0:0:
|
||||
320,192,58423,1,0,0:0:0:85:Snare.wav
|
||||
192,192,58423,1,0,0:0:0:0:
|
||||
448,192,58423,1,0,0:0:0:0:
|
||||
192,192,58623,1,0,0:0:0:95:Kick.wav
|
||||
64,192,58623,1,0,0:0:0:0:
|
||||
64,192,58823,1,4,0:0:0:95:
|
||||
448,192,58823,1,0,0:0:0:95:Kick.wav
|
||||
320,192,58823,1,0,0:0:0:0:
|
||||
192,192,58923,1,0,0:0:0:0:
|
||||
448,192,59023,1,0,0:0:0:0:
|
||||
64,192,59023,1,0,0:0:0:0:
|
||||
320,192,59223,1,0,0:0:0:0:
|
||||
448,192,59223,1,0,0:0:0:0:
|
||||
320,192,59423,1,0,0:0:0:0:
|
||||
64,192,59423,1,0,0:0:0:0:
|
||||
448,192,59623,1,0,0:0:0:0:
|
||||
64,192,59623,128,0,59823:0:0:0:0:
|
||||
448,192,59823,1,0,0:0:0:0:
|
||||
320,192,59823,1,0,0:0:0:0:
|
||||
320,192,60023,1,0,0:0:0:85:Snare.wav
|
||||
192,192,60023,1,0,0:0:0:0:
|
||||
64,192,60023,1,0,0:0:0:0:
|
||||
448,192,60423,128,0,60523:0:0:0:0:
|
||||
192,192,60423,128,0,60523:0:0:0:0:
|
||||
64,192,60623,128,0,60723:0:0:0:0:
|
||||
192,192,60623,128,0,60723:0:0:0:0:
|
||||
320,192,60823,128,4,61423:0:0:0:95:
|
||||
448,192,60823,1,0,0:0:0:95:Kick.wav
|
||||
64,192,60823,1,0,0:0:0:0:
|
||||
192,192,61023,1,0,0:0:0:0:
|
||||
192,192,61223,1,0,0:0:0:0:
|
||||
448,192,61423,128,0,62023:0:0:0:0:
|
||||
64,192,61423,1,0,0:0:0:0:
|
||||
64,192,61623,1,0,0:0:0:85:Snare.wav
|
||||
192,192,61623,1,0,0:0:0:0:
|
||||
320,192,61623,1,0,0:0:0:0:
|
||||
64,192,62023,128,4,63223:0:0:0:95:
|
||||
192,192,62023,1,0,0:0:0:95:Kick.wav
|
||||
320,192,62023,1,0,0:0:0:0:
|
||||
448,192,62223,1,0,0:0:0:0:
|
||||
448,192,62423,1,0,0:0:0:0:
|
||||
192,192,62623,1,0,0:0:0:0:
|
||||
192,192,62823,1,0,0:0:0:0:
|
||||
320,192,63023,1,0,0:0:0:0:
|
||||
192,192,63223,1,0,0:0:0:85:Snare.wav
|
||||
448,192,63223,1,0,0:0:0:0:
|
||||
320,192,63223,128,0,63423:0:0:0:0:
|
||||
192,192,63423,128,0,63623:0:0:0:0:
|
||||
64,192,63623,128,0,63723:0:0:0:0:
|
||||
448,192,63623,128,0,63723:0:0:0:0:
|
||||
320,192,63823,128,0,63923:0:0:0:0:
|
||||
192,192,63823,128,0,63923:0:0:0:0:
|
||||
448,192,64023,1,4,0:0:0:95:
|
||||
320,192,64023,1,0,0:0:0:95:Kick.wav
|
||||
64,192,64023,1,0,0:0:0:0:
|
||||
-5055
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc c75ef0e1156e1fb7c3eec623d7bb3ff00373825b76fe5a699be004eabfab596f # shrinks to acc = 0.0, n_fruits = None, n_droplets = None, n_tiny_droplets = None, n_tiny_droplet_misses = None, n_misses = None
|
||||
cc 7bf5d798492945aadd68cf23a1d25c530171b4e396be8c7c8b7b5273e3fb5a75 # shrinks to acc = 0.0, n_fruits = Some(0), n_droplets = None, n_tiny_droplets = None, n_tiny_droplet_misses = None, n_misses = None
|
||||
cc 3b895b7f5cf43539ab1536bb51a0212bc354b0de7959cd0f548c8eb19085f99c # shrinks to acc = 0.9572076425636069, n_fruits = None, n_droplets = None, n_tiny_droplets = None, n_tiny_droplet_misses = None, n_misses = Some(2)
|
||||
cc fffc517d7d7aeadfd6e7e9514cdfe04239233e7fb9c35dca2a6fe47b6bb3c215 # shrinks to acc = 0.0, n_fruits = None, n_droplets = Some(0), n_tiny_droplets = None, n_tiny_droplet_misses = None, n_misses = None
|
||||
cc fdeec58e1c568f52970c0a74a824ab6cf0300892af19f4bd3826f2af19a38d08 # shrinks to acc = 0.0, n_fruits = Some(10), n_droplets = None, n_tiny_droplets = None, n_tiny_droplet_misses = None, n_misses = Some(721)
|
||||
cc dd03c40dec8e5efd5031fca53c2a1804e9537a3e864b0c21136169b8d411ec23 # shrinks to acc = 0.0, n_fruits = None, n_droplets = None, n_tiny_droplets = Some(0), n_tiny_droplet_misses = Some(0), n_misses = None
|
||||
cc dd7bf3c50e5165f9e2f265336af23a38c78a0bfafd9b1dc54d32f50dfeb84c79 # shrinks to acc = 0.9407007157860147, n_fruits = Some(671), n_droplets = Some(0), n_tiny_droplets = None, n_tiny_droplet_misses = None, n_misses = Some(61)
|
||||
cc 6e78f69d0a44ad58d5a2408cbadf7913cc3b77934b6c16eb1f41587fc151545d # shrinks to acc = 0.0, n_fruits = None, n_droplets = None, n_tiny_droplets = None, n_tiny_droplet_misses = Some(292), n_misses = None
|
||||
@@ -0,0 +1,21 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc f43e82d27eb4d8c4b748c7b83a8fb3b6160daa5dc5cb863856a15e6480a325d0 # shrinks to acc = 0.0, n320 = None, n300 = None, n200 = None, n100 = None, n50 = None, n_misses = None, best_case = false
|
||||
cc 0bb9723c2bb7e394cd0f105b1c775d9f233885477b14ecc0b5167282b402a310 # shrinks to acc = 0.190179679226917, n320 = None, n300 = None, n200 = None, n100 = None, n50 = None, n_misses = None, best_case = false
|
||||
cc af7f41af420babe5dd8de26efc4831957214ee21d6991ded489a412bdcd8818a # shrinks to acc = 0.0, n320 = None, n300 = None, n200 = None, n100 = None, n50 = Some(0), n_misses = Some(0), best_case = true
|
||||
cc a254712f60935104af638a970cf19c33ea69b442bf5bde67f331b5bb8e65b42b # shrinks to acc = 0.0, n320 = None, n300 = Some(1), n200 = None, n100 = None, n50 = None, n_misses = None, best_case = false
|
||||
cc 43e15d089c0bf50dde4e2afa78e58955fa5c3826abd856cff5bdad8cc9b00e1d # shrinks to acc = 0.7251029619347622, n320 = None, n300 = None, n200 = Some(233), n100 = None, n50 = None, n_misses = None, best_case = false
|
||||
cc cc91915aa4df7793cc0caf070575495c40f781b6d9769824cf87a640523cbb7d # shrinks to acc = 0.0, n320 = None, n300 = Some(1), n200 = None, n100 = None, n50 = None, n_misses = None, best_case = true
|
||||
cc 4657290897894a74501af88cadbc328ee89ca52e7c7e2f0823b5fe6fdbb6e428 # shrinks to acc = 0.0, n320 = Some(1), n300 = None, n200 = Some(290), n100 = None, n50 = None, n_misses = Some(688), best_case = false
|
||||
cc c6c864a257c332ce9b9490ac45cece5ef0aaa59b78e333664c05ca38de0a4cf6 # shrinks to acc = 0.0, n320 = Some(374), n300 = None, n200 = None, n100 = Some(605), n50 = None, n_misses = None, best_case = false
|
||||
cc 2f2e0a687f294c30e4e055eaa860452a165cc4f896a3e71346ebbdb0f1e18e0d # shrinks to acc = 0.8763364816127952, n320 = None, n300 = None, n200 = None, n100 = None, n50 = Some(1), n_misses = Some(121), best_case = false
|
||||
cc b5c21ba35da112003a5e1062b091e93381ab3d6d227c5f0cdc7cce0669747152 # shrinks to acc = 0.4032924787038701, n320 = None, n300 = None, n200 = None, n100 = None, n50 = Some(0), n_misses = None, best_case = true
|
||||
cc 049e6f4db0c2cd283843a6d777d7fbf3001e07eb39eb8ae23338e6f371fffd24 # shrinks to acc = 0.9038578552279879, n320 = None, n300 = Some(0), n200 = None, n100 = None, n50 = None, n_misses = None, best_case = false
|
||||
cc 8067939c5014c26f15a25c1255fbc3452f42b159be3b1271029ba577a7da5af3 # shrinks to acc = 0.6801960172852813, n320 = Some(0), n300 = None, n200 = None, n100 = Some(0), n50 = None, n_misses = None, best_case = false
|
||||
cc 0924f546edd96e86ee6163aa08dd6b77fc5a6219be6766a5b5d3b6d0caa80145 # shrinks to acc = 0.5630003027452851, n320 = None, n300 = None, n200 = Some(4), n100 = None, n50 = Some(37), n_misses = None, best_case = false
|
||||
cc a3c828166cc0a217e7a4e2d3592372209f886b6ea78a3da40664f3201df5a49b # shrinks to acc = 0.0, n320 = Some(0), n300 = Some(66), n200 = None, n100 = None, n50 = None, n_misses = Some(529), best_case = false
|
||||
cc 1d15772c531f04fc3c0cc28ce76cf219ce7db950f36d3688bc01714a72fb8e5a # shrinks to acc = 0.0, n320 = None, n300 = None, n200 = None, n100 = None, n50 = None, n_misses = Some(595), best_case = false
|
||||
@@ -0,0 +1,14 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc aee5b74b9ef816122b8e9746e1666c6f1ee226bdf4fa6074dc561ead81e687d6 # shrinks to acc = 0.0, combo = None, n300 = None, n100 = None, n50 = None, n_misses = None, best_case = false
|
||||
cc 65db688d970d7dd0c2e0921aa54f207810145b4d3db4c71a3332b7daf75c813f # shrinks to acc = 0.0, combo = None, n300 = None, n100 = None, n50 = None, n_misses = Some(1), best_case = false
|
||||
cc 4dd36fb5fc6aaeb637305941c1dbd224df904729734425f6bd61aded44c20b82 # shrinks to acc = 0.7854494370626834, combo = None, n300 = None, n100 = Some(194), n50 = None, n_misses = None, best_case = false
|
||||
cc 6df5e623c62ffa2830f17f4e7a9bb573cb3eb4c4c45bc3c0793aad26aa72cf95 # shrinks to acc = 0.0, combo = None, n300 = None, n100 = None, n50 = Some(0), n_misses = None, best_case = false
|
||||
cc e5a861f6c665dd09e46423e71d7596edf98897d4130d3144aa6f5be580f31a8b # shrinks to acc = 0.0, combo = None, n300 = None, n100 = Some(293), n50 = None, n_misses = Some(309), best_case = false
|
||||
cc 2cd5c105bcca0b4255afccc15bee3894b06bd20ac3f5c5d3b785f7e0ef99df46 # shrinks to acc = 0.0, combo = None, n300 = Some(0), n100 = None, n50 = Some(479), n_misses = Some(123), best_case = false
|
||||
cc 2cba8a76243aac7233e9207a3162aaa1f08f933c0cb3a2ac79580ece3a7329fc # shrinks to acc = 0.0, n300 = Some(0), n100 = Some(0), n50 = Some(0), n_misses = None, best_case = false
|
||||
cc e93787ad8a849ec6d05750c8d09494b8f5a9fa785f843d9a8e2db986c0b32645 # shrinks to acc = 0.0, n300 = None, n100 = None, n50 = None, n_misses = Some(602), best_case = false
|
||||
@@ -0,0 +1,12 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 1883e1e612026e7f7c8035803830ff21b6d98c946cfb92c5c144e682a5d72ad2 # shrinks to acc = 0.0, n300 = None, n100 = None, n_misses = None, best_case = false
|
||||
cc 84792fb0ec8971122d0eb2e61355a637e9326ac2b7469226bba06a2defdc44dd # shrinks to acc = 0.659020825478002, n300 = None, n100 = None, n_misses = Some(99), best_case = false
|
||||
cc 94fb95e246e580e010115334e5fd7de6160b3a24a735e4cb4ab4341f5f7b769e # shrinks to acc = 0.0, n300 = Some(0), n100 = Some(1), n_misses = None, best_case = false
|
||||
cc fcc52507f0304061f5b94a7aa8bb5cc40e7156effe1e2c3434161a5c152409d0 # shrinks to acc = 0.0, n300 = None, n100 = Some(1), n_misses = None, best_case = false
|
||||
cc 8d6b15c9881ebf33b1b91bb9398264048b28a95cb902f747af6c8320063c21a6 # shrinks to acc = 0.0, n300 = Some(240), n100 = Some(50), n_misses = None, best_case = false
|
||||
cc eda23b52ad1453caba35977df01c1aecf4151a8fc64968fec38f66e2b4970422 # shrinks to acc = 0.0, n300 = None, n100 = None, n_misses = Some(290), best_case = false
|
||||
+393
-289
@@ -1,5 +1,6 @@
|
||||
use super::{CatchDifficultyAttributes, CatchPerformanceAttributes, CatchScoreState, CatchStars};
|
||||
use crate::{Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// Performance calculator on osu!catch maps.
|
||||
///
|
||||
@@ -33,18 +34,19 @@ use crate::{Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes};
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub struct CatchPP<'map> {
|
||||
map: &'map Beatmap,
|
||||
attributes: Option<CatchDifficultyAttributes>,
|
||||
mods: u32,
|
||||
combo: Option<usize>,
|
||||
pub(crate) map: &'map Beatmap,
|
||||
pub(crate) attributes: Option<CatchDifficultyAttributes>,
|
||||
pub(crate) mods: u32,
|
||||
pub(crate) acc: Option<f64>,
|
||||
pub(crate) combo: Option<usize>,
|
||||
|
||||
pub(crate) n_fruits: Option<usize>,
|
||||
pub(crate) n_droplets: Option<usize>,
|
||||
pub(crate) n_tiny_droplets: Option<usize>,
|
||||
pub(crate) n_tiny_droplet_misses: Option<usize>,
|
||||
pub(crate) n_misses: Option<usize>,
|
||||
passed_objects: Option<usize>,
|
||||
clock_rate: Option<f64>,
|
||||
pub(crate) passed_objects: Option<usize>,
|
||||
pub(crate) clock_rate: Option<f64>,
|
||||
}
|
||||
|
||||
impl<'map> CatchPP<'map> {
|
||||
@@ -55,6 +57,7 @@ impl<'map> CatchPP<'map> {
|
||||
map,
|
||||
attributes: None,
|
||||
mods: 0,
|
||||
acc: None,
|
||||
combo: None,
|
||||
|
||||
n_fruits: None,
|
||||
@@ -73,7 +76,7 @@ impl<'map> CatchPP<'map> {
|
||||
#[inline]
|
||||
pub fn attributes(mut self, attributes: impl CatchAttributeProvider) -> Self {
|
||||
if let Some(attributes) = attributes.attributes() {
|
||||
self.attributes.replace(attributes);
|
||||
self.attributes = Some(attributes);
|
||||
}
|
||||
|
||||
self
|
||||
@@ -92,7 +95,7 @@ impl<'map> CatchPP<'map> {
|
||||
/// Specify the max combo of the play.
|
||||
#[inline]
|
||||
pub fn combo(mut self, combo: usize) -> Self {
|
||||
self.combo.replace(combo);
|
||||
self.combo = Some(combo);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -100,7 +103,7 @@ impl<'map> CatchPP<'map> {
|
||||
/// Specify the amount of fruits of a play i.e. n300.
|
||||
#[inline]
|
||||
pub fn fruits(mut self, n_fruits: usize) -> Self {
|
||||
self.n_fruits.replace(n_fruits);
|
||||
self.n_fruits = Some(n_fruits);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -108,7 +111,7 @@ impl<'map> CatchPP<'map> {
|
||||
/// Specify the amount of droplets of a play i.e. n100.
|
||||
#[inline]
|
||||
pub fn droplets(mut self, n_droplets: usize) -> Self {
|
||||
self.n_droplets.replace(n_droplets);
|
||||
self.n_droplets = Some(n_droplets);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -116,7 +119,7 @@ impl<'map> CatchPP<'map> {
|
||||
/// Specify the amount of tiny droplets of a play i.e. n50.
|
||||
#[inline]
|
||||
pub fn tiny_droplets(mut self, n_tiny_droplets: usize) -> Self {
|
||||
self.n_tiny_droplets.replace(n_tiny_droplets);
|
||||
self.n_tiny_droplets = Some(n_tiny_droplets);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -124,7 +127,7 @@ impl<'map> CatchPP<'map> {
|
||||
/// Specify the amount of tiny droplet misses of a play i.e. n_katu.
|
||||
#[inline]
|
||||
pub fn tiny_droplet_misses(mut self, n_tiny_droplet_misses: usize) -> Self {
|
||||
self.n_tiny_droplet_misses.replace(n_tiny_droplet_misses);
|
||||
self.n_tiny_droplet_misses = Some(n_tiny_droplet_misses);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -144,7 +147,7 @@ impl<'map> CatchPP<'map> {
|
||||
/// [`CatchGradualPerformanceAttributes`](crate::catch::CatchGradualPerformance).
|
||||
#[inline]
|
||||
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
|
||||
self.passed_objects.replace(passed_objects);
|
||||
self.passed_objects = Some(passed_objects);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -181,169 +184,192 @@ impl<'map> CatchPP<'map> {
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: adjust this on the next rework
|
||||
/// Generate the hit results with respect to the given accuracy between `0.0` and `100.0`.
|
||||
///
|
||||
/// Be sure to set `misses` beforehand! Also, if available, set `attributes` beforehand.
|
||||
pub fn accuracy(mut self, mut acc: f64) -> Self {
|
||||
if self.attributes.is_none() {
|
||||
let mut calculator = CatchStars::new(self.map).mods(self.mods);
|
||||
|
||||
if let Some(passed_objects) = self.passed_objects {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
}
|
||||
|
||||
if let Some(clock_rate) = self.clock_rate {
|
||||
calculator = calculator.clock_rate(clock_rate);
|
||||
}
|
||||
|
||||
self.attributes = Some(calculator.calculate());
|
||||
}
|
||||
|
||||
let attributes = self.attributes.as_ref().unwrap();
|
||||
|
||||
let n_droplets = self.n_droplets.unwrap_or_else(|| {
|
||||
attributes
|
||||
.n_droplets
|
||||
.saturating_sub(self.n_misses.unwrap_or(0))
|
||||
});
|
||||
|
||||
let max_combo = attributes.max_combo();
|
||||
|
||||
let n_fruits = self.n_fruits.unwrap_or_else(|| {
|
||||
max_combo
|
||||
.saturating_sub(self.n_misses.unwrap_or(0))
|
||||
.saturating_sub(n_droplets)
|
||||
});
|
||||
|
||||
let max_tiny_droplets = attributes.n_tiny_droplets;
|
||||
acc /= 100.0;
|
||||
|
||||
let n_tiny_droplets = self.n_tiny_droplets.unwrap_or_else(|| {
|
||||
((acc * (max_combo + max_tiny_droplets) as f64).round() as usize)
|
||||
.saturating_sub(n_fruits)
|
||||
.saturating_sub(n_droplets)
|
||||
});
|
||||
|
||||
let n_tiny_droplet_misses = max_tiny_droplets.saturating_sub(n_tiny_droplets);
|
||||
|
||||
self.n_fruits.replace(n_fruits);
|
||||
self.n_droplets.replace(n_droplets);
|
||||
self.n_tiny_droplets.replace(n_tiny_droplets);
|
||||
self.n_tiny_droplet_misses.replace(n_tiny_droplet_misses);
|
||||
/// Specify the accuracy of a play between `0.0` and `100.0`.
|
||||
/// This will be used to generate matching hitresults.
|
||||
#[inline]
|
||||
pub fn accuracy(mut self, acc: f64) -> Self {
|
||||
self.acc = Some(acc / 100.0);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn assert_hitresults(self, attributes: CatchDifficultyAttributes) -> CatchPPInner {
|
||||
let max_combo = attributes.max_combo();
|
||||
/// Create the [`CatchScoreState`] that will be used for performance calculation.
|
||||
pub fn generate_state(&mut self) -> CatchScoreState {
|
||||
let attrs = match self.attributes {
|
||||
Some(ref attrs) => attrs,
|
||||
None => self.attributes.insert(self.generate_attributes()),
|
||||
};
|
||||
|
||||
let correct_combo_hits = self
|
||||
.n_fruits
|
||||
.and_then(|f| self.n_droplets.map(|d| f + d + self.n_misses.unwrap_or(0)))
|
||||
.filter(|h| *h == max_combo);
|
||||
let n_misses = self
|
||||
.n_misses
|
||||
.map_or(0, |n| n.min(attrs.n_fruits + attrs.n_droplets));
|
||||
|
||||
let correct_fruits = self.n_fruits.filter(|f| {
|
||||
*f >= attributes
|
||||
.n_fruits
|
||||
.saturating_sub(self.n_misses.unwrap_or(0))
|
||||
});
|
||||
let max_combo = self.combo.unwrap_or_else(|| attrs.max_combo() - n_misses);
|
||||
|
||||
let correct_droplets = self.n_droplets.filter(|d| {
|
||||
*d >= attributes
|
||||
.n_droplets
|
||||
.saturating_sub(self.n_misses.unwrap_or(0))
|
||||
});
|
||||
let mut best_state = CatchScoreState {
|
||||
max_combo,
|
||||
n_misses,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let correct_tinies = self
|
||||
.n_tiny_droplets
|
||||
.and_then(|t| self.n_tiny_droplet_misses.map(|m| t + m))
|
||||
.filter(|h| *h == attributes.n_tiny_droplets);
|
||||
let mut best_dist = f64::INFINITY;
|
||||
|
||||
if correct_combo_hits
|
||||
.and(correct_fruits)
|
||||
.and(correct_droplets)
|
||||
.and(correct_tinies)
|
||||
.is_none()
|
||||
{
|
||||
let mut n_fruits = self.n_fruits.unwrap_or(0);
|
||||
let mut n_droplets = self.n_droplets.unwrap_or(0);
|
||||
let mut n_tiny_droplets = self.n_tiny_droplets.unwrap_or(0);
|
||||
let n_tiny_droplet_misses = self.n_tiny_droplet_misses.unwrap_or(0);
|
||||
let (n_fruits, n_droplets) = match (self.n_fruits, self.n_droplets) {
|
||||
(Some(mut n_fruits), Some(mut n_droplets)) => {
|
||||
let n_remaining = (attrs.n_fruits + attrs.n_droplets)
|
||||
.saturating_sub(n_fruits + n_droplets + n_misses);
|
||||
|
||||
let missing = max_combo
|
||||
.saturating_sub(n_fruits)
|
||||
.saturating_sub(n_droplets)
|
||||
.saturating_sub(self.n_misses.unwrap_or(0));
|
||||
let new_droplets = n_remaining.min(attrs.n_droplets.saturating_sub(n_droplets));
|
||||
n_droplets += new_droplets;
|
||||
n_fruits += n_remaining - new_droplets;
|
||||
|
||||
let missing_fruits =
|
||||
missing.saturating_sub(attributes.n_droplets.saturating_sub(n_droplets));
|
||||
n_fruits = n_fruits
|
||||
.min((attrs.n_fruits + attrs.n_droplets).saturating_sub(n_droplets + n_misses));
|
||||
n_droplets =
|
||||
n_droplets.min(attrs.n_fruits + attrs.n_droplets - n_fruits - n_misses);
|
||||
|
||||
n_fruits += missing_fruits;
|
||||
n_droplets += missing.saturating_sub(missing_fruits);
|
||||
n_tiny_droplets += attributes
|
||||
(n_fruits, n_droplets)
|
||||
}
|
||||
(Some(mut n_fruits), None) => {
|
||||
let n_droplets = attrs.n_droplets.saturating_sub(
|
||||
n_misses.saturating_sub(attrs.n_fruits.saturating_sub(n_fruits)),
|
||||
);
|
||||
|
||||
n_fruits = attrs.n_fruits + attrs.n_droplets - n_misses - n_droplets;
|
||||
|
||||
(n_fruits, n_droplets)
|
||||
}
|
||||
(None, Some(mut n_droplets)) => {
|
||||
let n_fruits = attrs.n_fruits.saturating_sub(
|
||||
n_misses.saturating_sub(attrs.n_droplets.saturating_sub(n_droplets)),
|
||||
);
|
||||
|
||||
n_droplets = attrs.n_fruits + attrs.n_droplets - n_misses - n_fruits;
|
||||
|
||||
(n_fruits, n_droplets)
|
||||
}
|
||||
(None, None) => {
|
||||
let n_droplets = attrs.n_droplets.saturating_sub(n_misses);
|
||||
let n_fruits =
|
||||
attrs.n_fruits - (n_misses - (attrs.n_droplets.saturating_sub(n_droplets)));
|
||||
|
||||
(n_fruits, n_droplets)
|
||||
}
|
||||
};
|
||||
|
||||
best_state.n_fruits = n_fruits;
|
||||
best_state.n_droplets = n_droplets;
|
||||
|
||||
let mut find_best_tiny_droplets = |acc: f64| {
|
||||
let raw_tiny_droplets = acc
|
||||
* (attrs.n_fruits + attrs.n_droplets + attrs.n_tiny_droplets) as f64
|
||||
- (n_fruits + n_droplets) as f64;
|
||||
let min_tiny_droplets = attrs
|
||||
.n_tiny_droplets
|
||||
.saturating_sub(n_tiny_droplets)
|
||||
.saturating_sub(n_tiny_droplet_misses);
|
||||
.min(raw_tiny_droplets.floor() as usize);
|
||||
let max_tiny_droplets = attrs.n_tiny_droplets.min(raw_tiny_droplets.ceil() as usize);
|
||||
|
||||
return CatchPPInner {
|
||||
attributes,
|
||||
mods: self.mods,
|
||||
combo: self.combo,
|
||||
n_fruits,
|
||||
n_droplets,
|
||||
n_tiny_droplets,
|
||||
n_tiny_droplet_misses,
|
||||
n_misses: self.n_misses.unwrap_or(0),
|
||||
};
|
||||
for n_tiny_droplets in min_tiny_droplets..=max_tiny_droplets {
|
||||
let n_tiny_droplet_misses = attrs.n_tiny_droplets - n_tiny_droplets;
|
||||
|
||||
let curr_acc = accuracy(
|
||||
n_fruits,
|
||||
n_droplets,
|
||||
n_tiny_droplets,
|
||||
n_tiny_droplet_misses,
|
||||
n_misses,
|
||||
);
|
||||
let curr_dist = (acc - curr_acc).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
best_state.n_tiny_droplets = n_tiny_droplets;
|
||||
best_state.n_tiny_droplet_misses = n_tiny_droplet_misses;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match (self.n_tiny_droplets, self.n_tiny_droplet_misses) {
|
||||
(Some(n_tiny_droplets), Some(n_tiny_droplet_misses)) => match self.acc {
|
||||
Some(acc) => {
|
||||
match (n_tiny_droplets + n_tiny_droplet_misses).cmp(&attrs.n_tiny_droplets) {
|
||||
Ordering::Equal => {
|
||||
best_state.n_tiny_droplets = n_tiny_droplets;
|
||||
best_state.n_tiny_droplet_misses = n_tiny_droplet_misses;
|
||||
}
|
||||
Ordering::Less | Ordering::Greater => find_best_tiny_droplets(acc),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let n_remaining = attrs
|
||||
.n_tiny_droplets
|
||||
.saturating_sub(n_tiny_droplets + n_tiny_droplet_misses);
|
||||
|
||||
best_state.n_tiny_droplets = n_tiny_droplets + n_remaining;
|
||||
best_state.n_tiny_droplet_misses = n_tiny_droplet_misses;
|
||||
}
|
||||
},
|
||||
(Some(n_tiny_droplets), None) => {
|
||||
best_state.n_tiny_droplets = attrs.n_tiny_droplets.min(n_tiny_droplets);
|
||||
best_state.n_tiny_droplet_misses =
|
||||
attrs.n_tiny_droplets.saturating_sub(n_tiny_droplets);
|
||||
}
|
||||
(None, Some(n_tiny_droplet_misses)) => {
|
||||
best_state.n_tiny_droplets =
|
||||
attrs.n_tiny_droplets.saturating_sub(n_tiny_droplet_misses);
|
||||
best_state.n_tiny_droplet_misses = attrs.n_tiny_droplets.min(n_tiny_droplet_misses);
|
||||
}
|
||||
(None, None) => match self.acc {
|
||||
Some(acc) => find_best_tiny_droplets(acc),
|
||||
None => best_state.n_tiny_droplets = attrs.n_tiny_droplets,
|
||||
},
|
||||
}
|
||||
|
||||
CatchPPInner {
|
||||
attributes,
|
||||
mods: self.mods,
|
||||
combo: self.combo,
|
||||
n_fruits: self.n_fruits.unwrap_or(0),
|
||||
n_droplets: self.n_droplets.unwrap_or(0),
|
||||
n_tiny_droplets: self.n_tiny_droplets.unwrap_or(0),
|
||||
n_tiny_droplet_misses: self.n_tiny_droplet_misses.unwrap_or(0),
|
||||
n_misses: self.n_misses.unwrap_or(0),
|
||||
}
|
||||
best_state
|
||||
}
|
||||
|
||||
/// Calculate all performance related values, including pp and stars.
|
||||
pub fn calculate(mut self) -> CatchPerformanceAttributes {
|
||||
let attributes = self.attributes.take().unwrap_or_else(|| {
|
||||
let mut calculator = CatchStars::new(self.map).mods(self.mods);
|
||||
let state = self.generate_state();
|
||||
|
||||
if let Some(passed_objects) = self.passed_objects {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
}
|
||||
let attrs = self
|
||||
.attributes
|
||||
.take()
|
||||
.unwrap_or_else(|| self.generate_attributes());
|
||||
|
||||
if let Some(clock_rate) = self.clock_rate {
|
||||
calculator = calculator.clock_rate(clock_rate);
|
||||
}
|
||||
let inner = CatchPPInner {
|
||||
attrs,
|
||||
mods: self.mods,
|
||||
state,
|
||||
};
|
||||
|
||||
calculator.calculate()
|
||||
});
|
||||
inner.calculate()
|
||||
}
|
||||
|
||||
self.assert_hitresults(attributes).calculate()
|
||||
fn generate_attributes(&self) -> CatchDifficultyAttributes {
|
||||
let mut calculator = CatchStars::new(self.map).mods(self.mods);
|
||||
|
||||
if let Some(passed_objects) = self.passed_objects {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
}
|
||||
|
||||
if let Some(clock_rate) = self.clock_rate {
|
||||
calculator = calculator.clock_rate(clock_rate);
|
||||
}
|
||||
|
||||
calculator.calculate()
|
||||
}
|
||||
}
|
||||
|
||||
struct CatchPPInner {
|
||||
attributes: CatchDifficultyAttributes,
|
||||
attrs: CatchDifficultyAttributes,
|
||||
mods: u32,
|
||||
combo: Option<usize>,
|
||||
n_fruits: usize,
|
||||
n_droplets: usize,
|
||||
n_tiny_droplets: usize,
|
||||
n_tiny_droplet_misses: usize,
|
||||
n_misses: usize,
|
||||
state: CatchScoreState,
|
||||
}
|
||||
|
||||
impl CatchPPInner {
|
||||
fn calculate(self) -> CatchPerformanceAttributes {
|
||||
let attributes = &self.attributes;
|
||||
let attributes = &self.attrs;
|
||||
let stars = attributes.stars;
|
||||
let max_combo = attributes.max_combo();
|
||||
|
||||
@@ -364,11 +390,13 @@ impl CatchPPInner {
|
||||
pp *= len_bonus;
|
||||
|
||||
// Penalize misses exponentially
|
||||
pp *= 0.97_f64.powi(self.n_misses as i32);
|
||||
pp *= 0.97_f64.powi(self.state.n_misses as i32);
|
||||
|
||||
// Combo scaling
|
||||
if let Some(combo) = self.combo.filter(|_| max_combo > 0) {
|
||||
pp *= (combo as f64 / max_combo as f64).powf(0.8).min(1.0);
|
||||
if self.state.max_combo > 0 {
|
||||
pp *= (self.state.max_combo as f64 / max_combo as f64)
|
||||
.powf(0.8)
|
||||
.min(1.0);
|
||||
}
|
||||
|
||||
// AR scaling
|
||||
@@ -396,7 +424,7 @@ impl CatchPPInner {
|
||||
}
|
||||
|
||||
// Accuracy scaling
|
||||
pp *= self.acc().powf(5.5);
|
||||
pp *= self.state.accuracy().powf(5.5);
|
||||
|
||||
// NF penalty
|
||||
if self.mods.nf() {
|
||||
@@ -404,35 +432,13 @@ impl CatchPPInner {
|
||||
}
|
||||
|
||||
CatchPerformanceAttributes {
|
||||
difficulty: self.attributes,
|
||||
difficulty: self.attrs,
|
||||
pp,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn combo_hits(&self) -> usize {
|
||||
self.n_fruits + self.n_droplets + self.n_misses
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn successful_hits(&self) -> usize {
|
||||
self.n_fruits + self.n_droplets + self.n_tiny_droplets
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_hits(&self) -> usize {
|
||||
self.successful_hits() + self.n_tiny_droplet_misses + self.n_misses
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn acc(&self) -> f64 {
|
||||
let total_hits = self.total_hits();
|
||||
|
||||
if total_hits == 0 {
|
||||
1.0
|
||||
} else {
|
||||
(self.successful_hits() as f64 / total_hits as f64).clamp(0.0, 1.0)
|
||||
}
|
||||
self.state.n_fruits + self.state.n_droplets + self.state.n_misses
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,6 +447,7 @@ impl<'map> From<OsuPP<'map>> for CatchPP<'map> {
|
||||
fn from(osu: OsuPP<'map>) -> Self {
|
||||
let OsuPP {
|
||||
map,
|
||||
attributes: _,
|
||||
mods,
|
||||
acc,
|
||||
combo,
|
||||
@@ -450,13 +457,14 @@ impl<'map> From<OsuPP<'map>> for CatchPP<'map> {
|
||||
n_misses,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
..
|
||||
hitresult_priority: _,
|
||||
} = osu;
|
||||
|
||||
let res = Self {
|
||||
Self {
|
||||
map,
|
||||
attributes: None,
|
||||
mods,
|
||||
acc,
|
||||
combo,
|
||||
n_fruits: n300,
|
||||
n_droplets: n100,
|
||||
@@ -465,15 +473,23 @@ impl<'map> From<OsuPP<'map>> for CatchPP<'map> {
|
||||
n_misses,
|
||||
passed_objects,
|
||||
clock_rate,
|
||||
};
|
||||
|
||||
match acc {
|
||||
Some(acc) => res.accuracy(acc),
|
||||
None => res,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn accuracy(
|
||||
n_fruits: usize,
|
||||
n_droplets: usize,
|
||||
n_tiny_droplets: usize,
|
||||
n_tiny_droplet_misses: usize,
|
||||
n_misses: usize,
|
||||
) -> f64 {
|
||||
let numerator = n_fruits + n_droplets + n_tiny_droplets;
|
||||
let denominator = numerator + n_tiny_droplet_misses + n_misses;
|
||||
|
||||
numerator as f64 / denominator as f64
|
||||
}
|
||||
|
||||
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
||||
pub trait CatchAttributeProvider {
|
||||
/// Provide the actual difficulty attributes.
|
||||
@@ -518,130 +534,218 @@ impl CatchAttributeProvider for PerformanceAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::Beatmap;
|
||||
use proptest::{option, prelude::*};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
fn attributes() -> CatchDifficultyAttributes {
|
||||
CatchDifficultyAttributes {
|
||||
n_fruits: 1234,
|
||||
n_droplets: 567,
|
||||
n_tiny_droplets: 2345,
|
||||
static DATA: OnceLock<(Beatmap, CatchDifficultyAttributes)> = OnceLock::new();
|
||||
|
||||
const N_FRUITS: usize = 728;
|
||||
const N_DROPLETS: usize = 2;
|
||||
const N_TINY_DROPLETS: usize = 291;
|
||||
|
||||
fn test_data() -> (&'static Beatmap, CatchDifficultyAttributes) {
|
||||
let (map, attrs) = DATA.get_or_init(|| {
|
||||
let path = "./maps/2118524.osu";
|
||||
let map = Beatmap::from_path(path).unwrap();
|
||||
let attrs = CatchStars::new(&map).calculate();
|
||||
|
||||
assert_eq!(
|
||||
(N_FRUITS, N_DROPLETS, N_TINY_DROPLETS),
|
||||
(attrs.n_fruits, attrs.n_droplets, attrs.n_tiny_droplets)
|
||||
);
|
||||
|
||||
(map, attrs)
|
||||
});
|
||||
|
||||
(map, attrs.to_owned())
|
||||
}
|
||||
|
||||
/// Checks all remaining hitresult combinations w.r.t. the given parameters
|
||||
/// and returns the [`OsuScoreState`] that matches `acc` the best.
|
||||
///
|
||||
/// Very slow but accurate.
|
||||
fn brute_force_best(
|
||||
acc: f64,
|
||||
n_fruits: Option<usize>,
|
||||
n_droplets: Option<usize>,
|
||||
n_tiny_droplets: Option<usize>,
|
||||
n_tiny_droplet_misses: Option<usize>,
|
||||
n_misses: usize,
|
||||
) -> CatchScoreState {
|
||||
let n_misses = n_misses.min(N_FRUITS + N_DROPLETS);
|
||||
|
||||
let mut best_state = CatchScoreState {
|
||||
max_combo: N_FRUITS + N_DROPLETS - n_misses,
|
||||
n_misses,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut best_dist = f64::INFINITY;
|
||||
|
||||
let (new_fruits, new_droplets) = match (n_fruits, n_droplets) {
|
||||
(Some(mut n_fruits), Some(mut n_droplets)) => {
|
||||
let n_remaining =
|
||||
(N_FRUITS + N_DROPLETS).saturating_sub(n_fruits + n_droplets + n_misses);
|
||||
|
||||
let new_droplets = n_remaining.min(N_DROPLETS.saturating_sub(n_droplets));
|
||||
n_droplets += new_droplets;
|
||||
n_fruits += n_remaining - new_droplets;
|
||||
|
||||
n_fruits =
|
||||
n_fruits.min((N_FRUITS + N_DROPLETS).saturating_sub(n_droplets + n_misses));
|
||||
n_droplets = n_droplets.min(N_FRUITS + N_DROPLETS - n_fruits - n_misses);
|
||||
|
||||
(n_fruits, n_droplets)
|
||||
}
|
||||
(Some(mut n_fruits), None) => {
|
||||
let n_droplets = N_DROPLETS
|
||||
.saturating_sub(n_misses.saturating_sub(N_FRUITS.saturating_sub(n_fruits)));
|
||||
n_fruits = N_FRUITS + N_DROPLETS - n_misses - n_droplets;
|
||||
|
||||
(n_fruits, n_droplets)
|
||||
}
|
||||
(None, Some(mut n_droplets)) => {
|
||||
let n_fruits = N_FRUITS
|
||||
.saturating_sub(n_misses.saturating_sub(N_DROPLETS.saturating_sub(n_droplets)));
|
||||
n_droplets = N_FRUITS + N_DROPLETS - n_misses - n_fruits;
|
||||
|
||||
(n_fruits, n_droplets)
|
||||
}
|
||||
(None, None) => {
|
||||
let n_droplets = N_DROPLETS.saturating_sub(n_misses);
|
||||
let n_fruits = N_FRUITS - (n_misses - (N_DROPLETS.saturating_sub(n_droplets)));
|
||||
|
||||
(n_fruits, n_droplets)
|
||||
}
|
||||
};
|
||||
|
||||
best_state.n_fruits = new_fruits;
|
||||
best_state.n_droplets = new_droplets;
|
||||
|
||||
let (min_tiny_droplets, max_tiny_droplets) = match (n_tiny_droplets, n_tiny_droplet_misses)
|
||||
{
|
||||
(Some(n_tiny_droplets), Some(n_tiny_droplet_misses)) => {
|
||||
match (n_tiny_droplets + n_tiny_droplet_misses).cmp(&N_TINY_DROPLETS) {
|
||||
Ordering::Equal => (
|
||||
N_TINY_DROPLETS.min(n_tiny_droplets),
|
||||
N_TINY_DROPLETS.min(n_tiny_droplets),
|
||||
),
|
||||
Ordering::Less | Ordering::Greater => (0, N_TINY_DROPLETS),
|
||||
}
|
||||
}
|
||||
(Some(n_tiny_droplets), None) => (
|
||||
N_TINY_DROPLETS.min(n_tiny_droplets),
|
||||
N_TINY_DROPLETS.min(n_tiny_droplets),
|
||||
),
|
||||
(None, Some(n_tiny_droplet_misses)) => (
|
||||
N_TINY_DROPLETS.saturating_sub(n_tiny_droplet_misses),
|
||||
N_TINY_DROPLETS.saturating_sub(n_tiny_droplet_misses),
|
||||
),
|
||||
(None, None) => (0, N_TINY_DROPLETS),
|
||||
};
|
||||
|
||||
for new_tiny_droplets in min_tiny_droplets..=max_tiny_droplets {
|
||||
let new_tiny_droplet_misses = N_TINY_DROPLETS - new_tiny_droplets;
|
||||
|
||||
let curr_acc = accuracy(
|
||||
new_fruits,
|
||||
new_droplets,
|
||||
new_tiny_droplets,
|
||||
new_tiny_droplet_misses,
|
||||
n_misses,
|
||||
);
|
||||
|
||||
let curr_dist = (acc - curr_acc).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
best_state.n_tiny_droplets = new_tiny_droplets;
|
||||
best_state.n_tiny_droplet_misses = new_tiny_droplet_misses;
|
||||
}
|
||||
}
|
||||
|
||||
best_state
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(20_000))]
|
||||
#[test]
|
||||
fn catch_hitresults(
|
||||
acc in 0.0..=1.0,
|
||||
n_fruits in option::weighted(0.10, 0_usize..=N_FRUITS + 10),
|
||||
n_droplets in option::weighted(0.10, 0_usize..=N_DROPLETS + 10),
|
||||
n_tiny_droplets in option::weighted(0.10, 0_usize..=N_TINY_DROPLETS + 10),
|
||||
n_tiny_droplet_misses in option::weighted(0.10, 0_usize..=N_TINY_DROPLETS + 10),
|
||||
n_misses in option::weighted(0.15, 0_usize..=N_FRUITS + N_DROPLETS + 10),
|
||||
) {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let mut state = CatchPP::new(map)
|
||||
.attributes(attrs)
|
||||
.accuracy(acc * 100.0);
|
||||
|
||||
if let Some(n_fruits) = n_fruits {
|
||||
state = state.fruits(n_fruits);
|
||||
}
|
||||
|
||||
if let Some(n_droplets) = n_droplets {
|
||||
state = state.droplets(n_droplets);
|
||||
}
|
||||
|
||||
if let Some(n_tiny_droplets) = n_tiny_droplets {
|
||||
state = state.tiny_droplets(n_tiny_droplets);
|
||||
}
|
||||
|
||||
if let Some(n_tiny_droplet_misses) = n_tiny_droplet_misses {
|
||||
state = state.tiny_droplet_misses(n_tiny_droplet_misses);
|
||||
}
|
||||
|
||||
if let Some(n_misses) = n_misses {
|
||||
state = state.misses(n_misses);
|
||||
}
|
||||
|
||||
let state = state.generate_state();
|
||||
|
||||
let expected = brute_force_best(
|
||||
acc,
|
||||
n_fruits,
|
||||
n_droplets,
|
||||
n_tiny_droplets,
|
||||
n_tiny_droplet_misses,
|
||||
n_misses.unwrap_or(0),
|
||||
);
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fruits_only_accuracy() {
|
||||
let map = Beatmap::default();
|
||||
let attributes = attributes();
|
||||
|
||||
let total_objects = attributes.n_fruits + attributes.n_droplets;
|
||||
let target_acc = 97.5;
|
||||
|
||||
let calculator = CatchPP::new(&map)
|
||||
.attributes(attributes)
|
||||
.passed_objects(total_objects)
|
||||
.accuracy(target_acc);
|
||||
|
||||
let numerator = calculator.n_fruits.unwrap_or(0)
|
||||
+ calculator.n_droplets.unwrap_or(0)
|
||||
+ calculator.n_tiny_droplets.unwrap_or(0);
|
||||
let denominator = numerator
|
||||
+ calculator.n_tiny_droplet_misses.unwrap_or(0)
|
||||
+ calculator.n_misses.unwrap_or(0);
|
||||
let acc = 100.0 * numerator as f64 / denominator as f64;
|
||||
|
||||
assert!(
|
||||
(target_acc - acc).abs() < 1.0,
|
||||
"Expected: {target_acc} | Actual: {acc}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fruits_accuracy_droplets_and_tiny_droplets() {
|
||||
let map = Beatmap::default();
|
||||
let attributes = attributes();
|
||||
|
||||
let total_objects = attributes.n_fruits + attributes.n_droplets;
|
||||
let target_acc = 97.5;
|
||||
let n_droplets = 550;
|
||||
let n_tiny_droplets = 2222;
|
||||
|
||||
let calculator = CatchPP::new(&map)
|
||||
.attributes(attributes)
|
||||
.passed_objects(total_objects)
|
||||
.droplets(n_droplets)
|
||||
.tiny_droplets(n_tiny_droplets)
|
||||
.accuracy(target_acc);
|
||||
|
||||
assert_eq!(
|
||||
n_droplets,
|
||||
calculator.n_droplets.unwrap(),
|
||||
"Expected: {} | Actual: {}",
|
||||
n_droplets,
|
||||
calculator.n_droplets.unwrap()
|
||||
);
|
||||
|
||||
let numerator = calculator.n_fruits.unwrap_or(0)
|
||||
+ calculator.n_droplets.unwrap_or(0)
|
||||
+ calculator.n_tiny_droplets.unwrap_or(0);
|
||||
let denominator = numerator
|
||||
+ calculator.n_tiny_droplet_misses.unwrap_or(0)
|
||||
+ calculator.n_misses.unwrap_or(0);
|
||||
let acc = 100.0 * numerator as f64 / denominator as f64;
|
||||
|
||||
assert!(
|
||||
(target_acc - acc).abs() < 1.0,
|
||||
"Expected: {target_acc} | Actual: {acc}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fruits_missing_objects() {
|
||||
let map = Beatmap::default();
|
||||
let attributes = attributes();
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let total_objects = attributes.n_fruits + attributes.n_droplets;
|
||||
let n_fruits = attributes.n_fruits - 10;
|
||||
let n_droplets = attributes.n_droplets - 5;
|
||||
let n_tiny_droplets = attributes.n_tiny_droplets - 50;
|
||||
let n_tiny_droplet_misses = 20;
|
||||
let n_misses = 2;
|
||||
let state = CatchPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.fruits(N_FRUITS - 10)
|
||||
.droplets(N_DROPLETS - 1)
|
||||
.tiny_droplets(N_TINY_DROPLETS - 50)
|
||||
.tiny_droplet_misses(20)
|
||||
.misses(2)
|
||||
.generate_state();
|
||||
|
||||
let calculator = CatchPP::new(&map)
|
||||
.attributes(attributes.clone())
|
||||
.passed_objects(total_objects)
|
||||
.fruits(n_fruits)
|
||||
.droplets(n_droplets)
|
||||
.tiny_droplets(n_tiny_droplets)
|
||||
.tiny_droplet_misses(n_tiny_droplet_misses)
|
||||
.misses(n_misses)
|
||||
.assert_hitresults(attributes.clone());
|
||||
let expected = CatchScoreState {
|
||||
max_combo: N_FRUITS + N_DROPLETS - 2,
|
||||
n_fruits: N_FRUITS - 2,
|
||||
n_droplets: N_DROPLETS,
|
||||
n_tiny_droplets: N_TINY_DROPLETS - 20,
|
||||
n_tiny_droplet_misses: 20,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert!(
|
||||
(attributes.n_fruits as i32 - calculator.n_fruits as i32).abs() <= n_misses as i32,
|
||||
"Expected: {} | Actual: {} [+/- {} misses]",
|
||||
attributes.n_fruits,
|
||||
calculator.n_fruits,
|
||||
n_misses
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
attributes.n_droplets,
|
||||
calculator.n_droplets - (n_misses - (attributes.n_fruits - calculator.n_fruits)),
|
||||
"Expected: {} | Actual: {}",
|
||||
attributes.n_droplets,
|
||||
calculator.n_droplets - (n_misses - (attributes.n_fruits - calculator.n_fruits)),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
attributes.n_tiny_droplets,
|
||||
calculator.n_tiny_droplets + calculator.n_tiny_droplet_misses,
|
||||
"Expected: {} | Actual: {}",
|
||||
attributes.n_tiny_droplets,
|
||||
calculator.n_tiny_droplets + calculator.n_tiny_droplet_misses,
|
||||
);
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +24,29 @@ impl CatchScoreState {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Return the total amount of hits by adding everything up.
|
||||
#[inline]
|
||||
pub fn total_hits(&self) -> usize {
|
||||
self.n_fruits
|
||||
+ self.n_droplets
|
||||
+ self.n_tiny_droplets
|
||||
+ self.n_tiny_droplet_misses
|
||||
+ self.n_misses
|
||||
}
|
||||
|
||||
/// Calculate the accuracy between `0.0` and `1.0` for this state.
|
||||
#[inline]
|
||||
pub fn accuracy(&self) -> f64 {
|
||||
let total_hits = self.total_hits();
|
||||
|
||||
if total_hits == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let numerator = self.n_fruits + self.n_droplets + self.n_tiny_droplets;
|
||||
let denominator = total_hits;
|
||||
|
||||
numerator as f64 / denominator as f64
|
||||
}
|
||||
}
|
||||
|
||||
+758
-568
File diff suppressed because it is too large
Load Diff
+322
-276
@@ -208,17 +208,25 @@ impl<'map> OsuPP<'map> {
|
||||
self
|
||||
}
|
||||
|
||||
fn generate_hitresults(&self, max_combo: usize) -> OsuScoreState {
|
||||
/// Create the [`OsuScoreState`] that will be used for performance calculation.
|
||||
pub fn generate_state(&mut self) -> OsuScoreState {
|
||||
let max_combo = match self.attributes {
|
||||
Some(ref attrs) => attrs.max_combo,
|
||||
None => self.attributes.insert(self.generate_attributes()).max_combo,
|
||||
};
|
||||
|
||||
let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len());
|
||||
let priority = self.hitresult_priority.unwrap_or_default();
|
||||
|
||||
let mut n300 = self.n300.unwrap_or(0);
|
||||
let mut n100 = self.n100.unwrap_or(0);
|
||||
let mut n50 = self.n50.unwrap_or(0);
|
||||
let n_misses = self.n_misses.unwrap_or(0);
|
||||
let n_misses = self.n_misses.map_or(0, |n| n.min(n_objects));
|
||||
let n_remaining = n_objects - n_misses;
|
||||
|
||||
let mut n300 = self.n300.map_or(0, |n| n.min(n_remaining));
|
||||
let mut n100 = self.n100.map_or(0, |n| n.min(n_remaining));
|
||||
let mut n50 = self.n50.map_or(0, |n| n.min(n_remaining));
|
||||
|
||||
if let Some(acc) = self.acc {
|
||||
let target_total = (acc * (n_objects * 6) as f64).round() as usize;
|
||||
let target_total = acc * (6 * n_objects) as f64;
|
||||
|
||||
match (self.n300, self.n100, self.n50) {
|
||||
(Some(_), Some(_), Some(_)) => {
|
||||
@@ -233,80 +241,110 @@ impl<'map> OsuPP<'map> {
|
||||
(Some(_), None, Some(_)) => n100 = n_objects.saturating_sub(n300 + n50 + n_misses),
|
||||
(None, Some(_), Some(_)) => n300 = n_objects.saturating_sub(n100 + n50 + n_misses),
|
||||
(Some(_), None, None) => {
|
||||
let delta = (target_total - n_objects.saturating_sub(n_misses))
|
||||
.saturating_sub(n300 * 5);
|
||||
let mut best_dist = f64::MAX;
|
||||
|
||||
n100 = delta % 5;
|
||||
n50 = n_objects.saturating_sub(n300 + n100 + n_misses);
|
||||
n300 = n300.min(n_remaining);
|
||||
let n_remaining = n_remaining - n300;
|
||||
|
||||
let curr_total = 6 * n300 + 2 * n100 + n50;
|
||||
let raw_n100 = target_total - (n_remaining + 6 * n300) as f64;
|
||||
let min_n100 = n_remaining.min(raw_n100.floor() as usize);
|
||||
let max_n100 = n_remaining.min(raw_n100.ceil() as usize);
|
||||
|
||||
if curr_total < target_total {
|
||||
let n = (target_total - curr_total).min(n50);
|
||||
n50 -= n;
|
||||
n100 += n;
|
||||
} else {
|
||||
let n = (curr_total - target_total).min(n100);
|
||||
n100 -= n;
|
||||
n50 += n;
|
||||
for new100 in min_n100..=max_n100 {
|
||||
let new50 = n_remaining - new100;
|
||||
let dist = (acc - accuracy(n300, new100, new50, n_misses)).abs();
|
||||
|
||||
if dist < best_dist {
|
||||
best_dist = dist;
|
||||
n100 = new100;
|
||||
n50 = new50;
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, Some(_), None) => {
|
||||
let delta =
|
||||
(target_total - n_objects.saturating_sub(n_misses)).saturating_sub(n100);
|
||||
let mut best_dist = f64::MAX;
|
||||
|
||||
n300 = delta / 5;
|
||||
n100 = n100.min(n_remaining);
|
||||
let n_remaining = n_remaining - n100;
|
||||
|
||||
if n300 + n100 + n_misses > n_objects {
|
||||
n300 -= (n300 + n100 + n_misses) - n_objects;
|
||||
}
|
||||
let raw_n300 = (target_total - (n_remaining + 2 * n100) as f64) / 5.0;
|
||||
let min_n300 = n_remaining.min(raw_n300.floor() as usize);
|
||||
let max_n300 = n_remaining.min(raw_n300.ceil() as usize);
|
||||
|
||||
n50 = n_objects - n300 - n100 - n_misses;
|
||||
}
|
||||
(None, None, Some(_)) => {
|
||||
let delta = target_total - n_objects.saturating_sub(n_misses);
|
||||
for new300 in min_n300..=max_n300 {
|
||||
let new50 = n_remaining - new300;
|
||||
let curr_dist = (acc - accuracy(new300, n100, new50, n_misses)).abs();
|
||||
|
||||
n300 = delta / 5;
|
||||
n100 = delta % 5;
|
||||
|
||||
if n300 + n100 + n50 + n_misses > n_objects {
|
||||
let too_many = n300 + n100 + n50 + n_misses - n_objects;
|
||||
|
||||
if too_many > n100 {
|
||||
n300 -= too_many - n100;
|
||||
n100 = 0;
|
||||
} else {
|
||||
n100 -= too_many;
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
n300 = new300;
|
||||
n50 = new50;
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None, Some(_)) => {
|
||||
let mut best_dist = f64::MAX;
|
||||
|
||||
n100 += n_objects.saturating_sub(n300 + n100 + n50 + n_misses);
|
||||
n50 = n50.min(n_remaining);
|
||||
let n_remaining = n_remaining - n50;
|
||||
|
||||
let curr_total = 6 * n300 + 2 * n100 + n50;
|
||||
let raw_n300 =
|
||||
(target_total + (2 * n_misses + n50) as f64 - (2 * n_objects) as f64) / 4.0;
|
||||
|
||||
if curr_total < target_total {
|
||||
let n = n100.min((target_total - curr_total) / 4);
|
||||
n100 -= n;
|
||||
n300 += n;
|
||||
} else {
|
||||
let n = n300.min((curr_total - target_total) / 4);
|
||||
n300 -= n;
|
||||
n100 += n;
|
||||
let min_n300 = n_remaining.min(raw_n300.floor() as usize);
|
||||
let max_n300 = n_remaining.min(raw_n300.ceil() as usize);
|
||||
|
||||
for new300 in min_n300..=max_n300 {
|
||||
let new100 = n_remaining - new300;
|
||||
let curr_dist = (acc - accuracy(new300, new100, n50, n_misses)).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
n300 = new300;
|
||||
n100 = new100;
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, None, None) => {
|
||||
let delta = target_total - n_objects.saturating_sub(n_misses);
|
||||
let mut best_dist = f64::MAX;
|
||||
|
||||
n300 = delta / 5;
|
||||
n100 = delta % 5;
|
||||
n50 = n_objects.saturating_sub(n300 + n100 + n_misses);
|
||||
let raw_n300 = (target_total - n_remaining as f64) / 5.0;
|
||||
let min_n300 = n_remaining.min(raw_n300.floor() as usize);
|
||||
let max_n300 = n_remaining.min(raw_n300.ceil() as usize);
|
||||
|
||||
if let HitResultPriority::BestCase = priority {
|
||||
// Shift n50 to n100 by sacrificing n300
|
||||
let n = n300.min(n50 / 4);
|
||||
n300 -= n;
|
||||
n100 += 5 * n;
|
||||
n50 -= 4 * n;
|
||||
for new300 in min_n300..=max_n300 {
|
||||
let raw_n100 = target_total - (n_remaining + 5 * new300) as f64;
|
||||
let min_n100 = (raw_n100.floor() as usize).min(n_remaining - new300);
|
||||
let max_n100 = (raw_n100.ceil() as usize).min(n_remaining - new300);
|
||||
|
||||
for new100 in min_n100..=max_n100 {
|
||||
let new50 = n_remaining - new300 - new100;
|
||||
let curr_dist = (acc - accuracy(new300, new100, new50, n_misses)).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
n300 = new300;
|
||||
n100 = new100;
|
||||
n50 = new50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => {
|
||||
// Shift n50 to n100 by sacrificing n300
|
||||
let n = n300.min(n50 / 4);
|
||||
n300 -= n;
|
||||
n100 += 5 * n;
|
||||
n50 -= 4 * n;
|
||||
}
|
||||
HitResultPriority::WorstCase => {
|
||||
// Shift n100 to n50 by gaining n300
|
||||
let n = n100 / 5;
|
||||
n300 += n;
|
||||
n100 -= 5 * n;
|
||||
n50 += 4 * n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,33 +352,29 @@ impl<'map> OsuPP<'map> {
|
||||
let remaining = n_objects.saturating_sub(n300 + n100 + n50 + n_misses);
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => {
|
||||
if self.n300.is_none() {
|
||||
n300 = remaining;
|
||||
} else if self.n100.is_none() {
|
||||
n100 = remaining;
|
||||
} else if self.n50.is_none() {
|
||||
n50 = remaining;
|
||||
} else {
|
||||
n300 += remaining;
|
||||
}
|
||||
}
|
||||
HitResultPriority::WorstCase => {
|
||||
if self.n50.is_none() {
|
||||
n50 = remaining;
|
||||
} else if self.n100.is_none() {
|
||||
n100 = remaining;
|
||||
} else if self.n300.is_none() {
|
||||
n300 = remaining;
|
||||
} else {
|
||||
n50 += remaining;
|
||||
}
|
||||
}
|
||||
HitResultPriority::BestCase => match (self.n300, self.n100, self.n50) {
|
||||
(None, ..) => n300 = remaining,
|
||||
(_, None, _) => n100 = remaining,
|
||||
(.., None) => n50 = remaining,
|
||||
_ => n300 += remaining,
|
||||
},
|
||||
HitResultPriority::WorstCase => match (self.n50, self.n100, self.n300) {
|
||||
(None, ..) => n50 = remaining,
|
||||
(_, None, _) => n100 = remaining,
|
||||
(.., None) => n300 = remaining,
|
||||
_ => n50 += remaining,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let max_possible_combo = max_combo.saturating_sub(n_misses);
|
||||
|
||||
let max_combo = self
|
||||
.combo
|
||||
.map_or(max_possible_combo, |combo| combo.min(max_possible_combo));
|
||||
|
||||
OsuScoreState {
|
||||
max_combo: self.combo.unwrap_or(max_combo),
|
||||
max_combo,
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
@@ -350,21 +384,13 @@ impl<'map> OsuPP<'map> {
|
||||
|
||||
/// Calculate all performance related values, including pp and stars.
|
||||
pub fn calculate(mut self) -> OsuPerformanceAttributes {
|
||||
let attrs = self.attributes.take().unwrap_or_else(|| {
|
||||
let mut calculator = OsuStars::new(self.map).mods(self.mods);
|
||||
let state = self.generate_state();
|
||||
|
||||
if let Some(passed_objects) = self.passed_objects {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
}
|
||||
let attrs = self
|
||||
.attributes
|
||||
.take()
|
||||
.unwrap_or_else(|| self.generate_attributes());
|
||||
|
||||
if let Some(clock_rate) = self.clock_rate {
|
||||
calculator = calculator.clock_rate(clock_rate);
|
||||
}
|
||||
|
||||
calculator.calculate()
|
||||
});
|
||||
|
||||
let state = self.generate_hitresults(attrs.max_combo);
|
||||
let effective_miss_count = calculate_effective_misses(&attrs, &state);
|
||||
|
||||
let inner = OsuPpInner {
|
||||
@@ -377,6 +403,20 @@ impl<'map> OsuPP<'map> {
|
||||
|
||||
inner.calculate()
|
||||
}
|
||||
|
||||
fn generate_attributes(&self) -> OsuDifficultyAttributes {
|
||||
let mut calculator = OsuStars::new(self.map).mods(self.mods);
|
||||
|
||||
if let Some(passed_objects) = self.passed_objects {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
}
|
||||
|
||||
if let Some(clock_rate) = self.clock_rate {
|
||||
calculator = calculator.clock_rate(clock_rate);
|
||||
}
|
||||
|
||||
calculator.calculate()
|
||||
}
|
||||
}
|
||||
|
||||
struct OsuPpInner {
|
||||
@@ -696,6 +736,15 @@ fn calculate_effective_misses(attrs: &OsuDifficultyAttributes, state: &OsuScoreS
|
||||
combo_based_miss_count.max(state.n_misses as f64)
|
||||
}
|
||||
|
||||
fn accuracy(n300: usize, n100: usize, n50: usize, n_misses: usize) -> f64 {
|
||||
debug_assert_ne!(n300 + n100 + n50 + n_misses, 0);
|
||||
|
||||
let numerator = 6 * n300 + 2 * n100 + n50;
|
||||
let denominator = 6 * (n300 + n100 + n50 + n_misses);
|
||||
|
||||
numerator as f64 / denominator as f64
|
||||
}
|
||||
|
||||
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
||||
pub trait OsuAttributeProvider {
|
||||
/// Provide the actual difficulty attributes.
|
||||
@@ -745,43 +794,193 @@ impl OsuAttributeProvider for PerformanceAttributes {
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::Beatmap;
|
||||
use proptest::{option, prelude::*};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
fn test_data() -> (Beatmap, OsuDifficultyAttributes) {
|
||||
let path = "./maps/2785319.osu";
|
||||
let map = Beatmap::from_path(path).unwrap();
|
||||
static DATA: OnceLock<(Beatmap, OsuDifficultyAttributes)> = OnceLock::new();
|
||||
|
||||
let attrs = OsuDifficultyAttributes {
|
||||
aim: 2.8693628443424104,
|
||||
speed: 2.533869745015772,
|
||||
flashlight: 2.288770487900865,
|
||||
slider_factor: 0.9803052946037858,
|
||||
speed_note_count: 210.36373973116545,
|
||||
ar: 9.300000190734863,
|
||||
od: 8.800000190734863,
|
||||
hp: 5.0,
|
||||
n_circles: 307,
|
||||
n_sliders: 293,
|
||||
n_spinners: 1,
|
||||
stars: 5.669858729379631,
|
||||
max_combo: 909,
|
||||
const N_OBJECTS: usize = 601;
|
||||
|
||||
fn test_data() -> (&'static Beatmap, OsuDifficultyAttributes) {
|
||||
let (map, attrs) = DATA.get_or_init(|| {
|
||||
let path = "./maps/2785319.osu";
|
||||
let map = Beatmap::from_path(path).unwrap();
|
||||
let attrs = OsuStars::new(&map).calculate();
|
||||
|
||||
assert_eq!(
|
||||
(attrs.n_circles, attrs.n_sliders, attrs.n_spinners),
|
||||
(307, 293, 1)
|
||||
);
|
||||
assert_eq!(
|
||||
attrs.n_circles + attrs.n_sliders + attrs.n_spinners,
|
||||
N_OBJECTS,
|
||||
);
|
||||
|
||||
(map, attrs)
|
||||
});
|
||||
|
||||
(map, attrs.to_owned())
|
||||
}
|
||||
|
||||
/// Checks all remaining hitresult combinations w.r.t. the given parameters
|
||||
/// and returns the [`OsuScoreState`] that matches `acc` the best.
|
||||
///
|
||||
/// Very slow but accurate.
|
||||
fn brute_force_best(
|
||||
acc: f64,
|
||||
n300: Option<usize>,
|
||||
n100: Option<usize>,
|
||||
n50: Option<usize>,
|
||||
n_misses: usize,
|
||||
best_case: bool,
|
||||
) -> OsuScoreState {
|
||||
let n_misses = n_misses.min(N_OBJECTS);
|
||||
|
||||
let mut best_state = OsuScoreState {
|
||||
n_misses,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
(map, attrs)
|
||||
let mut best_dist = f64::INFINITY;
|
||||
|
||||
let n_remaining = N_OBJECTS - n_misses;
|
||||
|
||||
let (min_n300, max_n300) = match (n300, n100, n50) {
|
||||
(Some(n300), ..) => (n_remaining.min(n300), n_remaining.min(n300)),
|
||||
(None, Some(n100), Some(n50)) => (
|
||||
n_remaining.saturating_sub(n100 + n50),
|
||||
n_remaining.saturating_sub(n100 + n50),
|
||||
),
|
||||
(None, ..) => (
|
||||
0,
|
||||
n_remaining.saturating_sub(n100.unwrap_or(0) + n50.unwrap_or(0)),
|
||||
),
|
||||
};
|
||||
|
||||
for new300 in min_n300..=max_n300 {
|
||||
let (min_n100, max_n100) = match (n100, n50) {
|
||||
(Some(n100), _) => (n_remaining.min(n100), n_remaining.min(n100)),
|
||||
(None, Some(n50)) => (
|
||||
n_remaining.saturating_sub(new300 + n50),
|
||||
n_remaining.saturating_sub(new300 + n50),
|
||||
),
|
||||
(None, None) => (0, n_remaining - new300),
|
||||
};
|
||||
|
||||
for new100 in min_n100..=max_n100 {
|
||||
let new50 = match n50 {
|
||||
Some(n50) => n_remaining.min(n50),
|
||||
None => n_remaining.saturating_sub(new300 + new100),
|
||||
};
|
||||
|
||||
let curr_acc = accuracy(new300, new100, new50, n_misses);
|
||||
let curr_dist = (acc - curr_acc).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
best_state.n300 = new300;
|
||||
best_state.n100 = new100;
|
||||
best_state.n50 = new50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if best_state.n300 + best_state.n100 + best_state.n50 < n_remaining {
|
||||
let remaining = n_remaining - (best_state.n300 + best_state.n100 + best_state.n50);
|
||||
|
||||
if best_case {
|
||||
best_state.n300 += remaining;
|
||||
} else {
|
||||
best_state.n50 += remaining;
|
||||
}
|
||||
}
|
||||
|
||||
if n300.is_none() && n100.is_none() && n50.is_none() {
|
||||
if best_case {
|
||||
let n = best_state.n300.min(best_state.n50 / 4);
|
||||
best_state.n300 -= n;
|
||||
best_state.n100 += 5 * n;
|
||||
best_state.n50 -= 4 * n;
|
||||
} else {
|
||||
let n = best_state.n100 / 5;
|
||||
best_state.n300 += n;
|
||||
best_state.n100 -= 5 * n;
|
||||
best_state.n50 += 4 * n;
|
||||
}
|
||||
}
|
||||
|
||||
best_state
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(10_000))]
|
||||
#[test]
|
||||
fn osu_hitresults(
|
||||
acc in 0.0..=1.0,
|
||||
n300 in option::weighted(0.10, 0_usize..=N_OBJECTS + 10),
|
||||
n100 in option::weighted(0.10, 0_usize..=N_OBJECTS + 10),
|
||||
n50 in option::weighted(0.10, 0_usize..=N_OBJECTS + 10),
|
||||
n_misses in option::weighted(0.15, 0_usize..=N_OBJECTS + 10),
|
||||
best_case in prop::bool::ANY,
|
||||
) {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let priority = if best_case {
|
||||
HitResultPriority::BestCase
|
||||
} else {
|
||||
HitResultPriority::WorstCase
|
||||
};
|
||||
|
||||
let mut state = OsuPP::new(map)
|
||||
.attributes(attrs)
|
||||
.accuracy(acc * 100.0)
|
||||
.hitresult_priority(priority);
|
||||
|
||||
if let Some(n300) = n300 {
|
||||
state = state.n300(n300);
|
||||
}
|
||||
|
||||
if let Some(n100) = n100 {
|
||||
state = state.n100(n100);
|
||||
}
|
||||
|
||||
if let Some(n50) = n50 {
|
||||
state = state.n50(n50);
|
||||
}
|
||||
|
||||
if let Some(n_misses) = n_misses {
|
||||
state = state.n_misses(n_misses);
|
||||
}
|
||||
|
||||
let state = state.generate_state();
|
||||
|
||||
let mut expected = brute_force_best(
|
||||
acc,
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
n_misses.unwrap_or(0),
|
||||
best_case,
|
||||
);
|
||||
expected.max_combo = max_combo.saturating_sub(n_misses.map_or(0, |n| n.min(N_OBJECTS)));
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n300_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
let state = OsuPP::new(map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.n300(300)
|
||||
.n100(20)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
.generate_state();
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
@@ -797,16 +996,15 @@ mod test {
|
||||
#[test]
|
||||
fn hitresults_n300_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
let state = OsuPP::new(map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.n300(300)
|
||||
.n50(10)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
.generate_state();
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
@@ -822,15 +1020,14 @@ mod test {
|
||||
#[test]
|
||||
fn hitresults_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
let state = OsuPP::new(map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.n50(10)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_hitresults(max_combo);
|
||||
.generate_state();
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
@@ -846,9 +1043,8 @@ mod test {
|
||||
#[test]
|
||||
fn hitresults_n300_n100_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
let state = OsuPP::new(map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.n300(300)
|
||||
@@ -856,7 +1052,7 @@ mod test {
|
||||
.n50(10)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_hitresults(max_combo);
|
||||
.generate_state();
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
@@ -868,154 +1064,4 @@ mod test {
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.accuracy(98.0)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 584,
|
||||
n100: 15,
|
||||
n50: 0,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.accuracy(95.0)
|
||||
.n100(15)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 562,
|
||||
n100: 15,
|
||||
n50: 22,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.accuracy(95.0)
|
||||
.n50(10)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 560,
|
||||
n100: 29,
|
||||
n50: 10,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.accuracy(90.0)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 511,
|
||||
n100: 89,
|
||||
n50: 1,
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.accuracy(90.0)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 528,
|
||||
n100: 4,
|
||||
n50: 69,
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ impl<'map> AnyPP<'map> {
|
||||
match self {
|
||||
Self::Osu(o) => Self::Osu(o.hitresult_priority(priority)),
|
||||
Self::Taiko(t) => Self::Taiko(t.hitresult_priority(priority)),
|
||||
Self::Catch(_) => self, // FIXME: update when ctb hitresult generation is updated
|
||||
Self::Catch(_) => self,
|
||||
Self::Mania(m) => Self::Mania(m.hitresult_priority(priority)),
|
||||
}
|
||||
}
|
||||
@@ -274,6 +274,17 @@ impl<'map> AnyPP<'map> {
|
||||
Self::Mania(m) => Self::Mania(m.n320(n_geki)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the [`ScoreState`] that will be used for performance calculation.
|
||||
#[inline]
|
||||
pub fn generate_state(&mut self) -> ScoreState {
|
||||
match self {
|
||||
Self::Osu(o) => o.generate_state().into(),
|
||||
Self::Taiko(t) => t.generate_state().into(),
|
||||
Self::Catch(f) => f.generate_state().into(),
|
||||
Self::Mania(m) => m.generate_state().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// While generating remaining hitresults, decide how they should be distributed.
|
||||
|
||||
@@ -108,3 +108,63 @@ impl From<ScoreState> for ManiaScoreState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OsuScoreState> for ScoreState {
|
||||
#[inline]
|
||||
fn from(state: OsuScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
n_geki: 0,
|
||||
n_katu: 0,
|
||||
n300: state.n300,
|
||||
n100: state.n100,
|
||||
n50: state.n50,
|
||||
n_misses: state.n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TaikoScoreState> for ScoreState {
|
||||
#[inline]
|
||||
fn from(state: TaikoScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
n_geki: 0,
|
||||
n_katu: 0,
|
||||
n300: state.n300,
|
||||
n100: state.n100,
|
||||
n50: 0,
|
||||
n_misses: state.n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CatchScoreState> for ScoreState {
|
||||
#[inline]
|
||||
fn from(state: CatchScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
n_geki: 0,
|
||||
n_katu: state.n_tiny_droplet_misses,
|
||||
n300: state.n_fruits,
|
||||
n100: state.n_droplets,
|
||||
n50: state.n_tiny_droplets,
|
||||
n_misses: state.n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ManiaScoreState> for ScoreState {
|
||||
#[inline]
|
||||
fn from(state: ManiaScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: 0,
|
||||
n_geki: state.n320,
|
||||
n_katu: state.n200,
|
||||
n300: state.n300,
|
||||
n100: state.n100,
|
||||
n50: state.n50,
|
||||
n_misses: state.n_misses,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+212
-86
@@ -197,34 +197,13 @@ impl<'map> TaikoPP<'map> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Calculate all performance related values, including pp and stars.
|
||||
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
|
||||
let attrs = self.attributes.take().unwrap_or_else(|| {
|
||||
let mut calculator = TaikoStars::new(self.map.as_ref())
|
||||
.mods(self.mods)
|
||||
.is_convert(self.is_convert);
|
||||
|
||||
if let Some(passed_objects) = self.passed_objects {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
}
|
||||
|
||||
if let Some(clock_rate) = self.clock_rate {
|
||||
calculator = calculator.clock_rate(clock_rate);
|
||||
}
|
||||
|
||||
calculator.calculate()
|
||||
});
|
||||
|
||||
let inner = TaikoPpInner {
|
||||
mods: self.mods,
|
||||
state: self.generate_hitresults(attrs.max_combo),
|
||||
attrs,
|
||||
/// Create the [`TaikoScoreState`] that will be used for performance calculation.
|
||||
pub fn generate_state(&mut self) -> TaikoScoreState {
|
||||
let max_combo = match self.attributes {
|
||||
Some(ref attrs) => attrs.max_combo,
|
||||
None => self.attributes.insert(self.generate_attributes()).max_combo,
|
||||
};
|
||||
|
||||
inner.calculate()
|
||||
}
|
||||
|
||||
fn generate_hitresults(&self, max_combo: usize) -> TaikoScoreState {
|
||||
let total_result_count = if let Some(passed_objects) = self.passed_objects {
|
||||
max_combo.min(passed_objects)
|
||||
} else {
|
||||
@@ -233,9 +212,11 @@ impl<'map> TaikoPP<'map> {
|
||||
|
||||
let priority = self.hitresult_priority.unwrap_or_default();
|
||||
|
||||
let mut n300 = self.n300.unwrap_or(0);
|
||||
let mut n100 = self.n100.unwrap_or(0);
|
||||
let n_misses = self.n_misses.unwrap_or(0);
|
||||
let n_misses = self.n_misses.map_or(0, |n| n.min(total_result_count));
|
||||
let n_remaining = total_result_count - n_misses;
|
||||
|
||||
let mut n300 = self.n300.map_or(0, |n| n.min(n_remaining));
|
||||
let mut n100 = self.n100.map_or(0, |n| n.min(n_remaining));
|
||||
|
||||
if let Some(acc) = self.acc {
|
||||
match (self.n300, self.n100) {
|
||||
@@ -250,9 +231,24 @@ impl<'map> TaikoPP<'map> {
|
||||
(Some(_), None) => n100 += total_result_count.saturating_sub(n300 + n_misses),
|
||||
(None, Some(_)) => n300 += total_result_count.saturating_sub(n100 + n_misses),
|
||||
(None, None) => {
|
||||
let target_total = (acc * (total_result_count * 2) as f64).round() as usize;
|
||||
n300 = target_total - (total_result_count.saturating_sub(n_misses));
|
||||
n100 = total_result_count.saturating_sub(n300 + n_misses);
|
||||
let target_total = acc * (2 * total_result_count) as f64;
|
||||
|
||||
let mut best_dist = f64::MAX;
|
||||
|
||||
let raw_n300 = target_total - n_remaining as f64;
|
||||
let min_n300 = n_remaining.min(raw_n300.floor() as usize);
|
||||
let max_n300 = n_remaining.min(raw_n300.ceil() as usize);
|
||||
|
||||
for new300 in min_n300..=max_n300 {
|
||||
let new100 = n_remaining - new300;
|
||||
let dist = (acc - accuracy(new300, new100, n_misses)).abs();
|
||||
|
||||
if dist < best_dist {
|
||||
best_dist = dist;
|
||||
n300 = new300;
|
||||
n100 = new100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -260,19 +256,23 @@ impl<'map> TaikoPP<'map> {
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => match (self.n300, self.n100) {
|
||||
(Some(_), None) => n100 = remaining,
|
||||
(Some(_), Some(_)) => n300 += remaining,
|
||||
(None, _) => n300 = remaining,
|
||||
},
|
||||
HitResultPriority::WorstCase => match (self.n300, self.n100) {
|
||||
(None, Some(_)) => n300 = remaining,
|
||||
(Some(_), Some(_)) => n100 += remaining,
|
||||
(_, None) => n100 = remaining,
|
||||
_ => n300 += remaining,
|
||||
},
|
||||
HitResultPriority::WorstCase => match (self.n100, self.n300) {
|
||||
(None, _) => n100 = remaining,
|
||||
(_, None) => n300 = remaining,
|
||||
_ => n100 += remaining,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let max_combo = self.combo.map_or(max_combo, |combo| combo.min(max_combo));
|
||||
let max_possible_combo = max_combo.saturating_sub(n_misses);
|
||||
|
||||
let max_combo = self
|
||||
.combo
|
||||
.map_or(max_possible_combo, |combo| combo.min(max_possible_combo));
|
||||
|
||||
TaikoScoreState {
|
||||
max_combo,
|
||||
@@ -281,6 +281,40 @@ impl<'map> TaikoPP<'map> {
|
||||
n_misses,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate all performance related values, including pp and stars.
|
||||
pub fn calculate(mut self) -> TaikoPerformanceAttributes {
|
||||
let state = self.generate_state();
|
||||
|
||||
let attrs = self
|
||||
.attributes
|
||||
.take()
|
||||
.unwrap_or_else(|| self.generate_attributes());
|
||||
|
||||
let inner = TaikoPpInner {
|
||||
mods: self.mods,
|
||||
state,
|
||||
attrs,
|
||||
};
|
||||
|
||||
inner.calculate()
|
||||
}
|
||||
|
||||
fn generate_attributes(&self) -> TaikoDifficultyAttributes {
|
||||
let mut calculator = TaikoStars::new(self.map.as_ref())
|
||||
.mods(self.mods)
|
||||
.is_convert(self.is_convert);
|
||||
|
||||
if let Some(passed_objects) = self.passed_objects {
|
||||
calculator = calculator.passed_objects(passed_objects);
|
||||
}
|
||||
|
||||
if let Some(clock_rate) = self.clock_rate {
|
||||
calculator = calculator.clock_rate(clock_rate);
|
||||
}
|
||||
|
||||
calculator.calculate()
|
||||
}
|
||||
}
|
||||
|
||||
struct TaikoPpInner {
|
||||
@@ -437,6 +471,15 @@ impl<'map> From<OsuPP<'map>> for TaikoPP<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
fn accuracy(n300: usize, n100: usize, n_misses: usize) -> f64 {
|
||||
debug_assert_ne!(n300 + n100 + n_misses, 0);
|
||||
|
||||
let numerator = 2 * n300 + n100;
|
||||
let denominator = 2 * (n300 + n100 + n_misses);
|
||||
|
||||
numerator as f64 / denominator as f64
|
||||
}
|
||||
|
||||
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
|
||||
pub trait TaikoAttributeProvider {
|
||||
/// Provide the actual difficulty attributes.
|
||||
@@ -486,36 +529,149 @@ impl TaikoAttributeProvider for PerformanceAttributes {
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::Beatmap;
|
||||
use proptest::{option, prelude::*};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
fn test_data() -> (Beatmap, TaikoDifficultyAttributes) {
|
||||
let path = "./maps/1028484.osu";
|
||||
let map = Beatmap::from_path(path).unwrap();
|
||||
static DATA: OnceLock<(Beatmap, TaikoDifficultyAttributes)> = OnceLock::new();
|
||||
|
||||
let attrs = TaikoDifficultyAttributes {
|
||||
stamina: 1.4528845068865617,
|
||||
rhythm: 0.20130047251681948,
|
||||
colour: 1.0487315549761433,
|
||||
peak: 1.8881824429738323,
|
||||
hit_window: 35.0,
|
||||
stars: 2.9778030386845606,
|
||||
max_combo: 289,
|
||||
const MAX_COMBO: usize = 289;
|
||||
|
||||
fn test_data() -> (&'static Beatmap, TaikoDifficultyAttributes) {
|
||||
let (map, attrs) = DATA.get_or_init(|| {
|
||||
let path = "./maps/1028484.osu";
|
||||
let map = Beatmap::from_path(path).unwrap();
|
||||
let attrs = TaikoStars::new(&map).calculate();
|
||||
|
||||
assert_eq!(MAX_COMBO, attrs.max_combo);
|
||||
|
||||
(map, attrs)
|
||||
});
|
||||
|
||||
(map, attrs.to_owned())
|
||||
}
|
||||
|
||||
/// Checks all remaining hitresult combinations w.r.t. the given parameters
|
||||
/// and returns the [`TaikoScoreState`] that matches `acc` the best.
|
||||
///
|
||||
/// Very slow but accurate.
|
||||
fn brute_force_best(
|
||||
acc: f64,
|
||||
n300: Option<usize>,
|
||||
n100: Option<usize>,
|
||||
n_misses: usize,
|
||||
best_case: bool,
|
||||
) -> TaikoScoreState {
|
||||
let n_misses = n_misses.min(MAX_COMBO);
|
||||
|
||||
let mut best_state = TaikoScoreState {
|
||||
n_misses,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
(map, attrs)
|
||||
let mut best_dist = f64::INFINITY;
|
||||
|
||||
let n_objects = MAX_COMBO;
|
||||
let n_remaining = n_objects - n_misses;
|
||||
|
||||
let (min_n300, max_n300) = match (n300, n100) {
|
||||
(Some(n300), _) => (n_remaining.min(n300), n_remaining.min(n300)),
|
||||
(None, Some(n100)) => (
|
||||
n_remaining.saturating_sub(n100),
|
||||
n_remaining.saturating_sub(n100),
|
||||
),
|
||||
(None, None) => (0, n_remaining),
|
||||
};
|
||||
|
||||
for new300 in min_n300..=max_n300 {
|
||||
let new100 = match n100 {
|
||||
Some(n100) => n_remaining.min(n100),
|
||||
None => n_remaining - new300,
|
||||
};
|
||||
|
||||
let curr_acc = accuracy(new300, new100, n_misses);
|
||||
let curr_dist = (acc - curr_acc).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
best_state.n300 = new300;
|
||||
best_state.n100 = new100;
|
||||
}
|
||||
}
|
||||
|
||||
if best_state.n300 + best_state.n100 < n_remaining {
|
||||
let remaining = n_remaining - (best_state.n300 + best_state.n100);
|
||||
|
||||
if best_case {
|
||||
best_state.n300 += remaining;
|
||||
} else {
|
||||
best_state.n100 += remaining;
|
||||
}
|
||||
}
|
||||
|
||||
best_state
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig::with_cases(20_000))]
|
||||
#[test]
|
||||
fn taiko_hitresults(
|
||||
acc in 0.0..=1.0,
|
||||
n300 in option::weighted(0.10, 0_usize..=MAX_COMBO + 10),
|
||||
n100 in option::weighted(0.10, 0_usize..=MAX_COMBO + 10),
|
||||
n_misses in option::weighted(0.15, 0_usize..=MAX_COMBO + 10),
|
||||
best_case in prop::bool::ANY,
|
||||
) {
|
||||
let (map, attrs) = test_data();
|
||||
|
||||
let priority = if best_case {
|
||||
HitResultPriority::BestCase
|
||||
} else {
|
||||
HitResultPriority::WorstCase
|
||||
};
|
||||
|
||||
let mut state = TaikoPP::new(map)
|
||||
.attributes(attrs)
|
||||
.accuracy(acc * 100.0)
|
||||
.hitresult_priority(priority);
|
||||
|
||||
if let Some(n300) = n300 {
|
||||
state = state.n300(n300);
|
||||
}
|
||||
|
||||
if let Some(n100) = n100 {
|
||||
state = state.n100(n100);
|
||||
}
|
||||
|
||||
if let Some(n_misses) = n_misses {
|
||||
state = state.n_misses(n_misses);
|
||||
}
|
||||
|
||||
let state = state.generate_state();
|
||||
|
||||
let mut expected = brute_force_best(
|
||||
acc,
|
||||
n300,
|
||||
n100,
|
||||
n_misses.unwrap_or(0),
|
||||
best_case,
|
||||
);
|
||||
expected.max_combo = MAX_COMBO.saturating_sub(n_misses.unwrap_or(0));
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n300_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = TaikoPP::new(&map)
|
||||
let state = TaikoPP::new(map)
|
||||
.attributes(attrs)
|
||||
.combo(100)
|
||||
.n300(150)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
.generate_state();
|
||||
|
||||
let expected = TaikoScoreState {
|
||||
max_combo: 100,
|
||||
@@ -530,14 +686,13 @@ mod test {
|
||||
#[test]
|
||||
fn hitresults_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = TaikoPP::new(&map)
|
||||
let state = TaikoPP::new(map)
|
||||
.attributes(attrs)
|
||||
.combo(100)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
.generate_state();
|
||||
|
||||
let expected = TaikoScoreState {
|
||||
max_combo: 100,
|
||||
@@ -548,33 +703,4 @@ mod test {
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n_misses_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = TaikoPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(100)
|
||||
.accuracy(97.2)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = TaikoScoreState {
|
||||
max_combo: 100,
|
||||
n300: 275,
|
||||
n100: 12,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +60,9 @@ impl_mode! {
|
||||
n_droplets: 2,
|
||||
n_tiny_droplets: 291,
|
||||
};
|
||||
Mania: 1974394, ManiaDifficultyAttributes {
|
||||
stars: 4.824631127426499,
|
||||
Mania: 1638954, ManiaDifficultyAttributes {
|
||||
stars: 3.441830819988125,
|
||||
hit_window: 40.0,
|
||||
max_combo: 5064,
|
||||
max_combo: 956,
|
||||
};
|
||||
}
|
||||
|
||||
+7
-7
@@ -159,20 +159,20 @@ fn assert_catch(map: Beatmap) {
|
||||
fn assert_mania(map: Beatmap) {
|
||||
assert_eq!(map.mode, GameMode::Mania);
|
||||
assert_eq!(map.version, 14);
|
||||
assert_eq!(map.n_circles, 2815);
|
||||
assert_eq!(map.n_sliders, 423);
|
||||
assert_eq!(map.n_circles, 473);
|
||||
assert_eq!(map.n_sliders, 121);
|
||||
assert_eq!(map.n_spinners, 0);
|
||||
assert!((map.ar - 5.0).abs() <= f32::EPSILON);
|
||||
assert!((map.od - 8.0).abs() <= f32::EPSILON);
|
||||
assert!((map.cs - 4.0).abs() <= f32::EPSILON);
|
||||
assert!((map.hp - 9.0).abs() <= f32::EPSILON);
|
||||
assert!((map.hp - 8.0).abs() <= f32::EPSILON);
|
||||
assert!((map.slider_mult - 1.4).abs() <= f64::EPSILON);
|
||||
assert!((map.tick_rate - 1.0).abs() <= f64::EPSILON);
|
||||
assert_eq!(map.hit_objects.len(), 3238);
|
||||
assert_eq!(map.sounds.len(), 3238);
|
||||
assert_eq!(map.hit_objects.len(), 594);
|
||||
assert_eq!(map.sounds.len(), 594);
|
||||
assert_eq!(map.timing_points.len(), 1);
|
||||
assert_eq!(map.difficulty_points.len(), 1740);
|
||||
assert_eq!(map.effect_points.len(), 1762);
|
||||
assert_eq!(map.difficulty_points.len(), 0);
|
||||
assert_eq!(map.effect_points.len(), 1);
|
||||
assert!((map.stack_leniency - 0.7).abs() <= f32::EPSILON);
|
||||
assert_eq!(map.breaks.len(), 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user