From 2bfc2aca1a2291b819b13df2fb8ab04d9d2de3ca Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 10 Aug 2026 16:47:57 +0200 Subject: [PATCH] Keep pregame lobby chat open for an hour after the game ends The chat used to disappear the moment a session left CREATED status. Sessions now record a finishedAt timestamp when they're won or lost, and the lobby (with chat) stays reachable via /game/{session} for an hour afterward instead of immediately redirecting to the win/lose feedback page. The lobby template shows a distinct "game finished" header with a link to that feedback page during this window. Co-Authored-By: Claude Sonnet 5 --- migrations/Version20260810130000.php | 26 ++++++++ .../Controller/GameAdminSessionController.php | 1 + src/Game/Controller/GameApiController.php | 1 + src/Game/Controller/GameController.php | 2 +- src/Game/Entity/Session.php | 15 +++++ src/Game/Service/GameDashboardService.php | 26 +++++++- src/Game/Service/GameResponseService.php | 1 + templates/game/lobby.html.twig | 66 ++++++++++++------- 8 files changed, 112 insertions(+), 26 deletions(-) create mode 100644 migrations/Version20260810130000.php diff --git a/migrations/Version20260810130000.php b/migrations/Version20260810130000.php new file mode 100644 index 0000000..656396c --- /dev/null +++ b/migrations/Version20260810130000.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE session ADD finished_at DATETIME DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE session DROP finished_at'); + } +} diff --git a/src/Game/Controller/GameAdminSessionController.php b/src/Game/Controller/GameAdminSessionController.php index 75cfee9..8e10dfb 100644 --- a/src/Game/Controller/GameAdminSessionController.php +++ b/src/Game/Controller/GameAdminSessionController.php @@ -35,6 +35,7 @@ final class GameAdminSessionController extends AbstractController } $session->setStatus(SessionStatus::LOST); + $session->setFinishedAt(new \DateTime()); $em->flush(); $this->addFlash('success', sprintf('Session #%d closed.', $session->getId())); diff --git a/src/Game/Controller/GameApiController.php b/src/Game/Controller/GameApiController.php index d37fdc6..1c34eb9 100644 --- a/src/Game/Controller/GameApiController.php +++ b/src/Game/Controller/GameApiController.php @@ -43,6 +43,7 @@ final class GameApiController extends AbstractController if ($session->getStatus() === SessionStatus::PLAYING) { if ($session->getTimer() !== null && $now >= $session->getTimer()) { $session->setStatus(SessionStatus::LOST); + $session->setFinishedAt(new \DateTime()); $this->entityManager->persist($session); $this->entityManager->flush(); $isFinished = true; diff --git a/src/Game/Controller/GameController.php b/src/Game/Controller/GameController.php index d9929cc..4134c9e 100644 --- a/src/Game/Controller/GameController.php +++ b/src/Game/Controller/GameController.php @@ -149,7 +149,7 @@ final class GameController extends AbstractController // Lazily pick up readiness changes from other players since our last request $dashboardService->checkAllPlayersReady($session); - if ($session->getStatus() === SessionStatus::CREATED) { + if ($dashboardService->isLobbyChatOpen($session)) { return $this->render('game/lobby.html.twig', [ 'session' => $session, 'messages' => $dashboardService->getLobbyMessages($session), diff --git a/src/Game/Entity/Session.php b/src/Game/Entity/Session.php index 2659ad6..53c45ac 100644 --- a/src/Game/Entity/Session.php +++ b/src/Game/Entity/Session.php @@ -31,6 +31,9 @@ class Session #[ORM\Column(type: Types::DATETIME_MUTABLE)] private ?\DateTimeInterface $created = null; + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] + private ?\DateTimeInterface $finishedAt = null; + #[ORM\OneToMany(mappedBy: 'session', targetEntity: Player::class)] private Collection $players; @@ -97,6 +100,18 @@ class Session return $this; } + public function getFinishedAt(): ?\DateTimeInterface + { + return $this->finishedAt; + } + + public function setFinishedAt(?\DateTimeInterface $finishedAt): static + { + $this->finishedAt = $finishedAt; + + return $this; + } + /** * @return Collection */ diff --git a/src/Game/Service/GameDashboardService.php b/src/Game/Service/GameDashboardService.php index a0ebb4a..52f7416 100644 --- a/src/Game/Service/GameDashboardService.php +++ b/src/Game/Service/GameDashboardService.php @@ -24,6 +24,7 @@ final class GameDashboardService { private const READY_TIMEOUT_SECONDS = 60; private const LOBBY_MESSAGE_MAX_LENGTH = 500; + private const LOBBY_CHAT_GRACE_PERIOD_SECONDS = 3600; public function __construct( private readonly GameRepository $gameRepository, @@ -302,9 +303,32 @@ final class GameDashboardService return $this->lobbyMessageRepository->findForSession($session); } + /** + * The lobby chat is open while a session is still gathering players, and stays + * open for a grace period after the game ends so players can wrap up the + * conversation before it disappears. + */ + public function isLobbyChatOpen(Session $session): bool + { + if ($session->getStatus() === SessionStatus::CREATED) { + return true; + } + + if (!in_array($session->getStatus(), [SessionStatus::WON, SessionStatus::LOST], true)) { + return false; + } + + $finishedAt = $session->getFinishedAt(); + if ($finishedAt === null) { + return false; + } + + return (new \DateTime())->getTimestamp() - $finishedAt->getTimestamp() < self::LOBBY_CHAT_GRACE_PERIOD_SECONDS; + } + public function postLobbyMessage(Session $session, User $user, string $content): ?LobbyMessage { - if ($session->getStatus() !== SessionStatus::CREATED) { + if (!$this->isLobbyChatOpen($session)) { return null; } diff --git a/src/Game/Service/GameResponseService.php b/src/Game/Service/GameResponseService.php index a5c0e53..c01cdfa 100644 --- a/src/Game/Service/GameResponseService.php +++ b/src/Game/Service/GameResponseService.php @@ -1118,6 +1118,7 @@ class GameResponseService } $session->setStatus(SessionStatus::WON); + $session->setFinishedAt(new \DateTime()); $this->entityManager->persist($session); $this->entityManager->flush(); diff --git a/templates/game/lobby.html.twig b/templates/game/lobby.html.twig index 427fd49..9934258 100644 --- a/templates/game/lobby.html.twig +++ b/templates/game/lobby.html.twig @@ -1,38 +1,56 @@ {% extends 'layout/site.html.twig' %} -{% block title %}Waiting for players - {{ session.game.name }}{% endblock %} +{% set isCreated = session.status.value == 'created' %} +{% set isFinished = session.status.value in ['won', 'lost'] %} + +{% block title %}{{ isCreated ? 'Waiting for players' : 'Post-game chat' }} - {{ session.game.name }}{% endblock %} {% block body %}
-
-
-

Waiting for more players to join

-
-
-

{{ session.game.name }}

-

Share the invite code with your friends. Feel free to chat below while you wait — no need to reload the page.

- -
- Players joined: {{ session.players|length }} / {{ session.game.numberOfPlayers }} + {% if isCreated %} +
+
+

Waiting for more players to join

+
+

{{ session.game.name }}

+

Share the invite code with your friends. Feel free to chat below while you wait — no need to reload the page.

-
    - {% for sessionPlayer in session.players %} -
  • {{ sessionPlayer.user.username }}
  • - {% endfor %} -
+
+ Players joined: {{ session.players|length }} / {{ session.game.numberOfPlayers }} +
- {% if session.players|length >= session.game.numberOfPlayers %} -
- - -
- {% endif %} +
    + {% for sessionPlayer in session.players %} +
  • {{ sessionPlayer.user.username }}
  • + {% endfor %} +
- Back to Dashboard + {% if session.players|length >= session.game.numberOfPlayers %} +
+ + +
+ {% endif %} + + Back to Dashboard +
-
+ {% elseif isFinished %} +
+
+

{{ session.status.value == 'won' ? 'You won!' : 'Game over' }}

+
+
+

{{ session.game.name }}

+

The game has ended, but the chat is still open for a little while — feel free to keep talking.

+ + View results + Back to Dashboard +
+
+ {% endif %}