diff --git a/src/Game/Controller/GameController.php b/src/Game/Controller/GameController.php index 956eb07..5cd1357 100644 --- a/src/Game/Controller/GameController.php +++ b/src/Game/Controller/GameController.php @@ -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), ]); } diff --git a/src/Game/Enum/SessionSettingType.php b/src/Game/Enum/SessionSettingType.php index f799440..fa505d8 100644 --- a/src/Game/Enum/SessionSettingType.php +++ b/src/Game/Enum/SessionSettingType.php @@ -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'; diff --git a/src/Game/Service/GameDashboardService.php b/src/Game/Service/GameDashboardService.php index 95917cc..cbaf229 100644 --- a/src/Game/Service/GameDashboardService.php +++ b/src/Game/Service/GameDashboardService.php @@ -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(); diff --git a/src/Game/Service/GameResponseService.php b/src/Game/Service/GameResponseService.php index c8e8b7c..04b789d 100644 --- a/src/Game/Service/GameResponseService.php +++ b/src/Game/Service/GameResponseService.php @@ -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(); } diff --git a/templates/game/won.html.twig b/templates/game/won.html.twig index 20eb5f7..1f11ef0 100644 --- a/templates/game/won.html.twig +++ b/templates/game/won.html.twig @@ -62,7 +62,7 @@ Then the screens flood green: containment confirmed. The AI virus's grip on the server unravels file by file.
- 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.
diff --git a/tests/Game/GameDashboardServiceTest.php b/tests/Game/GameDashboardServiceTest.php index 5801981..47b6c02 100644 --- a/tests/Game/GameDashboardServiceTest.php +++ b/tests/Game/GameDashboardServiceTest.php @@ -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); @@ -360,8 +362,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 +419,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);