Private
Public Access
1
0

new: usr: a new feature came up - the abandoned plays can be restored, if both users are registered users #7

This commit is contained in:
2026-04-19 18:04:01 +02:00
parent c79584c7d2
commit 991b114a3c
23 changed files with 910 additions and 251 deletions

View File

@@ -13,8 +13,10 @@ namespace App\Util;
use App\Entity\Grid;
use App\Entity\GridRow;
use App\Entity\PlayedGame;
use App\Entity\Step;
use App\Interfaces\RpcManagerInterface;
use App\Repository\PlayedGameRepository;
use App\Repository\StepRepository;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
@@ -36,14 +38,15 @@ use Symfony\Component\Uid\Uuid;
*/
class RpcManager implements RpcManagerInterface
{
private const int ROWS = 16;
private const int ROWS = 16;
private const int COLS = 16;
private const int MINES = 51;
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly EntityManagerInterface $em,
private readonly LoggerInterface $logger,
private readonly PlayedGameRepository $playedGameRepository,
private readonly StepRepository $stepRepository,
) {
}
@@ -56,8 +59,17 @@ class RpcManager implements RpcManagerInterface
if (null === $playedGame) {
try {
return base64_encode(json_encode([
'users' => null,
'revealedCells' => null,
'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());
@@ -68,15 +80,42 @@ class RpcManager implements RpcManagerInterface
$revealedCells = $this->aggregateRevealedCells($playedGame);
try {
$redPoints = $playedGame->getRedPoints() ?? 0;
$bluePoints = $playedGame->getBluePoints() ?? 0;
$gameFinished = $redPoints > 25 || $bluePoints > 25;
return base64_encode(json_encode([
'users' => $users,
'revealedCells' => $revealedCells,
'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,
], 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);
@@ -94,20 +133,20 @@ class RpcManager implements RpcManagerInterface
$gridRow = new GridRow();
$gridRow->setGridCol($row);
$gridRow->setGrid($grid);
$this->entityManager->persist($gridRow);
$this->em->persist($gridRow);
}
$grid->setPlayedGame($playedGame);
$this->entityManager->persist($grid);
$this->em->persist($grid);
$playedGame->setGameAssoc($gameAssoc);
$playedGame->setUuid(Uuid::fromString($gameAssoc));
$playedGame->setGrid($grid);
$playedGame->setCreated(new DateTime());
$playedGame->setUpdated(new DateTime());
$this->entityManager->persist($playedGame);
$this->em->persist($playedGame);
$this->entityManager->flush();
$this->em->flush();
} catch (Exception $e) {
$this->logger->error($e->getMessage());
}
@@ -128,6 +167,7 @@ class RpcManager implements RpcManagerInterface
/**
* Fisher-Yates shuffle
*
* @see https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
*/
for ($i = count($set) - 1; $i > 0; $i--) {
@@ -185,6 +225,37 @@ class RpcManager implements RpcManagerInterface
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->getPlayer(),
'row' => (int)$step->getRow(),
'col' => (int)$step->getCol(),
];
}
private function getUserCollection(PlayedGame $playedGame): array
{
return [

View File

@@ -26,10 +26,9 @@ use JsonException;
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Class TopicManager
@@ -50,12 +49,14 @@ readonly class TopicManager implements TopicManagerInterface
private CacheManager $cacheManager,
private PlayedGameRepository $playedGameRepository,
private UserRepository $userRepository,
private RequestStack $requestStack,
) {
}
public function subscribe(string $gameAssoc, string $userName, ?UserInterface $user, Request $request): void
public function subscribe(string $gameAssoc, string $userName): void
{
$playedGame = $this->getPlayedGame($gameAssoc);
if (null === $playedGame) {
return;
}
@@ -71,7 +72,7 @@ readonly class TopicManager implements TopicManagerInterface
/** Save the player to the database on a fresh join */
if (!$isKnown && $count < 2) {
$users = $this->saveUserToDb($gameAssoc, $userName, $user, $count + 1, $request);
$users = $this->saveUserToDb($gameAssoc, $userName, $count + 1);
$count = $this->getPlayerCount($users);
}
@@ -96,6 +97,7 @@ readonly class TopicManager implements TopicManagerInterface
if ($count === 1) {
// One player waiting — mark as active and announce to the lobby
$playedGame->setUpdated(new DateTime());
$this->em->persist($playedGame);
$this->em->flush();
@@ -634,18 +636,13 @@ readonly class TopicManager implements TopicManagerInterface
}
}
private function saveUserToDb(
string $gameAssoc,
string $userName,
?UserInterface $user,
int $count,
Request $request
): array {
private function saveUserToDb(string $gameAssoc, string $userName, int $count): array
{
$playedGame = $this->getPlayedGame($gameAssoc);
null !== $user
null !== $this->requestStack->getCurrentRequest()->getUser()
? $this->saveRegisteredUser($userName, $count, $playedGame)
: $this->saveAnonUser($userName, $count, $playedGame, $request);
: $this->saveAnonUser($userName, $count, $playedGame);
$this->em->persist($playedGame);
$this->em->flush();
@@ -672,15 +669,16 @@ readonly class TopicManager implements TopicManagerInterface
}
}
private function saveAnonUser(string $userName, int $count, PlayedGame $playedGame, Request $request): void
private function saveAnonUser(string $userName, int $count, PlayedGame $playedGame): void
{
try {
$anon = new Gamer();
$anon->setUserName($userName);
$anon->setIp($request->getClientIp());
$anon->setCountry($this->extractCountry($request));
$anon->setUserAgent($request->headers->get('User-Agent'));
$anon->setIp($this->requestStack->getCurrentRequest()->getClientIp());
$anon->setCountry($this->extractCountry());
$anon->setUserAgent($this->requestStack->getCurrentRequest()->headers->get('User-Agent'));
$anon->setConnTimestamp(new DateTime());
$this->em->persist($anon);
if ($count === 1) {
@@ -719,6 +717,7 @@ readonly class TopicManager implements TopicManagerInterface
{
$challengerGame = $this->getPlayedGame($challengerGameAssoc);
$challengerName = 'Unknown';
if (null !== $challengerGame) {
$users = $this->getUserCollection($challengerGame);
$challengerName = $users['red'] ?: $users['redAnon'] ?: $users['blue'] ?: $users['blueAnon'] ?: 'Unknown';
@@ -754,6 +753,22 @@ readonly class TopicManager implements TopicManagerInterface
}
}
public function publishHeartbeat(string $gameAssoc, string $color): void
{
try {
$this->hub->publish(new Update(
'mineseeker/channel/' . $gameAssoc,
json_encode([
'type' => 'heartbeat',
'color' => $color,
'ts' => (int)(microtime(true) * 1000),
], JSON_THROW_ON_ERROR)
));
} catch (JsonException $e) {
$this->logger->error('Heartbeat publish error: ' . $e->getMessage());
}
}
private function publishToLobby(array $data): void
{
try {
@@ -766,7 +781,7 @@ readonly class TopicManager implements TopicManagerInterface
}
}
private function extractCountry(Request $request): ?string
private function extractCountry(): ?string
{
/** Common headers used by CDNs and proxies to pass country information */
$countryHeaders = [
@@ -777,7 +792,7 @@ readonly class TopicManager implements TopicManagerInterface
];
foreach ($countryHeaders as $header) {
$country = $request->headers->get($header);
$country = $this->requestStack->getCurrentRequest()->headers->get($header);
if (empty($country)) {
continue;