Add mainframe hint system and cron-driven timeout handling

The cron (app:hints:check, every minute) now walks each playing
player through a strict dependency chain - help, communication,
verification, navigation, rapports/decode, endgame urgency - and
sends at most one hint per player per run: the first step that's
both unresolved and old enough to nudge about. Later steps genuinely
depend on earlier ones (e.g. /verify isn't usable until a player has
finished contacting everyone), so a player never gets a hint for
something they can't act on yet.

Two bits of new tracking were needed:
- Help command usage wasn't recorded anywhere, so it's now saved to
  a new HelpUsedForPlayer{N} session setting.
- "All messages decoded" needed a reliable session-wide signal, so
  the rm right (previously granted alongside sudo when player 1
  decodes) now comes from player 3's decode instead, making
  sudo+scan+rm together mean "the whole team has decoded".

The cron also now finishes sessions whose countdown has run out:
sets the session to LOST and broadcasts the same game_finished
Mercure event the win path already uses, so every connected player
gets pushed to the lost page within a minute of the game actually
ending - not just whichever player's own client-side timer happens
to notice first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Frank
2026-08-16 17:56:16 +02:00
co-authored by Claude Sonnet 5
parent 2bda70b32b
commit 2951a39204
3 changed files with 270 additions and 47 deletions
+39 -5
View File
@@ -136,6 +136,35 @@ class GameResponseService
return json_decode($setting->getValue(), true) ?? [];
}
/**
* Records that a player has used the help command, so SendMainframeHintsCommand
* can stop nudging them towards it. Absence of this setting means "not used yet".
*/
private function markHelpCommandUsed(Player $player): void
{
$settingName = SessionSettingType::tryFrom('HelpUsedForPlayer' . $player->getScreen());
if (!$settingName) {
return;
}
$setting = $this->sessionSettingRepository->getSetting($player->getSession(), $settingName, $player);
if ($setting && $setting->getValue() === 'true') {
return;
}
if (!$setting) {
$setting = new SessionSetting();
$setting->setSession($player->getSession());
$setting->setPlayer($player);
$setting->setName($settingName);
}
$setting->setValue('true');
$this->entityManager->persist($setting);
$this->entityManager->flush();
}
private function checkGameCommando(string $message, Player $player) : array
{
$messagePart = explode(' ', $message);
@@ -152,7 +181,8 @@ class GameResponseService
else
return ['result' => ['Error sending']];
case '/help':
return ['result' => $this->getHelpCommand($rechten)];
$this->markHelpCommandUsed($player);
return ['result' => $this->getHelpCommand($rechten, $player)];
case '/decode':
if(!in_array('decode', $rechten))
return ['result' => ['Unknown command']];
@@ -175,7 +205,8 @@ class GameResponseService
$rechten = $this->getRechten($player);
switch($messagePart[0]) {
case 'help':
return ['result' => $this->getHelpCommand($rechten)];
$this->markHelpCommandUsed($player);
return ['result' => $this->getHelpCommand($rechten, $player)];
case 'ls':
if(!in_array('ls', $rechten))
return ['result' => ['Unknown command']];
@@ -281,9 +312,9 @@ class GameResponseService
}
}
private function getHelpCommand(mixed $rechten) : array
private function getHelpCommand(mixed $rechten, Player $player) : array
{
$messages = [];
$messages = ['Help function for Agent ' . $player->getScreen() . ':'];
foreach($rechten as $recht) {
switch($recht) {
@@ -566,13 +597,16 @@ class GameResponseService
if ($decodeMessage === DecodeMessage::PLAYER_1) {
$this->grantRightToAllPlayers($player->getSession(), 'sudo');
$this->grantRightToAllPlayers($player->getSession(), 'rm');
}
if ($decodeMessage === DecodeMessage::PLAYER_2) {
$this->grantRightToAllPlayers($player->getSession(), 'scan');
}
if ($decodeMessage === DecodeMessage::PLAYER_3) {
$this->grantRightToAllPlayers($player->getSession(), 'rm');
}
return $decodeMessage->value;
}