1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6declare(strict_types=1);
7
8namespace Tests\Models\Solo;
9
10use App\Exceptions\InvariantException;
11use App\Models\Solo\Score;
12use stdClass;
13use Tests\TestCase;
14
15class ScoreTest extends TestCase
16{
17 public function testStatisticsStoredInCorrectCasing()
18 {
19 $score = Score::createFromJsonOrExplode([
20 'accuracy' => 1,
21 'beatmap_id' => 1,
22 'ended_at' => json_time(now()),
23 'max_combo' => 100,
24 'mods' => [],
25 'passed' => true,
26 'rank' => 'S',
27 'ruleset_id' => 1,
28 'statistics' => ['Great' => 10, 'SmallTickHit' => 1],
29 'total_score' => 1000,
30 'user_id' => 1,
31 ]);
32
33 $score = $score->fresh();
34 $this->assertSame(1, json_decode($score->getAttributes()['data'], true)['statistics']['small_tick_hit']);
35 $this->assertSame(1, $score->data->statistics->small_tick_hit);
36 }
37
38 public function testLegacyPassScoreSetsRank()
39 {
40 $score = Score::createFromJsonOrExplode([
41 'accuracy' => 1,
42 'beatmap_id' => 1,
43 'ended_at' => json_time(now()),
44 'max_combo' => 100,
45 'mods' => [],
46 'passed' => true,
47 'rank' => 'S',
48 'ruleset_id' => 1,
49 'statistics' => ['great' => 10],
50 'total_score' => 1000,
51 'user_id' => 1,
52 ]);
53
54 $this->assertTrue($score->passed);
55 $this->assertSame($score->rank, 'S');
56
57 $legacy = $score->makeLegacyEntry();
58
59 $this->assertSame($legacy->rank, 'X');
60 }
61
62 public function testLegacyFailScoreIsRankF()
63 {
64 $score = Score::createFromJsonOrExplode([
65 'accuracy' => 1,
66 'beatmap_id' => 1,
67 'ended_at' => json_time(now()),
68 'max_combo' => 100,
69 'mods' => [],
70 'passed' => false,
71 'rank' => 'S', // lazer may send an incorrect rank for a failed score.
72 'ruleset_id' => 1,
73 'statistics' => ['great' => 10],
74 'total_score' => 1000,
75 'user_id' => 1,
76 ]);
77
78 $this->assertFalse($score->passed);
79 $this->assertSame($score->rank, 'F');
80
81 $legacy = $score->makeLegacyEntry();
82
83 $this->assertSame($legacy->rank, 'F');
84 }
85
86 public function testLegacyScoreHitCounts()
87 {
88 $legacy = Score::createFromJsonOrExplode([
89 'accuracy' => 1,
90 'beatmap_id' => 1,
91 'ended_at' => json_time(now()),
92 'max_combo' => 100,
93 'mods' => [],
94 'passed' => true,
95 'rank' => 'S',
96 'ruleset_id' => 0,
97 'statistics' => ['great' => 10, 'ok' => 20, 'meh' => 30, 'miss' => 40],
98 'total_score' => 1000,
99 'user_id' => 1,
100 ])->makeLegacyEntry();
101
102 $this->assertSame($legacy->count300, 10);
103 $this->assertSame($legacy->count100, 20);
104 $this->assertSame($legacy->count50, 30);
105 $this->assertSame($legacy->countmiss, 40);
106 }
107
108 public function testLegacyScoreHitCountsFromStudlyCaseStatistics()
109 {
110 $legacy = Score::createFromJsonOrExplode([
111 'accuracy' => 1,
112 'beatmap_id' => 1,
113 'ended_at' => json_time(now()),
114 'max_combo' => 100,
115 'mods' => [],
116 'passed' => true,
117 'rank' => 'S',
118 'ruleset_id' => 0,
119 'statistics' => ['Great' => 10, 'Ok' => 20, 'Meh' => 30, 'Miss' => 40],
120 'total_score' => 1000,
121 'user_id' => 1,
122 ])->makeLegacyEntry();
123
124 $this->assertSame($legacy->count300, 10);
125 $this->assertSame($legacy->count100, 20);
126 $this->assertSame($legacy->count50, 30);
127 $this->assertSame($legacy->countmiss, 40);
128 }
129
130 public function testModsPropertyType()
131 {
132 $score = new Score([
133 'beatmap_id' => 0,
134 'data' => [
135 'mods' => [['acronym' => 'DT']],
136 ],
137 'ended_at' => json_time(now()),
138 'ruleset_id' => 0,
139 'user_id' => 0,
140 ]);
141
142 $this->assertTrue($score->data->mods[0] instanceof stdClass, 'mods entry should be of type stdClass');
143 }
144
145 public function testWeightedPp(): void
146 {
147 $pp = 10;
148 $weight = 0.5;
149 $score = Score::factory()->create(['pp' => $pp]);
150 $score->weight = $weight;
151
152 $this->assertSame($score->weightedPp(), $pp * $weight);
153 }
154
155 public function testWeightedPpWithoutPerformance(): void
156 {
157 $score = Score::factory()->create(['pp' => null]);
158 $score->weight = 0.5;
159
160 $this->assertNull($score->weightedPp());
161 }
162
163 public function testWeightedPpWithoutWeight(): void
164 {
165 $score = Score::factory()->create(['pp' => 10]);
166
167 $this->assertNull($score->weightedPp());
168 }
169
170 public function testNegativeTotalScoreIsNotAccepted()
171 {
172 $this->expectException(InvariantException::class);
173 $this->expectExceptionMessage('Invalid total_score.');
174
175 Score::createFromJsonOrExplode([
176 'accuracy' => 1,
177 'beatmap_id' => 1,
178 'ended_at' => json_time(now()),
179 'max_combo' => 100,
180 'mods' => [],
181 'passed' => true,
182 'rank' => 'S',
183 'ruleset_id' => 1,
184 'statistics' => ['Great' => 10, 'SmallTickHit' => 1],
185 'total_score' => -1000,
186 'user_id' => 1,
187 ]);
188 }
189
190 public function testNegativeAccuracyIsNotAccepted()
191 {
192 $this->expectException(InvariantException::class);
193 $this->expectExceptionMessage('Invalid accuracy.');
194
195 Score::createFromJsonOrExplode([
196 'accuracy' => -1,
197 'beatmap_id' => 1,
198 'ended_at' => json_time(now()),
199 'max_combo' => 100,
200 'mods' => [],
201 'passed' => true,
202 'rank' => 'S',
203 'ruleset_id' => 1,
204 'statistics' => ['Great' => 10, 'SmallTickHit' => 1],
205 'total_score' => 1000,
206 'user_id' => 1,
207 ]);
208 }
209
210 public function testAccuracyAboveOneIsNotAccepted()
211 {
212 $this->expectException(InvariantException::class);
213 $this->expectExceptionMessage('Invalid accuracy.');
214
215 Score::createFromJsonOrExplode([
216 'accuracy' => 2,
217 'beatmap_id' => 1,
218 'ended_at' => json_time(now()),
219 'max_combo' => 100,
220 'mods' => [],
221 'passed' => true,
222 'rank' => 'S',
223 'ruleset_id' => 1,
224 'statistics' => ['Great' => 10, 'SmallTickHit' => 1],
225 'total_score' => 1000,
226 'user_id' => 1,
227 ]);
228 }
229
230 public function testNegativeTotalScoreWithoutModsIsNotAccepted()
231 {
232 $this->expectException(InvariantException::class);
233 $this->expectExceptionMessage('Invalid total_score_without_mods.');
234
235 Score::createFromJsonOrExplode([
236 'accuracy' => 1,
237 'beatmap_id' => 1,
238 'ended_at' => json_time(now()),
239 'max_combo' => 100,
240 'mods' => [],
241 'passed' => true,
242 'rank' => 'S',
243 'ruleset_id' => 1,
244 'statistics' => ['Great' => 10, 'SmallTickHit' => 1],
245 'total_score' => 1000,
246 'total_score_without_mods' => -1000,
247 'user_id' => 1,
248 ]);
249 }
250}