- Game1 terminal: flesh out the virtual filesystem with a realistic spread of Linux directories/files (~70 dirs, ~140 files) so it no longer reads as an obviously small puzzle set, without touching any win-condition or rapport files. - Add app:hints:check command + a php-cron container (BusyBox crond) that nudges players who haven't contacted every teammate 5 minutes into a session, via a new 'hint' Mercure message type. - Log the cron command's output to var/log/cron/cron.log and rotate it (25MB / 90 days) via logrotate, run daily from the same crontab. - Redirect PHP's error_log and Symfony's prod app/deprecation logs from stderr-only into var/log/php/*.log (kept alongside stderr), with the same rotation policy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
135 lines
4.3 KiB
PHP
135 lines
4.3 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Command;
|
|
|
|
use App\Game\Entity\Session;
|
|
use App\Game\Enum\GameSettingType;
|
|
use App\Game\Enum\SessionSettingType;
|
|
use App\Game\Enum\SessionStatus;
|
|
use App\Game\Repository\GameSettingRepository;
|
|
use App\Game\Repository\SessionRepository;
|
|
use App\Game\Repository\SessionSettingRepository;
|
|
use Symfony\Component\Console\Attribute\AsCommand;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
use Symfony\Component\Mercure\HubInterface;
|
|
use Symfony\Component\Mercure\Update;
|
|
|
|
#[AsCommand(
|
|
name: 'app:hints:check',
|
|
description: 'Checks running game sessions and sends mainframe hints to players who are behind schedule.'
|
|
)]
|
|
final class SendMainframeHintsCommand extends Command
|
|
{
|
|
private const DEFAULT_TOTAL_TIME = 3600;
|
|
|
|
public function __construct(
|
|
private readonly SessionRepository $sessionRepository,
|
|
private readonly SessionSettingRepository $sessionSettingRepository,
|
|
private readonly GameSettingRepository $gameSettingRepository,
|
|
private readonly HubInterface $hub,
|
|
) {
|
|
parent::__construct();
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
$sessions = $this->sessionRepository->findBy(['status' => SessionStatus::PLAYING]);
|
|
$hintsSent = 0;
|
|
|
|
foreach ($sessions as $session) {
|
|
if ($this->checkContactHint($session)) {
|
|
$hintsSent++;
|
|
}
|
|
}
|
|
|
|
$output->writeln(sprintf('<info>Checked %d running session(s), sent %d hint(s).</info>', count($sessions), $hintsSent));
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* "Get in touch" hint: if 5 minutes into the game the players haven't messaged
|
|
* everyone (a private message to each other player, plus one broadcast), nudge them.
|
|
* Repeats every run of this command for as long as the condition still holds.
|
|
*/
|
|
private function checkContactHint(Session $session): bool
|
|
{
|
|
$elapsed = $this->getElapsedPlayingSeconds($session);
|
|
if ($elapsed === null || $elapsed < 300) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->allPlayersHaveContactedEveryone($session)) {
|
|
return false;
|
|
}
|
|
|
|
$this->publishHint($session, 'Get in contact with your fellow agents to work together on defeating this AI virus.');
|
|
|
|
return true;
|
|
}
|
|
|
|
private function getElapsedPlayingSeconds(Session $session): ?int
|
|
{
|
|
$timer = $session->getTimer();
|
|
if ($timer === null) {
|
|
return null;
|
|
}
|
|
|
|
$totalTimeSetting = $this->gameSettingRepository->getSetting($session->getGame(), GameSettingType::TOTAL_TIME);
|
|
$totalTime = $totalTimeSetting ? (int)$totalTimeSetting->getValue() : self::DEFAULT_TOTAL_TIME;
|
|
|
|
$startedAt = $timer - $totalTime;
|
|
|
|
return time() - $startedAt;
|
|
}
|
|
|
|
private function allPlayersHaveContactedEveryone(Session $session): bool
|
|
{
|
|
$players = $session->getPlayers();
|
|
$screens = [];
|
|
|
|
foreach ($players as $player) {
|
|
if ($player->getScreen() === null) {
|
|
return false;
|
|
}
|
|
$screens[] = $player->getScreen();
|
|
}
|
|
|
|
foreach ($players as $player) {
|
|
$screen = $player->getScreen();
|
|
$trackingSettingName = SessionSettingType::tryFrom('ChatTrackingForPlayer' . $screen);
|
|
if (!$trackingSettingName) {
|
|
return false;
|
|
}
|
|
|
|
$setting = $this->sessionSettingRepository->getSetting($session, $trackingSettingName, $player);
|
|
$tracking = $setting ? (json_decode($setting->getValue() ?? '[]', true) ?? []) : [];
|
|
|
|
if (!in_array(0, $tracking)) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($screens as $otherScreen) {
|
|
if ($otherScreen !== $screen && !in_array($otherScreen, $tracking)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function publishHint(Session $session, string $message): void
|
|
{
|
|
$topic = '/game/hub/' . $session->getId();
|
|
try {
|
|
$this->hub->publish(new Update($topic, json_encode([0, $message, 'hint'])));
|
|
} catch (\Exception $e) {
|
|
// Mercure might be down
|
|
}
|
|
}
|
|
}
|