Some improvements

This commit is contained in:
armel committed 2026-09-08 22:33:51 +02:00
1 parent a914bd74fe
commit 729c0be834
16 files changed
+577 -36

No files matched your search

+1 -1
View File
@@ -15,7 +15,7 @@ final class GameEngine
{
private const PICKUP_INTERVAL = 10.0;
private const MAX_ZOMBIES = 30;
private const MAX_ZOMBIES = 45;
public function __construct(
private readonly Room $room,
+9 -9
View File
@@ -16,42 +16,42 @@ final class Spawner
{
public function spawnInterval(float $elapsed): float
{
return max(0.35, 1.2 - $elapsed * 0.008);
return max(0.22, 1.0 - $elapsed * 0.01);
}
public function speedMultiplier(float $elapsed): float
{
return min(2.2, 1.0 + $elapsed * 0.004);
return min(2.6, 1.0 + $elapsed * 0.006);
}
private function tankChance(float $elapsed): float
{
return min(0.5, 0.15 + $elapsed * 0.002);
return min(0.55, 0.18 + $elapsed * 0.0025);
}
private function megaTankChance(float $elapsed): float
{
return min(0.22, $elapsed * 0.0015);
return min(0.3, $elapsed * 0.002);
}
private function runnerChance(float $elapsed): float
{
return min(0.4, 0.2 + $elapsed * 0.001);
return min(0.45, 0.22 + $elapsed * 0.0015);
}
private function leaperChance(float $elapsed): float
{
return min(0.12, 0.04 + $elapsed * 0.06);
return min(0.16, 0.05 + $elapsed * 0.08);
}
private function witchChance(float $elapsed): float
{
return min(0.3, 0.1 + $elapsed * 0.002);
return min(0.35, 0.12 + $elapsed * 0.0025);
}
private function megaWitchChance(float $elapsed): float
{
return $elapsed < 45.0 ? 0.0 : min(0.12, ($elapsed - 45.0) * 0.0008);
return $elapsed < 40.0 ? 0.0 : min(0.16, ($elapsed - 40.0) * 0.001);
}
public function spawnZombie(Room $room): void
@@ -204,7 +204,7 @@ final class Spawner
$room->entities->append(new Leaper($x, $y, $room->entities, $room, $this->initialTarget($room), [
'speed' => (80.0 + $this->rand() * 40.0) * $speedMultiplier,
'size' => 28.0,
'health' => 3,
'health' => 10,
'knockback' => 240.0,
'scoreValue' => 6,
'color' => [0.15, 0.25, 0.75, 1.0],
+189
View File
@@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace App\Game;
use App\Game\Entities\Witch;
use App\Game\Entities\WitchProjectile;
use App\Game\Entities\Zombie;
/**
* Read-only benchmark harness for the authoritative game simulation.
*
* Measures per-tick elapsed time of GameEngine::tick() and the serialized
* snapshot payload size produced by GameSimulator::broadcastRoom() at various
* entity counts, so we can reason about capacity without touching production
* logic or firing real broadcasts.
*/
final class StressTestHarness
{
/** Reliable per-frame budget of the fixed 60 Hz simulation loop. */
public const TICK_BUDGET_SECONDS = 1.0 / 60.0;
/** Reverb hard cap on a single broadcast message (config/reverb.php). */
public const REVERB_MAX_BYTES = 20000;
public const SCENARIOS = ['zombies', 'projectiles'];
/**
* Measures sustained per-tick cost while keeping the entity population at
* (and refilling to) the requested count each tick, so the sample reflects
* steady-state load rather than a population that the players quickly thin
* out. The cost measured is the full collision/resolution path of
* GameEngine::tick(), which is what determines whether the 60 Hz loop can
* keep up.
*
* @return array{entities: int, scenarios: array<string, array{avgMs: float, medianMs: float, maxMs: float, budgetOk: bool}>}
*/
public function measureTickTime(int $entityCount, int $players = 2, int $ticks = 600, array $scenarios = self::SCENARIOS): array
{
$results = ['entities' => $entityCount, 'scenarios' => []];
foreach ($scenarios as $scenario) {
$room = $this->makeRoom($entityCount, $scenario, $players);
$engine = new GameEngine($room, new Spawner);
for ($i = 0; $i < 30; $i++) {
$this->stepAndRefill($engine, $room, $entityCount, $scenario);
}
$samples = [];
for ($i = 0; $i < $ticks; $i++) {
$start = hrtime(true);
$this->stepAndRefill($engine, $room, $entityCount, $scenario);
$samples[] = (hrtime(true) - $start) / 1e6;
}
sort($samples);
$avg = array_sum($samples) / count($samples);
$median = $samples[(int) floor(count($samples) / 2)];
$max = $samples[count($samples) - 1];
$results['scenarios'][$scenario] = [
'avgMs' => round($avg, 3),
'medianMs' => round($median, 3),
'maxMs' => round($max, 3),
'budgetOk' => $avg <= self::TICK_BUDGET_SECONDS * 1000.0,
];
}
return $results;
}
/** Run one tick, then re-seed the target population for steady-state load. */
private function stepAndRefill(GameEngine $engine, Room $room, int $target, string $scenario): void
{
$engine->tick(self::TICK_BUDGET_SECONDS);
$current = 0;
foreach ($room->entities as $entity) {
if ($scenario === 'projectiles') {
if ($entity instanceof Witch) {
$current++;
}
} elseif ($entity instanceof Zombie) {
$current++;
}
}
for ($i = $current; $i < $target; $i++) {
$x = 300.0 + (($i + $room->seq) % 20) * 40.0;
$y = 100.0 + intdiv($i + $room->seq, 20) * 60.0;
if ($scenario === 'projectiles') {
$room->entities->append(new Witch($x, $y, $room->entities, $room, $this->firstPlayer($room), [
'emergeTime' => 0.0,
'fireInterval' => 0.4,
]));
} else {
$room->entities->append(new Zombie($x, $y, $room->entities, $room, [
'emergeTime' => 0.0,
'speed' => 150.0,
'health' => 1,
], fn (Zombie $z) => $z->target = $this->firstPlayer($room)));
}
}
}
/**
* @return array{entities: int, scenarios: array<string, array{bytes: int, bytesPerEntity: float, exceedsReverb: bool}>}
*/
public function measureSnapshotBytes(int $entityCount, int $players = 2, array $scenarios = self::SCENARIOS): array
{
$results = ['entities' => $entityCount, 'scenarios' => []];
foreach ($scenarios as $scenario) {
$room = $this->makeRoom($entityCount, $scenario, $players);
$payload = null;
$simulator = new GameSimulator(function (Room $r, array $playersList, array $entities, array $events) use (&$payload): void {
$payload = [
'state' => $r->state,
'elapsed' => round($r->elapsed, 2),
'seq' => $r->seq,
'players' => $playersList,
'entities' => $entities,
'events' => $events,
];
});
$simulator->broadcastRoom($room);
$bytes = strlen(json_encode($payload) ?: '');
$results['scenarios'][$scenario] = [
'bytes' => $bytes,
'bytesPerEntity' => $entityCount > 0 ? round($bytes / $entityCount, 1) : 0.0,
'exceedsReverb' => $bytes > self::REVERB_MAX_BYTES,
];
}
return $results;
}
private function makeRoom(int $entityCount, string $scenario, int $players): Room
{
$room = new Room('bench');
$room->state = Room::STATE_PLAYING;
for ($i = 0; $i < $players; $i++) {
$input = new InputState;
$player = new Player((string) $i, "P$i", $input, $room->entities, $room, [
'x' => 200.0 + $i * 60.0,
'y' => 540.0,
'fireRate' => 0.1,
'bulletCount' => 3,
]);
$room->addPlayer($player);
}
for ($i = 0; $i < $entityCount; $i++) {
$x = 300.0 + ($i % 20) * 40.0;
$y = 100.0 + intdiv($i, 20) * 60.0;
if ($scenario === 'projectiles') {
$room->entities->append(new Witch($x, $y, $room->entities, $room, $this->firstPlayer($room), [
'emergeTime' => 0.0,
'fireInterval' => 0.5,
]));
} else {
$room->entities->append(new Zombie($x, $y, $room->entities, $room, [
'emergeTime' => 0.0,
'speed' => 150.0,
'health' => 2,
], fn (Zombie $z) => $z->target = $this->firstPlayer($room)));
}
}
foreach ($room->entities as $entity) {
if ($entity instanceof Zombie || $entity instanceof WitchProjectile) {
$entity->target = $this->firstPlayer($room);
}
}
return $room;
}
private function firstPlayer(Room $room): ?Player
{
return $room->players === [] ? null : $room->players[array_key_first($room->players)];
}
}