Skip to content

Commit

Permalink
Merge pull request ppy#11363 from nanaya/daily-stats-instant
Browse files Browse the repository at this point in the history
Update daily challenge streak count on play instead of at the end of the day
  • Loading branch information
notbakaneko authored Jul 29, 2024
2 parents 8206e0e + e810913 commit d5beb7e
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 170 deletions.
71 changes: 45 additions & 26 deletions app/Models/DailyChallengeUserStats.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class DailyChallengeUserStats extends Model
];

protected $casts = [
'last_percentile_calculation' => 'datetime',
'last_update' => 'datetime',
'last_weekly_streak' => 'datetime',
];
Expand Down Expand Up @@ -62,9 +63,7 @@ public static function calculate(CarbonImmutable $date): void
$highScoresByUserId[$highScore->user_id] = $highScore;
}
$statsByUserId = static
::where(fn ($q) => $q
->where('last_weekly_streak', '>=', $previousWeek->subDays(1))
->where('last_update', '<', $startTime))
::where('last_weekly_streak', '>=', $previousWeek->subDays(1))
->orWhereIn('user_id', array_keys($highScoresByUserId))
->get()
->keyBy('user_id');
Expand All @@ -73,40 +72,25 @@ public static function calculate(CarbonImmutable $date): void
$stats = $statsByUserId[$userId] ?? new static([
'user_id' => $userId,
]);
// ignore processed scores
if (($stats->last_update ?? $previousWeek) >= $startTime) {
continue;
}
$highScore = $highScoresByUserId[$userId] ?? null;
if ($highScore === null) {
$stats->daily_streak_current = 0;
if ($stats->last_weekly_streak < $previousWeek) {
$stats->weekly_streak_current = 0;
}
} else {
$stats->playcount += 1;

$stats->daily_streak_current += 1;
if (($stats->last_weekly_streak ?? $previousWeek) < $currentWeek) {
$stats->weekly_streak_current += 1;
}
$stats->last_weekly_streak = $currentWeek;

foreach (['daily', 'weekly'] as $type) {
if ($stats["{$type}_streak_best"] < $stats["{$type}_streak_current"]) {
$stats["{$type}_streak_best"] = $stats["{$type}_streak_current"];
}
}
$stats->updateStreak(
$highScore !== null,
$startTime,
currentWeek: $currentWeek,
previousWeek: $previousWeek,
);

if ($highScore !== null && ($stats->last_percentile_calculation ?? $previousWeek) < $startTime) {
if ($highScore->total_score >= $top10p) {
$stats->top_10p_placements += 1;
}
if ($highScore->total_score >= $top50p) {
$stats->top_50p_placements += 1;
}
$stats->last_percentile_calculation = $startTime;
}

$stats->last_update = $startTime;
$stats->save();
}
}
Expand All @@ -120,4 +104,39 @@ public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}

public function updateStreak(
bool $incrementing,
CarbonImmutable $startTime,
?CarbonImmutable $currentWeek = null,
?CarbonImmutable $previousWeek = null
): void {
$currentWeek ??= static::startOfWeek($startTime);
$previousWeek ??= $currentWeek->subWeek(1);

if ($incrementing) {
if (($this->last_update ?? $previousWeek) < $startTime) {
$this->playcount += 1;
$this->daily_streak_current += 1;
}

if (($this->last_weekly_streak ?? $previousWeek) < $currentWeek) {
$this->weekly_streak_current += 1;
}
$this->last_weekly_streak = $currentWeek;

foreach (['daily', 'weekly'] as $type) {
if ($this["{$type}_streak_best"] < $this["{$type}_streak_current"]) {
$this["{$type}_streak_best"] = $this["{$type}_streak_current"];
}
}
} else {
$this->daily_streak_current = 0;
if ($this->last_weekly_streak === null || $this->last_weekly_streak < $previousWeek) {
$this->weekly_streak_current = 0;
}
}

$this->last_update = $startTime;
}
}
9 changes: 8 additions & 1 deletion app/Models/Multiplayer/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,14 @@ public function completePlay(ScoreToken $scoreToken, array $params): ScoreLink

