Skip to content
Snippets Groups Projects
Unverified Commit e2ecf049 authored by Maxime FRIESS's avatar Maxime FRIESS :blue_heart:
Browse files

[game] Implemented ELO system

parent f0dca524
Branches
Tags
No related merge requests found
Pipeline #107140 passed with stages
in 59 seconds
......@@ -13,6 +13,7 @@
* type="object",
* @OA\Property(type="integer", property="id"),
* @OA\Property(type="integer", property="server_id"),
* @OA\Property(type="number", property="elo"),
* @OA\Property(type="string", property="created_at", format="date-time"),
* @OA\Property(type="string", property="updated_at", format="date-time"),
* )
......@@ -46,7 +47,7 @@ protected static function booted(): void
public function users()
{
return $this->belongsToMany(User::class, 'users_games', 'game_id', 'user_id')->using(UserGame::class)->withPivot("team_id", "rank");
return $this->belongsToMany(User::class, 'users_games', 'game_id', 'user_id')->using(UserGame::class)->withPivot("team_id", "rank", "elo");
}
public function server(): BelongsTo
......@@ -61,7 +62,7 @@ public function start()
$this->save();
}
public function end($user_data)
private function calculate_ranks($user_data)
{
// We first do the initial sorting to remove all oddities
// that the server could have added
......@@ -77,7 +78,9 @@ public function end($user_data)
$sorted_user_data[] = [
"id" => $user["id"],
"rank" => $k + 1,
"team_id" => $u->pivot->team_id
"team_id" => $u->pivot->team_id,
"elo" => $u->elo,
"game_elo" => 0
];
}
$user_data = $sorted_user_data;
......@@ -104,17 +107,82 @@ public function end($user_data)
$sorted_user_data = [];
foreach ($user_data as $k => $user) {
$u = $this->users()->findOrFail($user["id"]);
$sorted_user_data[] = [
"id" => $user["id"],
"rank" => $k + 1,
"team_id" => $user["team_id"]
];
$user["rank"] = $k + 1;
$sorted_user_data[] = $user;
}
$user_data = $sorted_user_data;
}
return $user_data;
}
private function probability_function($users, $user, $D, $N)
{
// We first calculate the probabilit of winning for the player.
// For player A agains all players i, R_x being the current ELO of x
// and N the number of players in a game, we have
// E_A = { sum from {1 <= i <= N, i <> A} {1 over {1 + 10^{(R_i-R_a) / D}}}} over { {N cdot (N - 1)} over 2 }
$E_A = 0;
foreach($users as $other) {
if ($user == $other)
continue;
$E_A += 1 / (1 + 10**(($other["elo"] - $user["elo"]) / $D));
}
return $E_A / ($N * ($N - 1) / 2);
}
private function score_function($user, $alpha, $N) {
// This function calculates the gain from the rank of the player
// S^exp_A ( p_A, %alpha ) = { a^{N - p_A} - 1 } over { sum from i = 1 to N {a^{N-i} - 1} }; %alpha in [ 0;+infinity ]
$top = ($alpha ** ($N - $user["rank"]) - 1);
$bottom = 0;
for($i = 1; $i <= $N; $i++) {
$bottom += $alpha ** ($N - $i) - 1;
}
return $top / $bottom;
}
private function calculate_elo($user_data)
{
$D = 400;
$K = 32;
$alpha = 2;
$N = count($user_data);
// We first calculate the probabilit of winning for each players.
// For player A agains all players i, R_x being the current ELO of x
// and N the number of players in a game, we have
// E_A = { sum from {1 <= i <= N, i <> A} {1 over {1 + 10^{(R_i-R_a) / D}}}} over { {N cdot (N - 1)} over 2 }
foreach($user_data as &$user)
{
$user["E_A"] = $this->probability_function($user_data, $user, $D, $N);
$user["new_elo"] = $user["elo"] + $K * ($N - 1) * ($this->score_function($user, $alpha, $N) - $user["E_A"]);
$user["game_elo"] = $user["new_elo"] - $user["elo"];
}
return $user_data;
}
public function end($user_data)
{
$user_data = $this->calculate_ranks($user_data);
if ($this->server->official)
{
$user_data = $this->calculate_elo($user_data);
}
foreach ($user_data as $user) {
$this->users()->updateExistingPivot($user['id'], ["rank" => $user["rank"]]);
$this->users()->updateExistingPivot($user['id'], ["rank" => $user["rank"], "elo" => $user["game_elo"]]);
$u = User::findOrFail($user["id"]);
$u->elo = $user["new_elo"];
$u->save();
}
$this->finished_at = Carbon::now();
......
......@@ -19,6 +19,7 @@
* @OA\Property(type="integer", property="id"),
* @OA\Property(type="string", property="username"),
* @OA\Property(type="integer", property="server_id", nullable=true),
* @OA\Property(type="number", property="elo"),
* @OA\Property(type="string", property="created_at", format="date-time"),
* @OA\Property(type="string", property="updated_at", format="date-time"),
* )
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->decimal("elo")->default(1000);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn("elo");
});
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users_games', function (Blueprint $table) {
$table->decimal("elo")->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users_games', function (Blueprint $table) {
$table->dropColumn("elo");
});
}
};
......@@ -14,7 +14,7 @@
class GameCreationTest extends TestCase
{
use RefreshDatabase;
public function test_good()
{
$user1 = User::create([
......@@ -34,13 +34,17 @@ public function test_good()
'password' => Hash::make('test'),
'owner_id' => $user1->id,
]);
$server->official = true;
$server->save();
$user1->server_id = $server->id;
$user1->server_state = 'connected';
$user1->elo = 1200;
$user1->save();
$user2->server_id = $server->id;
$user2->server_state = 'connected';
$user1->elo = 1000;
$user2->save();
$token = Auth::guard('server')->login($server);
......@@ -83,8 +87,10 @@ public function test_good()
]);
$response->assertStatus(200);
$this->assertEquals(Game::first()->users()->findOrFail($user1->id)->pivot->rank, 1);
$this->assertEquals(Game::first()->users()->findOrFail($user2->id)->pivot->rank, 2);
$this->assertEquals(1, Game::first()->users()->findOrFail($user1->id)->pivot->rank);
$this->assertGreaterThan(1200, Game::first()->users()->findOrFail($user1->id)->elo);
$this->assertEquals(2, Game::first()->users()->findOrFail($user2->id)->pivot->rank);
$this->assertLessThan(1000, Game::first()->users()->findOrFail($user2->id)->elo);
}
public function test_good_four_players()
......@@ -118,21 +124,27 @@ public function test_good_four_players()
'password' => Hash::make('test'),
'owner_id' => $user1->id,
]);
$server->official = true;
$server->save();
$user1->server_id = $server->id;
$user1->server_state = 'connected';
$user1->elo = 1200;
$user1->save();
$user2->server_id = $server->id;
$user2->server_state = 'connected';
$user1->elo = 1400;
$user2->save();
$user3->server_id = $server->id;
$user3->server_state = 'connected';
$user1->elo = 800;
$user3->save();
$user4->server_id = $server->id;
$user4->server_state = 'connected';
$user1->elo = 900;
$user4->save();
$token = Auth::guard('server')->login($server);
......@@ -222,6 +234,8 @@ public function test_good_four_players_teams()
'password' => Hash::make('test'),
'owner_id' => $user1->id,
]);
$server->official = true;
$server->save();
$user1->server_id = $server->id;
$user1->server_state = 'connected';
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment