3 Commits
Author SHA1 Message Date
FrankandClaude Sonnet 5 779ddcbccb Fix 3 pre-existing failing tests
- testToggleReady: user was never marked verified, so toggleReady()
  returned false before doing anything (an isVerified() gate was
  added to the service after this test was written).
- testCheckAllPlayersReadyTransitionsStatus: entityManager mock
  returned the same SessionSettingRepository for every getRepository()
  call, but the service now also fetches a GameSettingRepository for
  the session's total-time setting, causing a TypeError. Route the
  mock by requested class instead.
- testChatRegeneratesVerifyCodesIfShared: two competing
  method('getSetting') stubs were registered without with()
  constraints; PHPUnit keeps the first one it sees active for every
  call, so the (correct, more complete) willReturnCallback stub was
  silently dead code and the regeneration path never actually ran.
  Dropped the redundant first stub, and updated the flush() count now
  that the real flow (chat tracking + code regeneration) executes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 17:18:36 +02:00
FrankandClaude Sonnet 5 6b1436dfe7 Randomize decoy usernames and which rapports carry coded messages
Decoy names in /var/home and verifyCodes.txt were a hardcoded 4-name
list (Luke, Charles, William, Peter), so a real player registering
under one of those names would collide with it. Now each session
picks 6 decoys from a pool of 10, excluding any name already taken by
a real player, and stores the pick as a session setting.

Likewise the 3 "special report" rapports that get coded messages were
always Doyle, Vega and Lennox. Each session now randomly assigns 3 of
the 20 rapports to that role, and the win screen / mainframe-help
message reference whichever agents were actually picked instead of
the hardcoded names.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 17:09:52 +02:00
FrankandClaude Sonnet 5 02780d8f04 Add a favicon
Crops the key/globe mark out of the existing logo and wires it into
base.html.twig as favicon.ico plus PNG/apple-touch-icon variants.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 17:09:44 +02:00
14 changed files with 168 additions and 36 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

+3 -1
View File
@@ -282,7 +282,8 @@ final class GameController extends AbstractController
Session $session,
Request $request,
Security $security,
PlayerRepository $playerRepository
PlayerRepository $playerRepository,
GameResponseService $gameResponseService
): Response {
/** @var User $user */
$user = $security->getUser();
@@ -304,6 +305,7 @@ final class GameController extends AbstractController
return $this->render('game/won.html.twig', [
'session' => $session,
'agentsText' => $gameResponseService->getSpecialReportAgentsText($session),
]);
}
+5 -3
View File
@@ -57,9 +57,11 @@ enum SessionSettingType: string
case VERIFICATION_PROGRESS_FOR_PLAYER9 = 'VerificationProgressForPlayer9';
case VERIFICATION_PROGRESS_FOR_PLAYER10 = 'VerificationProgressForPlayer10';
case EVERYONE_VERIFIED = 'EveryoneVerified';
case SPECIAL_REPORT_CODE_DOYLE = 'SpecialReportCodeDoyle';
case SPECIAL_REPORT_CODE_VEGA = 'SpecialReportCodeVega';
case SPECIAL_REPORT_CODE_LENNOX = 'SpecialReportCodeLennox';
case SPECIAL_REPORT_CODE_1 = 'SpecialReportCode1';
case SPECIAL_REPORT_CODE_2 = 'SpecialReportCode2';
case SPECIAL_REPORT_CODE_3 = 'SpecialReportCode3';
case SPECIAL_REPORT_ASSIGNMENTS = 'SpecialReportAssignments';
case DECOY_USERNAMES = 'DecoyUsernames';
case READY_AT_FOR_PLAYER1 = 'ReadyAtForPlayer1';
case READY_AT_FOR_PLAYER2 = 'ReadyAtForPlayer2';
case READY_AT_FOR_PLAYER3 = 'ReadyAtForPlayer3';
+78
View File
@@ -26,6 +26,37 @@ final class GameDashboardService
private const LOBBY_MESSAGE_MAX_LENGTH = 500;
private const LOBBY_CHAT_GRACE_PERIOD_SECONDS = 3600;
private const DECOY_USERNAME_POOL = [
'Luke', 'Charles', 'William', 'Peter', 'Simon',
'Oliver', 'Daniel', 'Thomas', 'Edward', 'George',
];
private const DECOY_USERNAME_COUNT = 6;
// File => cover-identity agent name, mirrors assets/game1/filesystem/var/rapports/index.txt
private const RAPPORT_AGENTS = [
'/var/rapports/095_07-14.txt' => 'Mason',
'/var/rapports/007_19-52.txt' => 'Harper',
'/var/rapports/083_25-39.txt' => 'Doyle',
'/var/rapports/019_31-11.txt' => 'Vega',
'/var/rapports/075_46-77.txt' => 'Ellis',
'/var/rapports/031_53-28.txt' => 'Navarro',
'/var/rapports/072_61-05.txt' => 'Carter',
'/var/rapports/064_72-90.txt' => 'Quinn',
'/var/rapports/091_81-33.txt' => 'Raines',
'/var/rapports/079_89-47.txt' => 'Dalton',
'/var/rapports/098_92-14.txt' => 'Langston',
'/var/rapports/012_94-31.txt' => 'Donovan',
'/var/rapports/016_98-07.txt' => 'Fletcher',
'/var/rapports/087_102-45.txt' => 'Sanders',
'/var/rapports/094_110-19.txt' => 'Mitchell',
'/var/rapports/063_117-56.txt' => 'O’Connor',
'/var/rapports/017_123-88.txt' => 'Holloway',
'/var/rapports/011_130-62.txt' => 'Lennox',
'/var/rapports/093_138-24.txt' => 'Bennett',
'/var/rapports/001_145-93.txt' => 'Carver',
];
private const SPECIAL_REPORT_COUNT = 3;
public function __construct(
private readonly GameRepository $gameRepository,
private readonly SessionRepository $sessionRepository,
@@ -257,6 +288,50 @@ final class GameDashboardService
}
}
// Picks decoy usernames for /var/home and verifyCodes.txt, excluding any name
// already taken by a real player in this session so they can never collide.
private function initializeDecoyUsernames(Session $session): void
{
$realUsernames = array_map(
static fn (Player $p) => $p->getUser()->getUsername(),
$session->getPlayers()->toArray()
);
$availableDecoys = array_values(array_diff(self::DECOY_USERNAME_POOL, $realUsernames));
shuffle($availableDecoys);
$selected = array_slice($availableDecoys, 0, self::DECOY_USERNAME_COUNT);
$setting = new SessionSetting();
$setting->setSession($session);
$setting->setName(SessionSettingType::DECOY_USERNAMES);
$setting->setValue(json_encode($selected));
$this->entityManager->persist($setting);
}
// Picks which 3 rapports carry the coded messages this session, so it isn't
// always the same Doyle/Vega/Lennox trio.
private function initializeSpecialReportAssignments(Session $session): void
{
$files = array_keys(self::RAPPORT_AGENTS);
shuffle($files);
$chosen = array_slice($files, 0, self::SPECIAL_REPORT_COUNT);
$assignments = [];
foreach ($chosen as $index => $file) {
$assignments[] = [
'file' => $file,
'agent' => self::RAPPORT_AGENTS[$file],
'digit' => $index + 1,
];
}
$setting = new SessionSetting();
$setting->setSession($session);
$setting->setName(SessionSettingType::SPECIAL_REPORT_ASSIGNMENTS);
$setting->setValue(json_encode($assignments));
$this->entityManager->persist($setting);
}
public function startSession(Session $session): bool
{
if ($session->getStatus() !== SessionStatus::CREATED) {
@@ -286,6 +361,9 @@ final class GameDashboardService
$this->entityManager->persist($player);
}
$this->initializeDecoyUsernames($session);
$this->initializeSpecialReportAssignments($session);
$session->setStatus(SessionStatus::READY);
$this->entityManager->persist($session);
$this->entityManager->flush();
+54 -17
View File
@@ -959,11 +959,52 @@ class GameResponseService
$this->entityManager->persist($everyoneVerifiedSetting);
$this->entityManager->flush();
$message = "Mainframe Help Modus: Agents Doyle, Vega and Lennox rapports have been updated with coded messages.";
$agentNames = $this->getSpecialReportAgentNames($session);
$message = "Mainframe Help Modus: Agents " . $this->formatNameList($agentNames) . " rapports have been updated with coded messages.";
$this->eventDispatcher->dispatch(new PushMercureMessageEvent($session, [0, $message], 'help_modus_activated'));
}
}
private function getDecoyUsernames(Session $session): array
{
$setting = $this->sessionSettingRepository->getSetting($session, SessionSettingType::DECOY_USERNAMES);
$decoded = json_decode($setting?->getValue() ?? '[]', true);
return is_array($decoded) ? $decoded : [];
}
private function getSpecialReportAssignments(Session $session): array
{
$setting = $this->sessionSettingRepository->getSetting($session, SessionSettingType::SPECIAL_REPORT_ASSIGNMENTS);
$decoded = json_decode($setting?->getValue() ?? '[]', true);
return is_array($decoded) ? $decoded : [];
}
public function getSpecialReportAgentNames(Session $session): array
{
return array_column($this->getSpecialReportAssignments($session), 'agent');
}
public function getSpecialReportAgentsText(Session $session): string
{
return $this->formatNameList($this->getSpecialReportAgentNames($session));
}
private function formatNameList(array $names): string
{
if (count($names) === 0) {
return '';
}
if (count($names) === 1) {
return $names[0];
}
$last = array_pop($names);
return implode(', ', $names) . ' and ' . $last;
}
private function goToNewDir(string $pwd, string $newPwd, Player $player) : string|bool
{
$allPossiblePaths = $this->getAllPossiblePaths($player);
@@ -1080,7 +1121,7 @@ class GameResponseService
$paths[] = '/var/home';
$playerNames = ['root', 'Luke', 'Charles', 'William', 'Peter'];
$playerNames = array_merge(['root'], $this->getDecoyUsernames($player->getSession()));
$players = $player->getSession()->getPlayers();
@@ -1490,20 +1531,16 @@ class GameResponseService
return ['Error reading file'];
}
$specialFiles = [
'/var/rapports/083_25-39.txt' => [
'setting' => SessionSettingType::SPECIAL_REPORT_CODE_DOYLE,
'digit' => 1
],
'/var/rapports/019_31-11.txt' => [
'setting' => SessionSettingType::SPECIAL_REPORT_CODE_VEGA,
'digit' => 2
],
'/var/rapports/011_130-62.txt' => [
'setting' => SessionSettingType::SPECIAL_REPORT_CODE_LENNOX,
'digit' => 3
],
];
$specialFiles = [];
foreach ($this->getSpecialReportAssignments($player->getSession()) as $assignment) {
$settingType = SessionSettingType::tryFrom('SpecialReportCode' . $assignment['digit']);
if ($settingType) {
$specialFiles[$assignment['file']] = [
'setting' => $settingType,
'digit' => $assignment['digit'],
];
}
}
if (isset($specialFiles[$file])) {
$everyoneVerifiedSetting = $this->sessionSettingRepository->getSetting($player->getSession(), SessionSettingType::EVERYONE_VERIFIED);
@@ -1571,7 +1608,7 @@ class GameResponseService
$codes = json_decode($setting->getValue() ?? '[]', true) ?? [];
$playerNames = ['Luke', 'Charles', 'William', 'Peter'];
$playerNames = $this->getDecoyUsernames($player->getSession());
foreach ($player->getSession()->getPlayers() as $p) {
$playerNames[] = $p->getUser()->getUsername();
}
+4
View File
@@ -4,6 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ 'site.name'|trans }}{% endblock %}</title>
<link rel="icon" href="{{ asset('favicon.ico') }}" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('images/favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('images/favicon-16x16.png') }}">
<link rel="apple-touch-icon" href="{{ asset('images/apple-touch-icon.png') }}">
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
+1 -1
View File
@@ -62,7 +62,7 @@
Then the screens flood green: containment confirmed. The AI virus's grip on the server unravels file by file.
</p>
<p>
Agents Doyle, Vega and Lennox are safe. Their identities were never decoded. In the days that follow,
Agents {{ agentsText|default('Doyle, Vega and Lennox') }} are safe. Their identities were never decoded. In the days that follow,
the agency quietly credits an anonymous team of specialists for the save - you.
</p>
</div>
+21 -9
View File
@@ -121,9 +121,10 @@ class GameDashboardServiceTest extends TestCase
// SessionSetting (verification progress)
// Total = 2 * 6 = 12
// PLUS Session (persist)
// Total = 13
// PLUS SessionSetting (decoy usernames) + SessionSetting (special report assignments)
// Total = 15
$this->entityManager->expects($this->exactly(13))
$this->entityManager->expects($this->exactly(15))
->method('persist');
$result = $this->service->startSession($session);
@@ -155,9 +156,10 @@ class GameDashboardServiceTest extends TestCase
// Player (persist) + 5 Settings = 6 persists
// Total = 4 * 6 = 24
// PLUS Session (persist)
// Total = 25
// PLUS SessionSetting (decoy usernames) + SessionSetting (special report assignments)
// Total = 27
$this->entityManager->expects($this->exactly(25))
$this->entityManager->expects($this->exactly(27))
->method('persist');
$result = $this->service->startSession($session);
@@ -205,6 +207,7 @@ class GameDashboardServiceTest extends TestCase
public function testToggleReady(): void
{
$user = new User();
$user->setIsVerified(true);
$game = new Game();
$game->setNumberOfPlayers(1);
$session = new Session();
@@ -252,8 +255,13 @@ class GameDashboardServiceTest extends TestCase
$session->addPlayer($player2);
$repo = $this->createMock(\App\Game\Repository\SessionSettingRepository::class);
$gameSettingRepo = $this->createMock(\App\Game\Repository\GameSettingRepository::class);
$gameSettingRepo->method('getSetting')->willReturn(null); // Falls back to the default total time
$this->entityManager->method('getRepository')
->willReturn($repo);
->willReturnCallback(function ($class) use ($repo, $gameSettingRepo) {
return $class === \App\Game\Entity\GameSetting::class ? $gameSettingRepo : $repo;
});
$setting1 = new SessionSetting();
$setting1->setName(SessionSettingType::READY_AT_FOR_PLAYER1);
@@ -360,8 +368,10 @@ class GameDashboardServiceTest extends TestCase
// 4. Persist Session status update
// 5. 5 Settings for Player 1
// 6. 5 Settings for Player 2
// Total = 1 + 1 + 1 + 1 + 5 + 5 = 14 persists
$this->entityManager->expects($this->exactly(14))
// 7. SessionSetting (decoy usernames)
// 8. SessionSetting (special report assignments)
// Total = 1 + 1 + 1 + 1 + 5 + 5 + 1 + 1 = 16 persists
$this->entityManager->expects($this->exactly(16))
->method('persist');
$result = $this->service->joinSession('abc-123', $user);
@@ -415,8 +425,10 @@ class GameDashboardServiceTest extends TestCase
// 3. Player screen persist (during startSession)
// 4. Session status persist (during startSession)
// 5. 5 Settings for Player (during startSession)
// Total = 1 + 1 + 1 + 1 + 5 = 9 persists
$this->entityManager->expects($this->exactly(9))
// 6. SessionSetting (decoy usernames)
// 7. SessionSetting (special report assignments)
// Total = 1 + 1 + 1 + 1 + 5 + 1 + 1 = 11 persists
$this->entityManager->expects($this->exactly(11))
->method('persist');
$session = $this->service->createSession($game, $user, false);
@@ -91,10 +91,6 @@ class GameResponseServiceChatVerifyCodeTest extends TestCase
// Mock rights
$rightsSetting = new SessionSetting();
$rightsSetting->setValue(json_encode(['chat']));
$this->sessionSettingRepository->method('getSetting')
->willReturnMap([
[$session, SessionSettingType::RIGHTS_FOR_PLAYER1, $player, $rightsSetting],
]);
// Mock verify codes
$verifyCodesSetting = new SessionSetting();
@@ -115,7 +111,8 @@ class GameResponseServiceChatVerifyCodeTest extends TestCase
->method('dispatch')
->willReturnArgument(0);
$this->entityManager->expects($this->once())
// Flushed twice: once for the chat-tracking update, once for the code regeneration
$this->entityManager->expects($this->exactly(2))
->method('flush');
$raw = json_encode(['message' => '/chat Hello look at my code secret123', 'ts' => '123']);