return $this->getConnection()->transaction(function () use ($params, $scoreToken) {
$scoreLink = ScoreLink::complete($scoreToken, $params);
UserScoreAggregate::new($scoreLink->user, $this)->addScoreLink($scoreLink);
$user = $scoreLink->user;
$agg = UserScoreAggregate::new($user, $this);
$agg->addScoreLink($scoreLink);
if ($this->category === 'daily_challenge' && $agg->total_score > 0) {
$stats = $user->dailyChallengeUserStats()->firstOrNew();
$stats->updateStreak(true, $this->starts_at->toImmutable()->startOfDay());
$stats->save();
}

return $scoreLink;
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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('daily_challenge_user_stats', function (Blueprint $table) {
$table->timestamp('last_percentile_calculation')->nullable(true);
$table->timestamp('last_update')->useCurrent()->change();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('daily_challenge_user_stats', function (Blueprint $table) {
$table->dropColumn('last_percentile_calculation');
$table->timestamp('last_update')->useCurrent()->useCurrentOnUpdate()->change();
});
}
};
212 changes: 126 additions & 86 deletions tests/Models/DailyChallengeUserStatsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,92 +61,6 @@ public function testCalculateFromStart(): void
$this->assertTrue($playTime->equalTo($stats->last_update));
}

public function testCalculateTwiceADay(): void
{
$playTime = static::startOfWeek();
$playlistItem = static::preparePlaylistItem($playTime);

$user = User::factory()->create();
ScoreLink::factory()->passed()->create([
'playlist_item_id' => $playlistItem,
'user_id' => $user,
]);
UserScoreAggregate::new($user, $playlistItem->room)->save();

DailyChallengeUserStats::calculate($playTime);
DailyChallengeUserStats::calculate($playTime);

$stats = DailyChallengeUserStats::find($user->getKey());
$this->assertSame(1, $stats->playcount);
}

public function testCalculateIncrementAll(): void
{
$playTime = static::startOfWeek();
$playlistItem = static::preparePlaylistItem($playTime);

$user = User::factory()->create();
ScoreLink::factory()->passed()->create([
'playlist_item_id' => $playlistItem,
'user_id' => $user,
]);
UserScoreAggregate::new($user, $playlistItem->room)->save();

DailyChallengeUserStats::create([
'user_id' => $user->getKey(),
'playcount' => 1,
'daily_streak_current' => 1,
'daily_streak_best' => 1,
'weekly_streak_current' => 1,
'weekly_streak_best' => 1,
'top_10p_placements' => 1,
'top_50p_placements' => 1,
'last_weekly_streak' => $playTime->subWeeks(1),
'last_update' => $playTime->subDays(1),
]);

$this->expectCountChange(fn () => DailyChallengeUserStats::count(), 0);

DailyChallengeUserStats::calculate($playTime);

$stats = DailyChallengeUserStats::find($user->getKey());
$this->assertSame(2, $stats->daily_streak_current);
$this->assertSame(2, $stats->daily_streak_best);
$this->assertSame(2, $stats->weekly_streak_current);
$this->assertSame(2, $stats->weekly_streak_best);
$this->assertSame(2, $stats->top_10p_placements);
$this->assertSame(2, $stats->top_50p_placements);
$this->assertTrue($playTime->equalTo($stats->last_weekly_streak));
$this->assertTrue($playTime->equalTo($stats->last_update));
}

public function testCalculateIncrementWeeklyStreak(): void
{
$playTime = static::startOfWeek();
$playlistItem = static::preparePlaylistItem($playTime);

$user = User::factory()->create();
ScoreLink::factory()->passed()->create([
'playlist_item_id' => $playlistItem,
'user_id' => $user,
]);
UserScoreAggregate::new($user, $playlistItem->room)->save();

DailyChallengeUserStats::create([
'user_id' => $user->getKey(),
'weekly_streak_current' => 1,
'weekly_streak_best' => 1,
'last_weekly_streak' => $playTime->subWeeks(1),
'last_update' => $playTime->subDays(1),
]);
DailyChallengeUserStats::calculate($playTime);

$stats = DailyChallengeUserStats::find($user->getKey());
$this->assertSame(2, $stats->weekly_streak_current);
$this->assertSame(2, $stats->weekly_streak_best);
$this->assertTrue($playTime->equalTo($stats->last_weekly_streak));
}

public function testCalculateNoPlaysBreaksDailyStreak(): void
{
$playTime = static::startOfWeek();
Expand Down Expand Up @@ -231,4 +145,130 @@ public function testCalculatePercentile(): void
$this->assertSame($count50p, $stats->top_50p_placements, "i: {$i}");
}
}

