Deathgarden_Rebirth-Rewrite/dist/app/Console/Commands/ProcessMatchmaking.php

269 lines
10 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Classes\Matchmaking\MatchmakingPlayerCount;
use App\Enums\Game\Matchmaking\MatchmakingSide;
use App\Enums\Game\Matchmaking\MatchStatus;
use App\Models\Admin\MatchmakingSettings;
use App\Models\Game\Matchmaking\Game;
use App\Models\Game\Matchmaking\MatchConfiguration;
use App\Models\Game\Matchmaking\QueuedPlayer;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Log;
use Psr\Log\LoggerInterface;
class ProcessMatchmaking extends Command
{
public static int $repeatTimeSeconds = 10;
const ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY = 'matchmaking_attempt_1v4_1v5';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'matchmaking:process';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
protected static LoggerInterface $log;
/**
* Execute the console command.
*/
public function handle(): void
{
$matchmakingSettings = MatchmakingSettings::get();
// Select all queued Players/party leaders, descending by party size
$players = QueuedPlayer::withCount('followingUsers')
->sharedLock()
->whereNull('queued_player_id')
->orderByDesc('following_users_count')
->orderBy('created_at')
->get();
// If there are no players in the queue, stop here.
if($players->isEmpty()){
static::log()->info('No Users in Queue, Stopping');
return;
}
$runners = new Collection();
$hunters = new Collection();
// Split hunters and runners into separate collections
$players->each(function (QueuedPlayer $player) use ($hunters, $runners) {
if($player->side === MatchmakingSide::Hunter)
$hunters->add($player);
else
$runners->add($player);
});
static::log()->info('Users in Queue:'. json_encode([
'hunters' => $hunters->toArray(),
'runners' => $runners->toArray(),
],
JSON_PRETTY_PRINT
));
$this->tryFillOpenGames($hunters, $runners);
static::log()->info('Users in Queue after trying to fill:'. json_encode([
'hunters' => $hunters->toArray(),
'runners' => $runners->toArray(),
],
JSON_PRETTY_PRINT
));
$playerCount = $this->getTotalPlayersCount($players);
// If we only have one Hunter and could make a 1v4 or 1v5 we want to wait a bit before making a match.
// because there could be some runners not being fast enough in queue or the matchmaking command running unfortunatly while players are queuing up.
if ($playerCount->hunters === 1 && ($playerCount->runners === 4 || $playerCount->runners === 5)) {
if (Cache::has(static::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY)) {
/** @var Carbon $firstAttempt */
$firstAttempt = Cache::get(static::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY);
if ($firstAttempt->diffInSeconds(Carbon::now()) < $matchmakingSettings->matchWaitingTime){
return;
}
Cache::forget(static::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY);
}
else {
Cache::set(static::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY, Carbon::now());
return;
}
}
else {
Cache::forget(static::ONE_VS_FOUR_AND_VS_FIVE_FIRST_ATTEMPT_CACHE_KEY);
}
$availableMatchConfigs = MatchConfiguration::getAvailableMatchConfigs($playerCount->runners, $playerCount->hunters);
if($availableMatchConfigs->isEmpty())
return;
$selectedConfig = MatchConfiguration::selectMatchConfig($availableMatchConfigs, $playerCount->runners, $playerCount->hunters);
// Should never happen, but just to be careful
if($selectedConfig === null)
return;
$hunterGroupsSet = $this->determineMatchingPlayers($hunters, $selectedConfig->hunters);
$runnerGroupsSet = $this->determineMatchingPlayers($runners, $selectedConfig->runners);
// if we cannot create a match with our current player groups, stop
if($runnerGroupsSet === false || $hunterGroupsSet === false)
return;
rsort($runnerGroupsSet, SORT_NUMERIC);
rsort($hunterGroupsSet, SORT_NUMERIC);
$newGame = new Game();
$newGame->status = MatchStatus::Created;
$newGame->matchConfiguration()->associate($selectedConfig);
$newGame->save();
static::log()->info('New game created: '. json_encode($newGame->toArray(), JSON_PRETTY_PRINT));
foreach ($hunterGroupsSet as $groupSize) {
$foundQueuedPlayerIndex = $hunters->search(function (QueuedPlayer $hunter) use ($groupSize) {
return ($hunter->following_users_count + 1) === $groupSize;
});
$foundHunter = $hunters->pull($foundQueuedPlayerIndex);
$newGame->addQueuedPlayer($foundHunter);
}
foreach ($runnerGroupsSet as $groupSize) {
$foundQueuedPlayerIndex = $runners->search(function (QueuedPlayer $runner) use ($groupSize) {
return ($runner->following_users_count + 1) === $groupSize;
});
$foundRunner = $runners->pull($foundQueuedPlayerIndex);
$newGame->addQueuedPlayer($foundRunner);
}
$newGame->determineHost();
}
protected function tryFillOpenGames(Collection|array &$hunters, Collection|array &$runners): void
{
$openGames = Game::where('status', '=', MatchStatus::Opened->value)->get();
static::log()->info('Found Open Matches:' . json_encode($openGames->toArray(),JSON_PRETTY_PRINT));
foreach ($openGames as $game) {
static::log()->info("Game $game->id current players: ". json_encode($game->players));
$neededPlayers = $game->remainingPlayerCount();
// game is full and doesn't need filling
if($neededPlayers->getTotal() == 0)
continue;
if($neededPlayers->hunters > 0) {
static::log()->info("Game $game->id needs a hunter");
$hunterGroupsSet = $this->determineMatchingPlayers($hunters, $neededPlayers->hunters);
// see if there are any group combinations possible to fill the game
if($hunterGroupsSet === false)
continue;
// use biggest groups first
rsort($hunterGroupsSet, SORT_NUMERIC);
static::log()->info("Game $game->id Try fill game group sets: ". json_encode($runnerGroupSet));
foreach ($hunterGroupsSet as $groupSize) {
$foundQueuedPlayerIndex = $hunters->search(function (QueuedPlayer $hunter) use ($groupSize) {
return ($hunter->following_users_count + 1) === $groupSize;
});
$foundHunter = $hunters->pull($foundQueuedPlayerIndex);
static::log()->info('Filled hunter slot on open game.'. json_encode([
'hunter' => $foundHunter,
'game' => $game,
],
JSON_PRETTY_PRINT)
);
$game->addQueuedPlayer($foundHunter);
}
}
if($neededPlayers->runners > 0) {
static::log()->info("Game $game->id needs a runner");
$runnerGroupSet = $this->determineMatchingPlayers($runners, $neededPlayers->runners);
// see if there are any group combinations possible to fill the game
if($runnerGroupSet === false)
continue;
// use biggest groups first
rsort($runnerGroupSet, SORT_NUMERIC);
static::log()->info("Game $game->id Try fill game group sets: ". json_encode($runnerGroupSet));
foreach ($runnerGroupSet as $groupSize) {
$foundQueuedPlayerIndex = $runners->search(function (QueuedPlayer $runner) use ($groupSize) {
return ($runner->following_users_count + 1) === $groupSize;
});
$foundRunner = $runners->pull($foundQueuedPlayerIndex);
static::log()->info('Filled runner slot on open game.'. json_encode([
'runner' => $foundRunner,
'game' => $game,
],
JSON_PRETTY_PRINT)
);
$game->addQueuedPlayer($foundRunner);
}
}
}
}
private function getTotalPlayersCount(Collection &$queuedPlayerCollection): MatchmakingPlayerCount
{
$count = new MatchmakingPlayerCount();
foreach ($queuedPlayerCollection as $player) {
/** @var QueuedPlayer $player */
if($player->side == MatchmakingSide::Hunter)
$count->hunters += $player->following_users_count + 1;
else
$count->runners += $player->following_users_count + 1;
}
return $count;
}
/**
* @param Collection $queuedPlayers
* @param int $target
* @return array|false
*/
private function determineMatchingPlayers(Collection &$queuedPlayers, int $target): array|false
{
$availableNumbers = [];
$queuedPlayers->each(function (QueuedPlayer $player) use (&$availableNumbers) {
$availableNumbers[] = $player->following_users_count + 1;
});
$result = MatchmakingPlayerCount::findSubsetsOfSum($availableNumbers, $target, true);
if(count($result) > 0)
return $result;
return false;
}
public static function log(): LoggerInterface
{
return static::$log ?? static::$log = Log::channel('matchmaking');
}
}