expectException(InvalidArgumentException::class); $createUnit(); } /** * @return iterable */ public static function invalidUnitProvider(): iterable { yield 'empty unit id' => [ static fn (): UnitState => new UnitState('', 'alpha', new Position(0, 0), 10, 10, 4, 2, 4, 2), ]; yield 'empty team id' => [ static fn (): UnitState => new UnitState('unit-1', '', new Position(0, 0), 10, 10, 4, 2, 4, 2), ]; yield 'zero maximum health' => [ static fn (): UnitState => new UnitState('unit-1', 'alpha', new Position(0, 0), 0, 0, 4, 2, 4, 2), ]; yield 'negative health' => [ static fn (): UnitState => new UnitState('unit-1', 'alpha', new Position(0, 0), 10, -1, 4, 2, 4, 2), ]; yield 'health above maximum' => [ static fn (): UnitState => new UnitState('unit-1', 'alpha', new Position(0, 0), 10, 11, 4, 2, 4, 2), ]; yield 'negative attack' => [ static fn (): UnitState => new UnitState('unit-1', 'alpha', new Position(0, 0), 10, 10, -1, 2, 4, 2), ]; yield 'negative defense' => [ static fn (): UnitState => new UnitState('unit-1', 'alpha', new Position(0, 0), 10, 10, 4, -1, 4, 2), ]; yield 'zero speed' => [ static fn (): UnitState => new UnitState('unit-1', 'alpha', new Position(0, 0), 10, 10, 4, 2, 0, 2), ]; yield 'negative actions remaining' => [ static fn (): UnitState => new UnitState('unit-1', 'alpha', new Position(0, 0), 10, 10, 4, 2, 4, -1), ]; yield 'too many actions remaining' => [ static fn (): UnitState => new UnitState('unit-1', 'alpha', new Position(0, 0), 10, 10, 4, 2, 4, 3), ]; } public function testItReportsWhetherItIsDefeated(): void { self::assertTrue($this->unit(health: 0)->isDefeated()); self::assertFalse($this->unit(health: 1)->isDefeated()); } public function testMovingReturnsAChangedCopy(): void { $unit = $this->unit(); $moved = $unit->moveTo(new Position(2, 3)); self::assertNotSame($unit, $moved); self::assertSame('0:0', $unit->position->key()); self::assertSame('2:3', $moved->position->key()); } public function testSpendingAnActionReturnsAChangedCopyAndRejectsExhaustedUnits(): void { $unit = $this->unit(actionsRemaining: 1); $spent = $unit->spendAction(); self::assertSame(1, $unit->actionsRemaining); self::assertSame(0, $spent->actionsRemaining); $this->expectException(InvalidArgumentException::class); $spent->spendAction(); } public function testDamageCannotReduceHealthBelowZero(): void { $unit = $this->unit(health: 3); $damaged = $unit->takeDamage(5); self::assertSame(3, $unit->health); self::assertSame(0, $damaged->health); } public function testNegativeDamageIsANoOp(): void { $unit = $this->unit(health: 7); $damaged = $unit->takeDamage(-3); self::assertNotSame($unit, $damaged); self::assertSame(7, $damaged->health); } public function testMarkingAttackedReturnsAChangedCopy(): void { $unit = $this->unit(); $attacked = $unit->markAttacked(); self::assertFalse($unit->hasAttacked); self::assertTrue($attacked->hasAttacked); } public function testStartingTurnResetsALivingUnit(): void { $unit = $this->unit(actionsRemaining: 0, hasAttacked: true); $started = $unit->startTurn(); self::assertNotSame($unit, $started); self::assertSame(2, $started->actionsRemaining); self::assertFalse($started->hasAttacked); } public function testStartingTurnReturnsTheSameDefeatedUnit(): void { $unit = $this->unit(health: 0, actionsRemaining: 0, hasAttacked: true); self::assertSame($unit, $unit->startTurn()); } private function unit( int $health = 10, int $actionsRemaining = 2, bool $hasAttacked = false, ): UnitState { return new UnitState( 'unit-1', 'alpha', new Position(0, 0), 10, $health, 4, 2, 4, $actionsRemaining, $hasAttacked, ); } }