public function testFlowFromStart(): void
{
$playTime = static::startOfWeek();
$playlistItem = static::preparePlaylistItem($playTime);
$user = User::factory()->create();

$this->expectCountChange(fn () => DailyChallengeUserStats::count(), 1);

$this->roomAddPlay($user, $playlistItem, ['passed' => true]);

$stats = DailyChallengeUserStats::find($user->getKey());
$this->assertSame(1, $stats->playcount);
$this->assertSame(1, $stats->daily_streak_current);
$this->assertSame(1, $stats->daily_streak_best);
$this->assertSame(1, $stats->weekly_streak_current);
$this->assertSame(1, $stats->weekly_streak_best);
$this->assertSame(0, $stats->top_10p_placements);
$this->assertSame(0, $stats->top_50p_placements);
$this->assertTrue($playTime->equalTo($stats->last_weekly_streak));
$this->assertTrue($playTime->equalTo($stats->last_update));

// increments percentile and nothing else
DailyChallengeUserStats::calculate($playTime);

$stats->refresh();
$this->assertSame(1, $stats->playcount);
$this->assertSame(1, $stats->daily_streak_current);
$this->assertSame(1, $stats->daily_streak_best);
$this->assertSame(1, $stats->weekly_streak_current);
$this->assertSame(1, $stats->weekly_streak_best);
$this->assertSame(1, $stats->top_10p_placements);
$this->assertSame(1, $stats->top_50p_placements);
$this->assertTrue($playTime->equalTo($stats->last_weekly_streak));
$this->assertTrue($playTime->equalTo($stats->last_update));
}

public function testFlowMultipleTimes(): void
{
$playTime = static::startOfWeek();
$playlistItem = static::preparePlaylistItem($playTime);
$user = User::factory()->create();

$this->roomAddPlay($user, $playlistItem, ['passed' => true]);
$this->roomAddPlay($user, $playlistItem, ['passed' => true]);

DailyChallengeUserStats::calculate($playTime);
DailyChallengeUserStats::calculate($playTime);

$stats = DailyChallengeUserStats::find($user->getKey());
$this->assertSame(1, $stats->playcount);
}

public function testFlowIncrementAll(): void
{
$playTime = static::startOfWeek();
$playlistItem = static::preparePlaylistItem($playTime);
$user = User::factory()->create();

DailyChallengeUserStats::create([
'user_id' => $user->getKey(),
'playcount' => 1,
'daily_streak_current' => 1,
'daily_streak_best' => 1,
'weekly_streak_current' => 1,
'weekly_streak_best' => 1,
'top_10p_placements' => 1,
'top_50p_placements' => 1,
'last_weekly_streak' => $playTime->subWeeks(1),
'last_update' => $playTime->subDays(1),
]);

$this->expectCountChange(fn () => DailyChallengeUserStats::count(), 0);

$this->roomAddPlay($user, $playlistItem, ['passed' => true]);
$stats = DailyChallengeUserStats::find($user->getKey());
$this->assertSame(2, $stats->daily_streak_current);
$this->assertSame(2, $stats->daily_streak_best);
$this->assertSame(2, $stats->weekly_streak_current);
$this->assertSame(2, $stats->weekly_streak_best);
$this->assertSame(1, $stats->top_10p_placements);
$this->assertSame(1, $stats->top_50p_placements);
$this->assertTrue($playTime->equalTo($stats->last_weekly_streak));
$this->assertTrue($playTime->equalTo($stats->last_update));

DailyChallengeUserStats::calculate($playTime);

$stats->refresh();
$this->assertSame(2, $stats->daily_streak_current);
$this->assertSame(2, $stats->daily_streak_best);
$this->assertSame(2, $stats->weekly_streak_current);
$this->assertSame(2, $stats->weekly_streak_best);
$this->assertSame(2, $stats->top_10p_placements);
$this->assertSame(2, $stats->top_50p_placements);
$this->assertTrue($playTime->equalTo($stats->last_weekly_streak));
$this->assertTrue($playTime->equalTo($stats->last_update));
}

public function testFlowIncrementWeeklyStreak(): void
{
$playTime = static::startOfWeek();
$playlistItem = static::preparePlaylistItem($playTime);
$user = User::factory()->create();

DailyChallengeUserStats::create([
'user_id' => $user->getKey(),
'weekly_streak_current' => 1,
'weekly_streak_best' => 1,
'last_weekly_streak' => $playTime->subWeeks(1),
'last_update' => $playTime->subDays(1),
]);

$this->roomAddPlay($user, $playlistItem, ['passed' => true]);

$stats = DailyChallengeUserStats::find($user->getKey());
$this->assertSame(2, $stats->weekly_streak_current);
$this->assertSame(2, $stats->weekly_streak_best);
$this->assertTrue($playTime->equalTo($stats->last_weekly_streak));

DailyChallengeUserStats::calculate($playTime);

$stats->refresh();
$this->assertSame(2, $stats->weekly_streak_current);
$this->assertSame(2, $stats->weekly_streak_best);
$this->assertTrue($playTime->equalTo($stats->last_weekly_streak));
}
}
Loading

0 comments on commit d5beb7e

Please sign in to comment.