Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user season score calculation workflow #11768

Open
wants to merge 39 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
913b824
add season user total score calculation workflow
venix12 Dec 26, 2024
a704104
return type on user relation
venix12 Dec 26, 2024
398ad6d
check total score before querying season count
venix12 Dec 26, 2024
1cc8f42
remove unused import
venix12 Dec 26, 2024
db1de9c
use triple equal sign
venix12 Dec 26, 2024
77a94df
one more equal
venix12 Dec 26, 2024
cf3bc24
newline on function opening brace
venix12 Dec 26, 2024
dced08a
use float for total score instead
venix12 Dec 27, 2024
d723a85
check factor count before calculation
venix12 Dec 27, 2024
08b4b34
add tests
venix12 Dec 27, 2024
ea6bd9b
strict types
venix12 Dec 27, 2024
cb30c70
make parent_id unsigned
venix12 Jan 3, 2025
61ef3dc
alphabetize
venix12 Jan 3, 2025
4751d8e
move score factors to seasons database column
venix12 Jan 12, 2025
bade39a
group rooms in SeasonRoom instead of parent_id
venix12 Jan 12, 2025
086461f
UserSeasonScore -> UserSeasonScoreAggregate
venix12 Jan 12, 2025
68240dc
mute exception for Room::completePlay() usage
venix12 Jan 12, 2025
921b989
clean-up properties
venix12 Jan 12, 2025
66283ac
remove redundant function
venix12 Jan 13, 2025
a551a10
delete factor cache key from config
venix12 Jan 18, 2025
84779f5
remove factors cache key from .env
venix12 Jan 20, 2025
eba3c08
use hasOneThrough relationship on room season
venix12 Jan 22, 2025
0063dd6
various calculate() improvements
venix12 Jan 22, 2025
fa929d3
use proper base model for SeasonRoom
venix12 Jan 22, 2025
0ea73f2
define relations in SeasonRoomFactory
venix12 Jan 22, 2025
c7a9801
simplify score user ids query
venix12 Jan 22, 2025
e51885a
associate season
venix12 Jan 22, 2025
539a0b2
missing licence header
venix12 Jan 22, 2025
910d77b
missing strict types declarations
venix12 Jan 22, 2025
aff7ed0
Merge branch 'master' into seasons-score-calculation
venix12 Jan 22, 2025
6643d4b
use proper foreign key for season relationship
venix12 Jan 22, 2025
190cb84
proper argument order for assertSame
venix12 Jan 23, 2025
f1bf951
more logical createRoomWithPlay() argument order
venix12 Jan 23, 2025
e8f0dc1
use static instead
venix12 Jan 23, 2025
1b72c3d
redate migrations
venix12 Jan 23, 2025
b859bd5
one space too much
venix12 Jan 23, 2025
57b7441
use double for total_score column instead
venix12 Jan 25, 2025
0638d90
use proper table name for drop
venix12 Jan 25, 2025
3cc049e
use empty array if factors are not set
venix12 Jan 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions app/Console/Commands/UserSeasonScoresRecalculate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

namespace App\Console\Commands;

use App\Models\Multiplayer\UserScoreAggregate;
use App\Models\Season;
use App\Models\User;
use Illuminate\Console\Command;

class UserSeasonScoresRecalculate extends Command
{
protected $signature = 'user-season-scores:recalculate {--season-id=}';
protected $description = 'Recalculate user scores for all active seasons or a specified season.';

public function handle(): void
{
$seasonId = $this->option('season-id');

if (present($seasonId)) {
$this->recalculate(Season::findOrFail(get_int($seasonId)));
} else {
$activeSeasons = Season::active()->get();

foreach ($activeSeasons as $season) {
$this->recalculate($season);
}
}
}

protected function recalculate(Season $season): void
{
$scoreUserIds = UserScoreAggregate::whereIn('room_id', $season->rooms->pluck('id'))
->distinct('user_id')
->pluck('user_id');

$bar = $this->output->createProgressBar($scoreUserIds->count());

User::whereIn('user_id', $scoreUserIds)
->chunkById(100, function ($userChunk) use ($bar, $season) {
foreach ($userChunk as $user) {
$seasonScore = $user->seasonScores()
->where('season_id', $season->getKey())
->firstOrNew();

$seasonScore->season()->associate($season);
$seasonScore->calculate(false);
$seasonScore->save();

$bar->advance();
}
});

$bar->finish();
}
}
26 changes: 22 additions & 4 deletions app/Models/Multiplayer/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
Expand All @@ -41,7 +42,7 @@
* @property int $participant_count
* @property \Illuminate\Database\Eloquent\Collection $playlist PlaylistItem
* @property \Illuminate\Database\Eloquent\Collection $scoreLinks ScoreLink
* @property-read Collection<\App\Models\Season> $seasons
* @property-read Season $season
* @property \Carbon\Carbon $starts_at
* @property \Carbon\Carbon|null $updated_at
* @property int $user_id
Expand Down Expand Up @@ -186,7 +187,7 @@ public static function search(array $rawParams, ?int $maxLimit = null)
}

if (isset($seasonId)) {
$query->whereRelation('seasons', 'seasons.id', $seasonId);
$query->whereRelation('season', 'season_id', $seasonId);
}

