2026-04-09 20:21:01 +02:00
|
|
|
|
<?php declare(strict_types=1);
|
2019-10-27 18:51:28 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* This file is part of the SplendidBear Websites' projects.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Copyright (c) 2019 @ www.splendidbear.org
|
|
|
|
|
|
*
|
|
|
|
|
|
* For the full copyright and license information, please view the LICENSE
|
|
|
|
|
|
* file that was distributed with this source code.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Util;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Entity\Grid;
|
|
|
|
|
|
use App\Entity\GridRow;
|
|
|
|
|
|
use App\Entity\PlayedGame;
|
2026-04-19 18:04:01 +02:00
|
|
|
|
use App\Entity\Step;
|
2026-04-09 20:21:01 +02:00
|
|
|
|
use App\Interfaces\RpcManagerInterface;
|
2026-04-12 08:01:46 +02:00
|
|
|
|
use App\Repository\PlayedGameRepository;
|
2026-04-19 18:04:01 +02:00
|
|
|
|
use App\Repository\StepRepository;
|
2019-10-27 18:51:28 +01:00
|
|
|
|
use DateTime;
|
|
|
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
|
|
|
|
use Exception;
|
2026-04-09 22:00:53 +02:00
|
|
|
|
use JsonException;
|
2019-10-27 18:51:28 +01:00
|
|
|
|
use Psr\Log\LoggerInterface;
|
2026-04-14 19:37:42 +02:00
|
|
|
|
use Random\RandomException;
|
2026-04-09 22:00:53 +02:00
|
|
|
|
use RuntimeException;
|
2026-04-14 19:37:42 +02:00
|
|
|
|
use Symfony\Component\Uid\Uuid;
|
2019-10-27 18:51:28 +01:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Class RpcManager
|
|
|
|
|
|
*
|
2026-04-09 20:21:01 +02:00
|
|
|
|
* @package App\Util
|
|
|
|
|
|
* @author Lang <https://www.splendidbear.org>
|
|
|
|
|
|
* @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.
|
2019-10-27 18:51:28 +01:00
|
|
|
|
*/
|
2026-04-09 22:00:53 +02:00
|
|
|
|
class RpcManager implements RpcManagerInterface
|
2019-10-27 18:51:28 +01:00
|
|
|
|
{
|
2026-04-19 18:04:01 +02:00
|
|
|
|
private const int ROWS = 16;
|
2026-04-14 19:37:42 +02:00
|
|
|
|
private const int COLS = 16;
|
|
|
|
|
|
private const int MINES = 51;
|
2026-04-10 12:23:21 +02:00
|
|
|
|
|
2026-04-09 20:21:01 +02:00
|
|
|
|
public function __construct(
|
2026-04-19 18:04:01 +02:00
|
|
|
|
private readonly EntityManagerInterface $em,
|
2026-04-09 22:00:53 +02:00
|
|
|
|
private readonly LoggerInterface $logger,
|
2026-04-12 08:01:46 +02:00
|
|
|
|
private readonly PlayedGameRepository $playedGameRepository,
|
2026-04-19 18:04:01 +02:00
|
|
|
|
private readonly StepRepository $stepRepository,
|
2026-04-09 20:21:01 +02:00
|
|
|
|
) {
|
2019-10-27 18:51:28 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function getConnectInformation($params): string
|
|
|
|
|
|
{
|
2026-04-09 12:10:37 +02:00
|
|
|
|
$gameAssoc = is_array($params) ? $params[0] : $params;
|
2026-04-10 12:23:21 +02:00
|
|
|
|
|
2026-04-12 08:01:46 +02:00
|
|
|
|
$playedGame = $this->playedGameRepository->findOneByGameAssoc($gameAssoc);
|
2026-04-10 12:23:21 +02:00
|
|
|
|
|
|
|
|
|
|
if (null === $playedGame) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return base64_encode(json_encode([
|
2026-04-19 18:04:01 +02:00
|
|
|
|
'users' => null,
|
|
|
|
|
|
'revealedCells' => null,
|
|
|
|
|
|
'lastStep' => ['red' => null, 'blue' => null],
|
|
|
|
|
|
'mostRecentStep' => null,
|
|
|
|
|
|
'redPoints' => 0,
|
|
|
|
|
|
'bluePoints' => 0,
|
|
|
|
|
|
'redBonusPoints' => 0,
|
|
|
|
|
|
'blueBonusPoints' => 0,
|
|
|
|
|
|
'redBonusStats' => [],
|
|
|
|
|
|
'blueBonusStats' => [],
|
|
|
|
|
|
'gameFinished' => false,
|
2026-04-10 12:23:21 +02:00
|
|
|
|
], JSON_THROW_ON_ERROR));
|
|
|
|
|
|
} catch (JsonException $e) {
|
|
|
|
|
|
throw new RuntimeException($e->getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-10 12:57:03 +02:00
|
|
|
|
$users = $this->getUserCollection($playedGame);
|
2026-04-10 12:23:21 +02:00
|
|
|
|
$revealedCells = $this->aggregateRevealedCells($playedGame);
|
2019-10-27 18:51:28 +01:00
|
|
|
|
|
2026-04-09 22:00:53 +02:00
|
|
|
|
try {
|
2026-04-19 18:04:01 +02:00
|
|
|
|
$redPoints = $playedGame->getRedPoints() ?? 0;
|
|
|
|
|
|
$bluePoints = $playedGame->getBluePoints() ?? 0;
|
|
|
|
|
|
$gameFinished = $redPoints > 25 || $bluePoints > 25;
|
|
|
|
|
|
|
2026-04-09 22:00:53 +02:00
|
|
|
|
return base64_encode(json_encode([
|
2026-04-19 18:04:01 +02:00
|
|
|
|
'users' => $users,
|
|
|
|
|
|
'revealedCells' => $revealedCells,
|
|
|
|
|
|
'lastStep' => $this->getLastStepPerPlayer($playedGame),
|
|
|
|
|
|
'mostRecentStep' => $this->getMostRecentStep($playedGame),
|
|
|
|
|
|
'redPoints' => $redPoints,
|
|
|
|
|
|
'bluePoints' => $bluePoints,
|
|
|
|
|
|
'redBonusPoints' => $playedGame->getRedBonusPoints() ?? 0,
|
|
|
|
|
|
'blueBonusPoints' => $playedGame->getBlueBonusPoints() ?? 0,
|
|
|
|
|
|
'redBonusStats' => $playedGame->getRedBonusStats() ?? [],
|
|
|
|
|
|
'blueBonusStats' => $playedGame->getBlueBonusStats() ?? [],
|
|
|
|
|
|
'gameFinished' => $gameFinished,
|
2026-04-10 12:23:21 +02:00
|
|
|
|
], JSON_THROW_ON_ERROR));
|
2026-04-09 22:00:53 +02:00
|
|
|
|
} catch (JsonException $e) {
|
|
|
|
|
|
throw new RuntimeException($e->getMessage());
|
|
|
|
|
|
}
|
2019-10-27 18:51:28 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 18:04:01 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-10 12:23:21 +02:00
|
|
|
|
public function saveGrid(string $gameAssoc): bool
|
2019-10-27 18:51:28 +01:00
|
|
|
|
{
|
2026-04-12 08:01:46 +02:00
|
|
|
|
$existingGame = $this->playedGameRepository->findOneByGameAssoc($gameAssoc);
|
2026-04-09 22:00:53 +02:00
|
|
|
|
|
|
|
|
|
|
if (null !== $existingGame) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-10 12:57:03 +02:00
|
|
|
|
$grid2d = $this->generateGrid();
|
2019-10-27 18:51:28 +01:00
|
|
|
|
$playedGame = new PlayedGame();
|
2026-04-10 12:57:03 +02:00
|
|
|
|
$grid = new Grid();
|
2019-10-27 18:51:28 +01:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-04-10 12:23:21 +02:00
|
|
|
|
foreach ($grid2d as $row) {
|
2019-10-27 18:51:28 +01:00
|
|
|
|
$gridRow = new GridRow();
|
|
|
|
|
|
$gridRow->setGridCol($row);
|
|
|
|
|
|
$gridRow->setGrid($grid);
|
2026-04-19 18:04:01 +02:00
|
|
|
|
$this->em->persist($gridRow);
|
2019-10-27 18:51:28 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$grid->setPlayedGame($playedGame);
|
2026-04-19 18:04:01 +02:00
|
|
|
|
$this->em->persist($grid);
|
2019-10-27 18:51:28 +01:00
|
|
|
|
|
2026-04-10 12:23:21 +02:00
|
|
|
|
$playedGame->setGameAssoc($gameAssoc);
|
2026-04-14 19:37:42 +02:00
|
|
|
|
$playedGame->setUuid(Uuid::fromString($gameAssoc));
|
2019-10-27 18:51:28 +01:00
|
|
|
|
$playedGame->setGrid($grid);
|
|
|
|
|
|
$playedGame->setCreated(new DateTime());
|
|
|
|
|
|
$playedGame->setUpdated(new DateTime());
|
2026-04-19 18:04:01 +02:00
|
|
|
|
$this->em->persist($playedGame);
|
2019-10-27 18:51:28 +01:00
|
|
|
|
|
2026-04-19 18:04:01 +02:00
|
|
|
|
$this->em->flush();
|
2019-10-27 18:51:28 +01:00
|
|
|
|
} catch (Exception $e) {
|
|
|
|
|
|
$this->logger->error($e->getMessage());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-10 12:23:21 +02:00
|
|
|
|
* Generate a random 16×16 grid with 51 mines and adjacent-mine numbers.
|
2019-10-27 18:51:28 +01:00
|
|
|
|
*/
|
2026-04-10 12:23:21 +02:00
|
|
|
|
private function generateGrid(): array
|
2019-10-27 18:51:28 +01:00
|
|
|
|
{
|
2026-04-14 19:37:42 +02:00
|
|
|
|
/** Build flat set: 51 mines ('m') + remaining water ('w') */
|
2026-04-10 12:23:21 +02:00
|
|
|
|
$set = array_merge(
|
|
|
|
|
|
array_fill(0, self::MINES, 'm'),
|
|
|
|
|
|
array_fill(0, self::ROWS * self::COLS - self::MINES, 'w'),
|
|
|
|
|
|
);
|
2019-10-27 18:51:28 +01:00
|
|
|
|
|
2026-04-14 19:37:42 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Fisher-Yates shuffle
|
2026-04-19 18:04:01 +02:00
|
|
|
|
*
|
2026-04-14 19:37:42 +02:00
|
|
|
|
* @see https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
|
|
|
|
|
|
*/
|
2026-04-10 12:23:21 +02:00
|
|
|
|
for ($i = count($set) - 1; $i > 0; $i--) {
|
2026-04-14 19:37:42 +02:00
|
|
|
|
try {
|
|
|
|
|
|
$j = random_int(0, $i);
|
|
|
|
|
|
} catch (RandomException $e) {
|
|
|
|
|
|
throw new RuntimeException('Failed to generate random index: ' . $e->getMessage());
|
|
|
|
|
|
}
|
2026-04-10 12:23:21 +02:00
|
|
|
|
[$set[$i], $set[$j]] = [$set[$j], $set[$i]];
|
|
|
|
|
|
}
|
2019-10-27 18:51:28 +01:00
|
|
|
|
|
2026-04-14 19:37:42 +02:00
|
|
|
|
/** Reshape to 2-D */
|
2026-04-10 12:23:21 +02:00
|
|
|
|
$grid = [];
|
|
|
|
|
|
for ($r = 0; $r < self::ROWS; $r++) {
|
|
|
|
|
|
$grid[$r] = array_slice($set, $r * self::COLS, self::COLS);
|
|
|
|
|
|
}
|
2019-10-27 18:51:28 +01:00
|
|
|
|
|
2026-04-14 19:37:42 +02:00
|
|
|
|
/** Replace 'w' with adjacent-mine count */
|
2026-04-10 12:23:21 +02:00
|
|
|
|
$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;
|
2019-10-27 18:51:28 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-10 12:23:21 +02:00
|
|
|
|
return $grid;
|
2019-10-27 18:51:28 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-10 12:23:21 +02:00
|
|
|
|
* Collect all cells revealed so far, enriched with the player colour from each Step.
|
2019-10-27 18:51:28 +01:00
|
|
|
|
*/
|
2026-04-10 12:23:21 +02:00
|
|
|
|
private function aggregateRevealedCells(PlayedGame $playedGame): array
|
2019-10-27 18:51:28 +01:00
|
|
|
|
{
|
2026-04-10 12:23:21 +02:00
|
|
|
|
$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;
|
2019-10-27 18:51:28 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 18:04:01 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* 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->getPlayer(),
|
|
|
|
|
|
'row' => (int)$step->getRow(),
|
|
|
|
|
|
'col' => (int)$step->getCol(),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-10-27 18:51:28 +01:00
|
|
|
|
private function getUserCollection(PlayedGame $playedGame): array
|
|
|
|
|
|
{
|
2026-04-09 20:21:01 +02:00
|
|
|
|
return [
|
2026-04-10 12:57:03 +02:00
|
|
|
|
'red' => null !== $playedGame->getRed() ? $playedGame->getRed()->getUsername() : '',
|
|
|
|
|
|
'blue' => null !== $playedGame->getBlue() ? $playedGame->getBlue()->getUsername() : '',
|
2026-04-09 20:21:01 +02:00
|
|
|
|
'redAnon' => null !== $playedGame->getRedAnon() ? $playedGame->getRedAnon()->getUserName() : '',
|
2026-04-10 12:57:03 +02:00
|
|
|
|
'blueAnon' => null !== $playedGame->getBlueAnon() ? $playedGame->getBlueAnon()->getUserName() : '',
|
2026-04-09 20:21:01 +02:00
|
|
|
|
];
|
2019-10-27 18:51:28 +01:00
|
|
|
|
}
|
2026-04-10 12:57:03 +02:00
|
|
|
|
}
|