* @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 ROWS = 16; private const COLS = 16; private const MINES = 51; public function __construct( private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, ) { } public function getConnectInformation($params): string { $gameAssoc = is_array($params) ? $params[0] : $params; $playedGame = $this->entityManager ->getRepository(PlayedGame::class) ->findOneByGameAssoc($gameAssoc); if (null === $playedGame) { try { return base64_encode(json_encode([ 'users' => null, 'revealedCells' => null, ], JSON_THROW_ON_ERROR)); } catch (JsonException $e) { throw new RuntimeException($e->getMessage()); } } $users = $this->getUserCollection($playedGame); $revealedCells = $this->aggregateRevealedCells($playedGame); try { return base64_encode(json_encode([ 'users' => $users, 'revealedCells' => $revealedCells, ], JSON_THROW_ON_ERROR)); } catch (JsonException $e) { throw new RuntimeException($e->getMessage()); } } public function saveGrid(string $gameAssoc): bool { $existingGame = $this->entityManager ->getRepository(PlayedGame::class) ->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->setGridCol($row); $gridRow->setGrid($grid); $this->entityManager->persist($gridRow); } $grid->setPlayedGame($playedGame); $this->entityManager->persist($grid); $playedGame->setGameAssoc($gameAssoc); $playedGame->setGrid($grid); $playedGame->setCreated(new DateTime()); $playedGame->setUpdated(new DateTime()); $this->entityManager->persist($playedGame); $this->entityManager->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 for ($i = count($set) - 1; $i > 0; $i--) { $j = random_int(0, $i); [$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->getSteps() as $step) { if (null === $step->getRevealedCells()) { continue; } $player = $step->getPlayer(); foreach ($step->getRevealedCells() as $cell) { $all[] = array_merge($cell, ['player' => $player]); } } return $all; } private function getUserCollection(PlayedGame $playedGame): array { return [ 'red' => null !== $playedGame->getRed() ? $playedGame->getRed()->getUsername() : '', 'blue' => null !== $playedGame->getBlue() ? $playedGame->getBlue()->getUsername() : '', 'redAnon' => null !== $playedGame->getRedAnon() ? $playedGame->getRedAnon()->getUserName() : '', 'blueAnon' => null !== $playedGame->getBlueAnon()? $playedGame->getBlueAnon()->getUserName(): '', ]; } }