the browser-facing portion of osu!
at master 6.1 kB view raw
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 App\Models; 9 10use App\Models\Multiplayer\PlaylistItemUserHighScore; 11use Carbon\CarbonImmutable; 12use Ds\Set; 13use Illuminate\Database\Eloquent\Relations\BelongsTo; 14 15class DailyChallengeUserStats extends Model 16{ 17 const array INITIAL_VALUES = [ 18 'daily_streak_best' => 0, 19 'daily_streak_current' => 0, 20 'last_percentile_calculation' => '2000-01-01 00:00:00', 21 'last_update' => '2000-01-01 00:00:00', 22 'last_weekly_streak' => '2000-01-01 00:00:00', 23 'playcount' => 0, 24 'top_10p_placements' => 0, 25 'top_50p_placements' => 0, 26 'weekly_streak_best' => 0, 27 'weekly_streak_current' => 0, 28 ]; 29 30 public $incrementing = false; 31 public $timestamps = false; 32 33 protected $attributes = self::INITIAL_VALUES; 34 35 protected $casts = [ 36 'last_percentile_calculation' => 'datetime', 37 'last_update' => 'datetime', 38 'last_weekly_streak' => 'datetime', 39 ]; 40 protected $primaryKey = 'user_id'; 41 protected $table = 'daily_challenge_user_stats'; 42 43 public static function calculate(CarbonImmutable $date): void 44 { 45 $startTime = $date->startOfDay(); 46 $currentWeek = static::startOfWeek($startTime); 47 $previousWeek = $currentWeek->subWeeks(1); 48 // this function assumes one daily challenge per day and one playlist item per daily challenge 49 $playlist = Multiplayer\Room::dailyChallengeFor($startTime)?->playlist[0] ?? null; 50 51 if ($playlist === null) { 52 // or maybe do something with existing streaks 53 return; 54 } 55 56 $highScoresByUserId = $playlist 57 ->highScores() 58 ->passing() 59 ->get() 60 ->keyBy('user_id'); 61 $statsByUserId = static 62 ::where('last_weekly_streak', '>=', $previousWeek->subWeeks(1)) 63 ->orWhereIn('user_id', $highScoresByUserId->keys()) 64 ->get() 65 ->keyBy('user_id'); 66 $percentile = $playlist->scorePercentile(); 67 68 $userIds = new Set([...$statsByUserId->keys(), ...$highScoresByUserId->keys()]); 69 foreach ($userIds as $userId) { 70 $stats = $statsByUserId[$userId] ?? new static([ 71 'user_id' => $userId, 72 ]); 73 $highScore = $highScoresByUserId[$userId] ?? null; 74 75 $stats->updateStreak( 76 $highScore !== null, 77 $startTime, 78 currentWeek: $currentWeek, 79 previousWeek: $previousWeek, 80 ); 81 82 $stats->updatePercentile($percentile, $highScore, $startTime); 83 84 $stats->save(); 85 } 86 } 87 88 public static function startOfWeek(CarbonImmutable $date): CarbonImmutable 89 { 90 return $date->startOfWeek(CarbonImmutable::THURSDAY); 91 } 92 93 public function user(): BelongsTo 94 { 95 return $this->belongsTo(User::class, 'user_id'); 96 } 97 98 public function fix(): void 99 { 100 $highScores = PlaylistItemUserHighScore 101 ::where('user_id', $this->user_id) 102 ->whereRelation('playlistItem.room', 'category', 'daily_challenge') 103 ->passing() 104 ->with('playlistItem.room') 105 ->orderBy('created_at') 106 ->get(); 107 108 $this->fill(static::INITIAL_VALUES); 109 110 foreach ($highScores as $highScore) { 111 $playlistItem = $highScore->playlistItem; 112 $room = $playlistItem->room; 113 $startTime = $room->starts_at->toImmutable()->startOfDay(); 114 $this->updateStreak(true, $startTime); 115 if ($room->hasEnded()) { 116 $this->updatePercentile($playlistItem->scorePercentile(), $highScore, $startTime); 117 } 118 } 119 $streakBreakDay = CarbonImmutable::yesterday(); 120 if ($this->last_update < $streakBreakDay) { 121 $this->updateStreak(false, $streakBreakDay); 122 } 123 124 $this->saveOrExplode(); 125 } 126 127 public function updateStreak( 128 bool $incrementing, 129 CarbonImmutable $startTime, 130 ?CarbonImmutable $currentWeek = null, 131 ?CarbonImmutable $previousWeek = null 132 ): void { 133 $currentWeek ??= static::startOfWeek($startTime); 134 $previousWeek ??= $currentWeek->subWeek(1); 135 136 $lastUpdate = $this->last_update; 137 if ($lastUpdate >= $startTime) { 138 return; 139 } 140 141 if ($incrementing) { 142 $previousDay = $startTime->subDays(1); 143 144 if ($lastUpdate < $previousDay) { 145 $this->updateStreak(false, $previousDay); 146 } 147 148 $this->playcount += 1; 149 $this->daily_streak_current += 1; 150 $this->last_update = $startTime; 151 152 if ($this->last_weekly_streak < $currentWeek) { 153 $this->weekly_streak_current += 1; 154 $this->last_weekly_streak = $currentWeek; 155 } 156 157 foreach (['daily', 'weekly'] as $type) { 158 if ($this["{$type}_streak_best"] < $this["{$type}_streak_current"]) { 159 $this["{$type}_streak_best"] = $this["{$type}_streak_current"]; 160 } 161 } 162 } else { 163 $this->daily_streak_current = 0; 164 if ($this->last_weekly_streak < $previousWeek) { 165 $this->weekly_streak_current = 0; 166 } 167 } 168 } 169 170 private function updatePercentile( 171 array $playlistPercentile, 172 ?PlaylistItemUserHighScore $highScore, 173 CarbonImmutable $startTime 174 ): void { 175 if ($highScore === null || $this->last_percentile_calculation >= $startTime) { 176 return; 177 } 178 179 foreach ($playlistPercentile as $p => $totalScore) { 180 if ($highScore->total_score >= $totalScore) { 181 $this->{"{$p}_placements"}++; 182 } 183 } 184 $this->last_percentile_calculation = $startTime; 185 } 186}