Add win/lose game-ending flow

Removing all 3 locked files while the timer is still running now marks
the session WON immediately (checked right after every successful rm)
and broadcasts a "game_finished" signal over Mercure so every
connected player gets redirected together, not just the one who
removed the last file. A new /won/{session} route + won.html.twig
mirrors the existing lost flow (victory narrative + the same feedback
form).

The existing timer-expiry path already set LOST but always redirected
to lostUrl regardless of actual status; it now picks won/lost based on
the status the server reports.

Also fixes a pre-existing bug on the lost page (and would-be bug on
the new won page): PlayerService::GetCurrentlyActiveAsPlayer() only
matches players in READY/PLAYING sessions, so by the time a session
has ended it always returned null there, silently breaking the
feedback form. Both pages now look the player up directly via
PlayerRepository instead.

Added a navigatingAway flag so the page's "confirm before leaving"
prompt doesn't block our own win/lose redirects.
This commit is contained in:
Frank
2026-07-11 21:41:16 +02:00
parent eee6c3a369
commit 3984a33282
5 changed files with 237 additions and 8 deletions
+50
View File
@@ -4,6 +4,7 @@ namespace App\Game\Service;
use App\Game\Enum\DecodeMessage;
use App\Game\Enum\SessionSettingType;
use App\Game\Enum\SessionStatus;
use App\Game\Entity\Player;
use App\Game\Entity\Session;
use App\Game\Entity\SessionSetting;
@@ -230,6 +231,18 @@ class GameResponseService
return ['result' => ['You are not allowed to remove this file.']];
$this->playerService->addDeletedFileToSession($player, $fullPath);
if ($this->checkFilesRemovalWin($player)) {
return [
'result' => [
'File removed: ' . $filename,
'MAINFRAME: All protected files purged. The AI virus has been contained. Well done, agents.',
],
'messageType' => 'mainframe',
'gameWon' => true,
];
}
$filesRemovalDeadline = $this->startLockedFilesRemovalDeadline($player->getSession(), $fullPath);
$lock = $this->triggerLock($player);
@@ -1030,6 +1043,39 @@ class GameResponseService
];
}
/**
* Checks whether all locked files are currently removed and, if so, marks the session
* as won (once) and broadcasts a "game finished" signal to every connected player.
*/
private function checkFilesRemovalWin(Player $player): bool
{
$session = $player->getSession();
if ($session->getStatus() !== SessionStatus::PLAYING) {
return $session->getStatus() === SessionStatus::WON;
}
$deletedFiles = $this->playerService->getDeletedFilesOfSession($player);
$lockedFiles = $this->getLockedFiles();
if (count(array_intersect($lockedFiles, $deletedFiles)) < count($lockedFiles)) {
return false;
}
$session->setStatus(SessionStatus::WON);
$this->entityManager->persist($session);
$this->entityManager->flush();
$topic = '/game/hub/' . $session->getId();
try {
$this->hub->publish(new Update($topic, json_encode(['type' => 'game_finished', 'status' => 'won'])));
} catch (\Exception $e) {
// Mercure might be down
}
return true;
}
/**
* Starts (if not already running) the 60-second window within which all locked files
* must be removed, or the ones already removed get restored by the virus. Returns the
@@ -1084,6 +1130,10 @@ class GameResponseService
*/
private function enforceLockedFilesRemovalDeadline(Session $session): void
{
if ($session->getStatus() !== SessionStatus::PLAYING) {
return;
}
$setting = $this->sessionSettingRepository->getSetting($session, SessionSettingType::LOCKED_FILES_REMOVAL_DEADLINE);
if (!$setting || !$setting->getValue()) {
return;