initial commit

This commit is contained in:
armel committed 2026-09-07 20:37:40 +02:00
commit e8c3f9862a
193 files changed
+29795

No files matched your search

+226
View File
@@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Game;
use App\Game\Entities\Boss;
use App\Game\Entities\Bullet;
use App\Game\Entities\Zombie;
use App\Game\GameEngine;
use App\Game\InputState;
use App\Game\Player;
use App\Game\Room;
use App\Game\Spawner;
use App\Game\Vector2;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class GameEngineTest extends TestCase
{
#[Test]
public function tick_does_nothing_when_not_playing(): void
{
$room = new Room('test');
$engine = new GameEngine($room, new Spawner);
$engine->tick(1.0 / 60.0);
$this->assertSame(Room::STATE_LOBBY, $room->state);
}
#[Test]
public function zombies_spawn_during_play(): void
{
$room = new Room('test');
$room->state = Room::STATE_PLAYING;
$engine = new GameEngine($room, new Spawner);
$room->addPlayer(new Player('p1', 'Tester', new InputState(fire: true), $room->entities, $room, ['x' => 960.0, 'y' => 540.0]));
$sawZombie = false;
for ($i = 0; $i < 600; $i++) {
$engine->tick(1.0 / 60.0);
if ($room->entities->findByKind('zombie') !== []) {
$sawZombie = true;
}
if ($room->state === Room::STATE_OVER) {
break;
}
}
$this->assertTrue($sawZombie, 'zombies should spawn during play');
}
#[Test]
public function game_ends_when_all_players_die(): void
{
$room = new Room('test', 1);
$room->state = Room::STATE_PLAYING;
$engine = new GameEngine($room, new Spawner);
$player = new Player('p1', 'Tester', new InputState, $room->entities, $room, [
'x' => 960.0,
'y' => 540.0,
'health' => 1,
]);
$room->addPlayer($player);
$zombie = new Zombie(944.4, 540.0, $room->entities, $room, ['emergeTime' => 0.0]);
$zombie->target = $player;
$room->entities->append($zombie);
$engine->tick(1.0 / 60.0);
$this->assertSame(Room::STATE_OVER, $room->state);
$this->assertTrue($player->dead);
}
#[Test]
public function bullets_damage_zombies(): void
{
$room = new Room('test');
$room->state = Room::STATE_PLAYING;
$engine = new GameEngine($room, new Spawner);
$input = new InputState(fire: true);
$player = new Player('p1', 'T', $input, $room->entities, $room, ['x' => 500.0, 'y' => 500.0]);
$room->addPlayer($player);
$zombie = new Zombie(1000.0, 500.0, $room->entities, $room, ['emergeTime' => 0.0]);
$zombie->target = $player;
$room->entities->append($zombie);
$engine->tick(1.0 / 60.0);
$bullets = $room->entities->findByKind('bullet');
$this->assertNotEmpty($bullets, 'player shot a bullet');
}
#[Test]
public function boss_spawns_at_score_threshold(): void
{
$room = new Room('test', 1);
$room->state = Room::STATE_PLAYING;
$engine = new GameEngine($room, new Spawner);
$player = new Player('p1', 'T', new InputState, $room->entities, $room, ['x' => 500.0, 'y' => 500.0]);
$room->addPlayer($player);
$zombie = new Zombie(510.0, 500.0, $room->entities, $room, ['health' => 1, 'emergeTime' => 0.0, 'scoreValue' => 55]);
$zombie->target = $player;
$room->entities->append($zombie);
$bullet = new Bullet(509.0, 500.0, new Vector2(1.0, 0.0), $room->entities, $room, $player, 800.0, 8.0, 1);
$room->entities->append($bullet);
$engine->tick(1.0 / 60.0);
$this->assertGreaterThan(0, $player->score, 'player killed zombie at score threshold');
$this->assertTrue($room->hasBoss(), 'boss should spawn when score reaches threshold');
}
#[Test]
public function kills_increment_score(): void
{
$room = new Room('test');
$room->state = Room::STATE_PLAYING;
$engine = new GameEngine($room, new Spawner);
$player = new Player('p1', 'T', new InputState, $room->entities, $room, ['x' => 500.0, 'y' => 500.0]);
$room->addPlayer($player);
$zombie = new Zombie(510.0, 500.0, $room->entities, $room, ['health' => 1, 'emergeTime' => 0.0]);
$zombie->target = $player;
$room->entities->append($zombie);
$bullet = new Bullet(509.0, 500.0, new Vector2(1.0, 0.0), $room->entities, $room, $player, 800.0, 8.0, 10);
$room->entities->append($bullet);
$engine->tick(1.0 / 60.0);
$this->assertGreaterThan(0, $player->score, 'player scored from killing zombie');
}
#[Test]
public function zombies_appear_when_megatank_dies(): void
{
$room = new Room('test');
$room->state = Room::STATE_PLAYING;
$room->elapsed = 400.0;
$spawner = new Spawner;
$engine = new GameEngine($room, $spawner);
$player = new Player('p1', 'T', new InputState, $room->entities, $room, ['x' => 960.0, 'y' => 540.0]);
$room->addPlayer($player);
$mega = null;
for ($i = 0; $i < 100 && $mega === null; $i++) {
$spawner->spawnZombie($room);
$megas = $room->entities->findByKind('megatank');
if ($megas !== []) {
$mega = $megas[0];
}
}
$this->assertNotNull($mega, 'a megatank should spawn when elapsed is high');
$mega->pos = new Vector2(960.0, 250.0);
$zombiesBefore = count($room->entities->findByKind('zombie'));
$portalsBefore = $this->portalEventCount($room);
$room->entities->append(new Bullet(960.0, 250.0, new Vector2(1.0, 0.0), $room->entities, $room, $player, 800.0, 8.0, 100));
$engine->tick(1.0 / 60.0);
$this->assertSame([], $room->entities->findByKind('megatank'), 'megatank should be removed on death');
$this->assertSame($zombiesBefore + 3, count($room->entities->findByKind('zombie')), 'megatank death should spawn three zombies');
$this->assertSame($portalsBefore + 3, $this->portalEventCount($room), 'each spawned zombie should get a portal effect');
$this->assertContains('kill', array_map(fn (array $ev) => $ev['k'], $room->events));
}
#[Test]
public function witches_appear_when_mega_witch_dies(): void
{
$room = new Room('test');
$room->state = Room::STATE_PLAYING;
$spawner = new Spawner;
$engine = new GameEngine($room, $spawner);
$player = new Player('p1', 'T', new InputState, $room->entities, $room, ['x' => 960.0, 'y' => 540.0]);
$room->addPlayer($player);
$spawner->spawnMegaWitch($room, 960.0, 250.0, 1.0, 1.0);
$this->assertSame(1, count($room->entities->findByKind('megawitch')));
$portalsBefore = $this->portalEventCount($room);
$mega = $room->entities->findByKind('megawitch')[0];
$room->entities->append(new Bullet($mega->pos->x, $mega->pos->y, new Vector2(1.0, 0.0), $room->entities, $room, $player, 800.0, 8.0, 100));
$engine->tick(1.0 / 60.0);
$this->assertSame([], $room->entities->findByKind('megawitch'));
$this->assertCount(3, $room->entities->findByKind('witch'));
$this->assertSame($portalsBefore + 3, $this->portalEventCount($room));
}
#[Test]
public function emerging_minions_report_emerge_progress_in_snapshot(): void
{
$room = new Room('test');
$zombie = new Zombie(10.0, 10.0, $room->entities, $room, ['emergeTime' => 0.8]);
$snap = $zombie->toSnapshot();
$this->assertSame(0.8, $snap['er']);
$this->assertSame(0.8, $snap['et']);
$zombie->update(0.3);
$this->assertSame(0.5, $zombie->toSnapshot()['er'], 'emerge remaining should shrink as the zombie emerges');
$plain = new Zombie(30.0, 30.0, $room->entities, $room);
$this->assertArrayNotHasKey('er', $plain->toSnapshot(), 'non-emerging entities need no emerge data');
}
private function portalEventCount(Room $room): int
{
$count = 0;
foreach ($room->events as $event) {
if ($event['k'] === 'portal') {
$count++;
}
}
return $count;
}
#[Test]
public function boss_defeated_event_emitted_on_boss_death(): void
{
$room = new Room('test');
$room->state = Room::STATE_PLAYING;
$engine = new GameEngine($room, new Spawner);
$player = new Player('p1', 'T', new InputState, $room->entities, $room, ['x' => 500.0, 'y' => 500.0]);
$room->addPlayer($player);
$boss = new Boss(505.0, 500.0, $room->entities, $room, $player, [
'health' => 1,
'emergeTime' => 0.0,
]);
$boss->target = $player;
$room->entities->append($boss);
$bullet = new Bullet(503.0, 500.0, new Vector2(1.0, 0.0), $room->entities, $room, $player, 800.0, 8.0, 10);
$room->entities->append($bullet);
$engine->tick(1.0 / 60.0);
$eventKinds = array_map(fn (array $ev) => $ev['k'], $room->events);
$this->assertContains('bossdefeated', $eventKinds);
}
}