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>
This commit is contained in:
@@ -282,7 +282,8 @@ final class GameController extends AbstractController
|
|||||||
Session $session,
|
Session $session,
|
||||||
Request $request,
|
Request $request,
|
||||||
Security $security,
|
Security $security,
|
||||||
PlayerRepository $playerRepository
|
PlayerRepository $playerRepository,
|
||||||
|
GameResponseService $gameResponseService
|
||||||
): Response {
|
): Response {
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
$user = $security->getUser();
|
$user = $security->getUser();
|
||||||
@@ -304,6 +305,7 @@ final class GameController extends AbstractController
|
|||||||
|
|
||||||
return $this->render('game/won.html.twig', [
|
return $this->render('game/won.html.twig', [
|
||||||
'session' => $session,
|
'session' => $session,
|
||||||
|
'agentsText' => $gameResponseService->getSpecialReportAgentsText($session),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,9 +57,11 @@ enum SessionSettingType: string
|
|||||||
case VERIFICATION_PROGRESS_FOR_PLAYER9 = 'VerificationProgressForPlayer9';
|
case VERIFICATION_PROGRESS_FOR_PLAYER9 = 'VerificationProgressForPlayer9';
|
||||||
case VERIFICATION_PROGRESS_FOR_PLAYER10 = 'VerificationProgressForPlayer10';
|
case VERIFICATION_PROGRESS_FOR_PLAYER10 = 'VerificationProgressForPlayer10';
|
||||||
case EVERYONE_VERIFIED = 'EveryoneVerified';
|
case EVERYONE_VERIFIED = 'EveryoneVerified';
|
||||||
case SPECIAL_REPORT_CODE_DOYLE = 'SpecialReportCodeDoyle';
|
case SPECIAL_REPORT_CODE_1 = 'SpecialReportCode1';
|
||||||
case SPECIAL_REPORT_CODE_VEGA = 'SpecialReportCodeVega';
|
case SPECIAL_REPORT_CODE_2 = 'SpecialReportCode2';
|
||||||
case SPECIAL_REPORT_CODE_LENNOX = 'SpecialReportCodeLennox';
|
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_PLAYER1 = 'ReadyAtForPlayer1';
|
||||||
case READY_AT_FOR_PLAYER2 = 'ReadyAtForPlayer2';
|
case READY_AT_FOR_PLAYER2 = 'ReadyAtForPlayer2';
|
||||||
case READY_AT_FOR_PLAYER3 = 'ReadyAtForPlayer3';
|
case READY_AT_FOR_PLAYER3 = 'ReadyAtForPlayer3';
|
||||||
|
|||||||
@@ -26,6 +26,37 @@ final class GameDashboardService
|
|||||||
private const LOBBY_MESSAGE_MAX_LENGTH = 500;
|
private const LOBBY_MESSAGE_MAX_LENGTH = 500;
|
||||||
private const LOBBY_CHAT_GRACE_PERIOD_SECONDS = 3600;
|
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(
|
public function __construct(
|
||||||
private readonly GameRepository $gameRepository,
|
private readonly GameRepository $gameRepository,
|
||||||
private readonly SessionRepository $sessionRepository,
|
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
|
public function startSession(Session $session): bool
|
||||||
{
|
{
|
||||||
if ($session->getStatus() !== SessionStatus::CREATED) {
|
if ($session->getStatus() !== SessionStatus::CREATED) {
|
||||||
@@ -286,6 +361,9 @@ final class GameDashboardService
|
|||||||
$this->entityManager->persist($player);
|
$this->entityManager->persist($player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->initializeDecoyUsernames($session);
|
||||||
|
$this->initializeSpecialReportAssignments($session);
|
||||||
|
|
||||||
$session->setStatus(SessionStatus::READY);
|
$session->setStatus(SessionStatus::READY);
|
||||||
$this->entityManager->persist($session);
|
$this->entityManager->persist($session);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|||||||
@@ -959,11 +959,52 @@ class GameResponseService
|
|||||||
$this->entityManager->persist($everyoneVerifiedSetting);
|
$this->entityManager->persist($everyoneVerifiedSetting);
|
||||||
$this->entityManager->flush();
|
$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'));
|
$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
|
private function goToNewDir(string $pwd, string $newPwd, Player $player) : string|bool
|
||||||
{
|
{
|
||||||
$allPossiblePaths = $this->getAllPossiblePaths($player);
|
$allPossiblePaths = $this->getAllPossiblePaths($player);
|
||||||
@@ -1080,7 +1121,7 @@ class GameResponseService
|
|||||||
|
|
||||||
$paths[] = '/var/home';
|
$paths[] = '/var/home';
|
||||||
|
|
||||||
$playerNames = ['root', 'Luke', 'Charles', 'William', 'Peter'];
|
$playerNames = array_merge(['root'], $this->getDecoyUsernames($player->getSession()));
|
||||||
|
|
||||||
$players = $player->getSession()->getPlayers();
|
$players = $player->getSession()->getPlayers();
|
||||||
|
|
||||||
@@ -1490,20 +1531,16 @@ class GameResponseService
|
|||||||
return ['Error reading file'];
|
return ['Error reading file'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$specialFiles = [
|
$specialFiles = [];
|
||||||
'/var/rapports/083_25-39.txt' => [
|
foreach ($this->getSpecialReportAssignments($player->getSession()) as $assignment) {
|
||||||
'setting' => SessionSettingType::SPECIAL_REPORT_CODE_DOYLE,
|
$settingType = SessionSettingType::tryFrom('SpecialReportCode' . $assignment['digit']);
|
||||||
'digit' => 1
|
if ($settingType) {
|
||||||
],
|
$specialFiles[$assignment['file']] = [
|
||||||
'/var/rapports/019_31-11.txt' => [
|
'setting' => $settingType,
|
||||||
'setting' => SessionSettingType::SPECIAL_REPORT_CODE_VEGA,
|
'digit' => $assignment['digit'],
|
||||||
'digit' => 2
|
|
||||||
],
|
|
||||||
'/var/rapports/011_130-62.txt' => [
|
|
||||||
'setting' => SessionSettingType::SPECIAL_REPORT_CODE_LENNOX,
|
|
||||||
'digit' => 3
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($specialFiles[$file])) {
|
if (isset($specialFiles[$file])) {
|
||||||
$everyoneVerifiedSetting = $this->sessionSettingRepository->getSetting($player->getSession(), SessionSettingType::EVERYONE_VERIFIED);
|
$everyoneVerifiedSetting = $this->sessionSettingRepository->getSetting($player->getSession(), SessionSettingType::EVERYONE_VERIFIED);
|
||||||
@@ -1571,7 +1608,7 @@ class GameResponseService
|
|||||||
|
|
||||||
$codes = json_decode($setting->getValue() ?? '[]', true) ?? [];
|
$codes = json_decode($setting->getValue() ?? '[]', true) ?? [];
|
||||||
|
|
||||||
$playerNames = ['Luke', 'Charles', 'William', 'Peter'];
|
$playerNames = $this->getDecoyUsernames($player->getSession());
|
||||||
foreach ($player->getSession()->getPlayers() as $p) {
|
foreach ($player->getSession()->getPlayers() as $p) {
|
||||||
$playerNames[] = $p->getUser()->getUsername();
|
$playerNames[] = $p->getUser()->getUsername();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
Then the screens flood green: containment confirmed. The AI virus's grip on the server unravels file by file.
|
Then the screens flood green: containment confirmed. The AI virus's grip on the server unravels file by file.
|
||||||
</p>
|
</p>
|
||||||
<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.
|
the agency quietly credits an anonymous team of specialists for the save - you.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -121,9 +121,10 @@ class GameDashboardServiceTest extends TestCase
|
|||||||
// SessionSetting (verification progress)
|
// SessionSetting (verification progress)
|
||||||
// Total = 2 * 6 = 12
|
// Total = 2 * 6 = 12
|
||||||
// PLUS Session (persist)
|
// 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');
|
->method('persist');
|
||||||
|
|
||||||
$result = $this->service->startSession($session);
|
$result = $this->service->startSession($session);
|
||||||
@@ -155,9 +156,10 @@ class GameDashboardServiceTest extends TestCase
|
|||||||
// Player (persist) + 5 Settings = 6 persists
|
// Player (persist) + 5 Settings = 6 persists
|
||||||
// Total = 4 * 6 = 24
|
// Total = 4 * 6 = 24
|
||||||
// PLUS Session (persist)
|
// 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');
|
->method('persist');
|
||||||
|
|
||||||
$result = $this->service->startSession($session);
|
$result = $this->service->startSession($session);
|
||||||
@@ -360,8 +362,10 @@ class GameDashboardServiceTest extends TestCase
|
|||||||
// 4. Persist Session status update
|
// 4. Persist Session status update
|
||||||
// 5. 5 Settings for Player 1
|
// 5. 5 Settings for Player 1
|
||||||
// 6. 5 Settings for Player 2
|
// 6. 5 Settings for Player 2
|
||||||
// Total = 1 + 1 + 1 + 1 + 5 + 5 = 14 persists
|
// 7. SessionSetting (decoy usernames)
|
||||||
$this->entityManager->expects($this->exactly(14))
|
// 8. SessionSetting (special report assignments)
|
||||||
|
// Total = 1 + 1 + 1 + 1 + 5 + 5 + 1 + 1 = 16 persists
|
||||||
|
$this->entityManager->expects($this->exactly(16))
|
||||||
->method('persist');
|
->method('persist');
|
||||||
|
|
||||||
$result = $this->service->joinSession('abc-123', $user);
|
$result = $this->service->joinSession('abc-123', $user);
|
||||||
@@ -415,8 +419,10 @@ class GameDashboardServiceTest extends TestCase
|
|||||||
// 3. Player screen persist (during startSession)
|
// 3. Player screen persist (during startSession)
|
||||||
// 4. Session status persist (during startSession)
|
// 4. Session status persist (during startSession)
|
||||||
// 5. 5 Settings for Player (during startSession)
|
// 5. 5 Settings for Player (during startSession)
|
||||||
// Total = 1 + 1 + 1 + 1 + 5 = 9 persists
|
// 6. SessionSetting (decoy usernames)
|
||||||
$this->entityManager->expects($this->exactly(9))
|
// 7. SessionSetting (special report assignments)
|
||||||
|
// Total = 1 + 1 + 1 + 1 + 5 + 1 + 1 = 11 persists
|
||||||
|
$this->entityManager->expects($this->exactly(11))
|
||||||
->method('persist');
|
->method('persist');
|
||||||
|
|
||||||
$session = $this->service->createSession($game, $user, false);
|
$session = $this->service->createSession($game, $user, false);
|
||||||
|
|||||||
Reference in New Issue
Block a user