if (in_array($category, static::CATEGORIES, true)) {
Expand Down Expand Up @@ -259,9 +260,16 @@ public function scoreLinks()
return $this->hasMany(ScoreLink::class);
}

public function seasons()
public function season(): HasOneThrough
{
return $this->belongsToMany(Season::class, SeasonRoom::class);
return $this->hasOneThrough(
Season::class,
SeasonRoom::class,
'room_id',
'id',
'id',
'season_id',
);
}

public function userHighScores()
Expand Down Expand Up @@ -458,6 +466,16 @@ public function completePlay(ScoreToken $scoreToken, array $params): ScoreLink
$stats->save();
}

if ($this->category === 'spotlight' && $agg->total_score > 0 && $this->season !== null) {
$seasonScore = $user->seasonScores()
->where('season_id', $this->season->getKey())
->firstOrNew();

$seasonScore->season()->associate($this->season);
$seasonScore->calculate();
$seasonScore->save();
}

return $scoreLink;
});
}
Expand Down
7 changes: 7 additions & 0 deletions app/Models/Season.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@
* @property bool $finalised
* @property string $name
* @property-read Collection<Multiplayer\Room> $rooms
* @property float[]|null $score_factors
* @property string|null $url
*/
class Season extends Model
{
protected $casts = [
'finalised' => 'boolean',
'score_factors' => 'array',
];

public function scopeActive($query)
{
return $query->where('finalised', false);
}

public static function latestOrId($id)
{
if ($id === 'latest') {
Expand Down
3 changes: 1 addition & 2 deletions app/Models/SeasonRoom.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

/**
* @property string|null $group_indicator
* @property int $id
* @property int $room_id
* @property int $season_id
Expand Down
6 changes: 6 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
* @property-read Collection<Score\Mania> $scoresMania
* @property-read Collection<Score\Osu> $scoresOsu
* @property-read Collection<Score\Taiko> $scoresTaiko
* @property-read Collection<UserSeasonScoreAggregate> $seasonScores
* @property-read UserStatistics\Fruits|null $statisticsFruits
* @property-read UserStatistics\Mania|null $statisticsMania
* @property-read UserStatistics\Mania4k|null $statisticsMania4k
Expand Down Expand Up @@ -1359,6 +1360,11 @@ public function country()
return $this->belongsTo(Country::class, 'country_acronym');
}

public function seasonScores(): HasMany
{
return $this->hasMany(UserSeasonScoreAggregate::class);
}

public function statisticsOsu()
{
return $this->hasOne(UserStatistics\Osu::class);
Expand Down
69 changes: 69 additions & 0 deletions app/Models/UserSeasonScoreAggregate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

namespace App\Models;

use App\Exceptions\InvariantException;
use App\Models\Multiplayer\UserScoreAggregate;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

/**
* @property-read Season $season
* @property int $season_id
* @property float $total_score
* @property int $user_id
*/
class UserSeasonScoreAggregate extends Model
{
public $incrementing = false;
public $timestamps = false;

protected $primaryKey = ':composite';
protected $primaryKeys = ['user_id', 'season_id'];

public function calculate(bool $muteExceptions = true): void
{
$seasonRooms = SeasonRoom::where('season_id', $this->season->getKey())->get();
$userScores = UserScoreAggregate::whereIn('room_id', $seasonRooms->pluck('room_id'))
->where('user_id', $this->user_id)
->get();

$factors = $this->season->score_factors ?? [];
$roomGroupCount = $seasonRooms->groupBy('group_indicator')->count();

if ($roomGroupCount > count($factors)) {
// don't interrupt Room::completePlay() and throw exception only for recalculation command
if ($muteExceptions) {
return;
} else {
throw new InvariantException(osu_trans('rankings.seasons.validation.not_enough_factors'));
}
}

$roomsById = $seasonRooms->keyBy('room_id');
$scores = [];
foreach ($userScores as $score) {
$group = $roomsById[$score->room_id]->group_indicator;
$scores[$group] = max($scores[$group] ?? 0, $score->total_score);
}

rsort($factors);
rsort($scores);

$total = 0;
foreach ($scores as $index => $score) {
$total += $score * $factors[$index];
}

$this->total_score = $total;
}

public function season(): BelongsTo
{
return $this->belongsTo(Season::class);
}
}
25 changes: 25 additions & 0 deletions database/factories/SeasonRoomFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

namespace Database\Factories;

use App\Models\Multiplayer\Room;
use App\Models\Season;
use App\Models\SeasonRoom;

class SeasonRoomFactory extends Factory
{
protected $model = SeasonRoom::class;

public function definition(): array
{
return [
'room_id' => Room::factory(),
'season_id' => Season::factory(),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('season_rooms', function (Blueprint $table) {
$table->unique('room_id');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('season_rooms', function (Blueprint $table) {
$table->dropUnique(['room_id']);
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('seasons', function (Blueprint $table) {
$table->json('score_factors')->nullable();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('seasons', function (Blueprint $table) {
$table->dropColumn('score_factors');
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('season_rooms', function (Blueprint $table) {
$table->string('group_indicator')->nullable();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('season_rooms', function (Blueprint $table) {
$table->dropColumn('group_indicator');
});
}
};
Loading
Loading