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'); } }