<?phpnamespace Tests;use App\Scene\CatBounceScene;use App\Scene\ConcentricScene;use App\Scene\FilmScene;use App\Scene\GapCircleScene;use App\Scene\MarbleScene;use App\Scene\MultiplyScene;use App\Scene\PlinkoScene;use App\Scene\RainPondScene;use App\Scene\ZoomRingsScene;use JumPHP\Physics2d\Ring;use JumPHP\Physics2d\World;use JumPHP\Shorts\Melody;use JumPHP\Shorts\Mixer;use JumPHP\Shorts\Recorder;use JumPHP\Shorts\Voice;use JumPHP\Testing\GameTestCase;final class BounceTest extends GameTestCase{ public function testTheCircleIsClosedOnThreeQuartersAndItsOpeningTurns(): void { $this->game->start(GapCircleScene::class, ['seed' => 3]); $ring = $this->world()->rings()[0]; self::assertEqualsWithDelta(M_PI / 2, $ring->gap, 1e-9); $angle = $ring->angle; $this->game->run(30); self::assertNotEqualsWithDelta($angle, $ring->angle, 0.1); } public function testTheBallBouncesInTheCircleAndEachBounceIsTheNextNote(): void { $this->game->start(GapCircleScene::class, ['seed' => 3, 'melody' => 'C4 E4 G4']); $this->game->run(150); $notes = array_values(array_filter($this->recorder()->notes(), fn (array $n) => $n['wave'] === 'pluck')); self::assertGreaterThan(2, count($notes)); $expected = [Melody::frequency('C4'), Melody::frequency('E4'), Melody::frequency('G4')]; foreach (array_slice($notes, 0, 6) as $i => $note) { self::assertEqualsWithDelta($expected[$i % 3], $note['freq'], 0.01); } $this->game->shot('gap'); } public function testTheBallEventuallyFindsTheExitAndTheNextOpeningIsNarrower(): void { $this->game->start(GapCircleScene::class, ['seed' => 3]); $first = $this->world()->rings()[0]->gap; $ball = $this->world()->balls()[0]; // thrown straight up at the opening $ball->x = 135.0; $ball->y = 250.0; $ball->vx = 0.0; $ball->vy = -420.0; $this->game->run(70); self::assertGreaterThanOrEqual(1, $this->game->peek('escapes')); $this->game->run(110); // the banner goes away, the next round starts self::assertGreaterThan(0, $this->game->peek('round')); self::assertLessThan($first, $this->world()->rings()[0]->gap); } public function testEachHitOnTheWallMakesANewBall(): void { $this->game->start(MultiplyScene::class, ['seed' => 5]); self::assertCount(1, $this->world()->balls()); $this->game->run(60 * 3); self::assertGreaterThan(4, count($this->world()->balls())); $this->game->run(60 * 5); self::assertSame(MultiplyScene::MAX_BALLS, count($this->world()->balls()), 'full, and the oldest balls fade so it never goes past'); $this->game->shot('multiply'); } public function testTheRingsBreakOneAfterTheOtherFromTheInside(): void { $this->game->start(ConcentricScene::class, ['seed' => 3]); $solid = fn () => count(array_filter($this->world()->rings(), fn (Ring $r) => $r->solid)); self::assertSame(ConcentricScene::RINGS, $solid()); $this->game->run(60 * 3); self::assertLessThan(ConcentricScene::RINGS, $solid()); $broken = array_values(array_filter($this->world()->rings(), fn (Ring $r) => !$r->solid)); self::assertSame($this->world()->rings()[0], $broken[0], 'the inner ring breaks first'); $this->game->shot('rings'); } public function testABrokenRingPlaysABellHigherThanTheBounces(): void { $this->game->start(ConcentricScene::class, ['seed' => 3, 'melody' => 'C4']); $this->game->run(60 * 3); $bells = array_filter($this->recorder()->notes(), fn (array $n) => $n['wave'] === 'bell'); self::assertNotEmpty($bells); foreach ($bells as $bell) { self::assertGreaterThan(Melody::frequency('C4') * 1.5, $bell['freq']); } } public function testMarblesDropAndRingOnTheRamps(): void { $this->game->start(MarbleScene::class, ['seed' => 2]); $this->game->run(60 * 5 + 20); self::assertGreaterThan(10, $this->game->peek('dropped')); self::assertLessThanOrEqual(MarbleScene::MAX_ALIVE, count($this->world()->balls())); self::assertGreaterThan(10, count($this->recorder()->notes())); $this->game->shot('marble'); } public function testTheSameSeedMakesTheSameVideo(): void { $run = function (string $scene, int $seed): array { $this->game->start($scene, ['seed' => $seed]); $this->game->run(60); return array_map(fn (array $n) => [$n['time'], $n['freq']], $this->recorder()->notes()); }; foreach ([GapCircleScene::class, MultiplyScene::class, ConcentricScene::class, MarbleScene::class, ZoomRingsScene::class, CatBounceScene::class] as $scene) { $a = $run($scene, 21); self::assertNotEmpty($a, $scene); self::assertSame($a, $run($scene, 21), $scene); } } public function testTheFilmSceneStartsTheSceneOfTheTemplate(): void { foreach (FilmScene::TEMPLATES as $template => $class) { $this->game->start(FilmScene::class, ['template' => $template, 'seed' => 4]); $this->game->run(2); self::assertInstanceOf($class, $this->game->scene(), $template); } } public function testTheNotesAreMixedIntoASoundTrackOfTheVideosLength(): void { $wav = Mixer::mix([['time' => 0.5, 'freq' => 440.0], ['time' => 1.0, 'freq' => 660.0]], 2.0); self::assertSame('RIFF', substr($wav, 0, 4)); self::assertCount(2 * Voice::RATE, unpack('v*', substr($wav, 44))); } private function world(): World { return $this->game->peek('world'); } private function recorder(): Recorder { return $this->game->peek('recorder'); }}