* @category Class * @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License * @link www.splendidbear.org * @since 2026. 04. 09. */ class RpcManager implements RpcManagerInterface { private const int ROWS = 16; private const int COLS = 16; private const int MINES = 51; public function __construct( private readonly EntityManagerInterface $em, private readonly LoggerInterface $logger, private readonly PlayedGameRepository $playedGameRepository, private readonly StepRepository $stepRepository, ) { } public function getConnectInformation($params): string { $gameAssoc = is_array($params) ? $params[0] : $params; $playedGame = $this->playedGameRepository->findOneByGameAssoc($gameAssoc); if (null === $playedGame) { try { return base64_encode(json_encode([ 'users' => null, 'revealedCells' => null, 'lastStep' => ['red' => null, 'blue' => null], 'mostRecentStep' => null, 'redPoints' => 0, 'bluePoints' => 0, 'redBonusPoints' => 0, 'blueBonusPoints' => 0, 'redBonusStats' => [], 'blueBonusStats' => [], 'gameFinished' => false, ], JSON_THROW_ON_ERROR)); } catch (JsonException $e) { throw new RuntimeException($e->getMessage()); } } $users = $this->getUserCollection($playedGame); $revealedCells = $this->aggregateRevealedCells($playedGame); try { $redPoints = $playedGame->redPoints ?? 0; $bluePoints = $playedGame->bluePoints ?? 0; $gameFinished = $redPoints > 25 || $bluePoints > 25; return base64_encode(json_encode([ 'users' => $users, 'revealedCells' => $revealedCells, 'lastStep' => $this->getLastStepPerPlayer($playedGame), 'mostRecentStep' => $this->getMostRecentStep($playedGame), 'redPoints' => $redPoints, 'bluePoints' => $bluePoints, 'redBonusPoints' => $playedGame->redBonusPoints ?? 0, 'blueBonusPoints' => $playedGame->blueBonusPoints ?? 0, 'redBonusStats' => $playedGame->redBonusStats ?? [], 'blueBonusStats' => $playedGame->blueBonusStats ?? [], 'gameFinished' => $gameFinished, ], JSON_THROW_ON_ERROR)); } catch (JsonException $e) { throw new RuntimeException($e->getMessage()); } } /** * Get the most recent step of the game (if any). * Returns an array with player, row, col information or null if no steps exist. */ private function getMostRecentStep(PlayedGame $playedGame): ?array { try { return $this->stepToArray($this->stepRepository->findMostRecent($playedGame)); } catch (Exception $e) { $this->logger->error('Error getting most recent step: ' . $e->getMessage()); return null; } } public function saveGrid(string $gameAssoc): bool { $existingGame = $this->playedGameRepository->findOneByGameAssoc($gameAssoc); if (null !== $existingGame) { return true; } $grid2d = $this->generateGrid(); $playedGame = new PlayedGame(); $grid = new Grid(); try { foreach ($grid2d as $row) { $gridRow = new GridRow(); $gridRow->gridCol = $row; $gridRow->grid = $grid; $this->em->persist($gridRow); } $grid->playedGame = $playedGame; $this->em->persist($grid); $playedGame->gameAssoc = $gameAssoc; $playedGame->uuid = Uuid::fromString($gameAssoc); $playedGame->grid = $grid; $playedGame->created = new DateTime(); $playedGame->updated = new DateTime(); $this->em->persist($playedGame); $this->em->flush(); } catch (Exception $e) { $this->logger->error($e->getMessage()); } return true; } /** * Generate a random 16×16 grid with 51 mines and adjacent-mine numbers. */ private function generateGrid(): array { /** Build flat set: 51 mines ('m') + remaining water ('w') */ $set = array_merge( array_fill(0, self::MINES, 'm'), array_fill(0, self::ROWS * self::COLS - self::MINES, 'w'), ); /** * Fisher-Yates shuffle * * @see https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle */ for ($i = count($set) - 1; $i > 0; $i--) { try { $j = random_int(0, $i); } catch (RandomException $e) { throw new RuntimeException('Failed to generate random index: ' . $e->getMessage()); } [$set[$i], $set[$j]] = [$set[$j], $set[$i]]; } /** Reshape to 2-D */ $grid = []; for ($r = 0; $r < self::ROWS; $r++) { $grid[$r] = array_slice($set, $r * self::COLS, self::COLS); } /** Replace 'w' with adjacent-mine count */ $dirs = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]; for ($r = 0; $r < self::ROWS; $r++) { for ($c = 0; $c < self::COLS; $c++) { if ('w' !== $grid[$r][$c]) { continue; } $count = 0; foreach ($dirs as [$dr, $dc]) { if (isset($grid[$r + $dr][$c + $dc]) && 'm' === $grid[$r + $dr][$c + $dc]) { $count++; } } $grid[$r][$c] = $count; } } return $grid; } /** * Collect all cells revealed so far, enriched with the player colour from each Step. */ private function aggregateRevealedCells(PlayedGame $playedGame): array { $all = []; foreach ($playedGame->steps as $step) { if (null === $step->revealedCells) { continue; } $player = $step->player; foreach ($step->revealedCells as $cell) { $all[] = array_merge($cell, ['player' => $player]); } } return $all; } /** * Get the last step for each player. * Returns an array with 'red' and 'blue' keys, each containing row, col information or null if no steps exist for * that player. */ private function getLastStepPerPlayer(PlayedGame $playedGame): array { try { return [ 'red' => $this->stepToArray($this->stepRepository->findMostRecentForPlayer($playedGame, 'red')), 'blue' => $this->stepToArray($this->stepRepository->findMostRecentForPlayer($playedGame, 'blue')), ]; } catch (Exception $e) { $this->logger->error('Error getting last step per player: ' . $e->getMessage()); return ['red' => null, 'blue' => null]; } } private function stepToArray(?Step $step): ?array { if (null === $step) { return null; } return [ 'player' => $step->player, 'row' => (int)$step->row, 'col' => (int)$step->col, ]; } private function getUserCollection(PlayedGame $playedGame): array { return [ 'red' => null !== $playedGame->red ? $playedGame->red->getUsername() : '', 'blue' => null !== $playedGame->blue ? $playedGame->blue->getUsername() : '', 'redAnon' => null !== $playedGame->redAnon ? $playedGame->redAnon->userName : '', 'blueAnon' => null !== $playedGame->blueAnon ? $playedGame->blueAnon->userName : '', ]; } }