unit('unit-1', 'alpha', new Position(0, 0)); $match = new MatchState(new Battlefield(8, 8), [$unit], 'alpha'); $moved = $unit->moveTo(new Position(1, 0))->spendAction(); $next = $match->withUnit($moved); self::assertSame('0:0', $match->unit('unit-1')->position->key()); self::assertSame('1:0', $next->unit('unit-1')->position->key()); self::assertSame(1, $next->unit('unit-1')->actionsRemaining); } public function testMoveUpdatesUnitAndAppendsActionLogWithoutMutatingMatch(): void { $match = new MatchState( new Battlefield(8, 8), [ $this->unit('alpha-1', 'alpha', new Position(0, 0)), $this->unit('bravo-1', 'bravo', new Position(7, 7)), ], 'alpha', ); $next = (new CombatEngine())->move($match, 'alpha-1', new Position(2, 0)); self::assertSame('0:0', $match->unit('alpha-1')->position->key()); self::assertSame('2:0', $next->unit('alpha-1')->position->key()); self::assertSame(1, $next->unit('alpha-1')->actionsRemaining); self::assertSame(['alpha-1 moved to 2:0'], $next->actionLog); } public function testMoveRejectsUnitFromInactiveTeam(): void { $match = new MatchState( new Battlefield(8, 8), [ $this->unit('alpha-1', 'alpha', new Position(0, 0)), $this->unit('bravo-1', 'bravo', new Position(7, 7)), ], 'alpha', ); $this->expectException(CombatException::class); $this->expectExceptionMessage("It is not this unit's turn."); (new CombatEngine())->move($match, 'bravo-1', new Position(6, 7)); } public function testMoveRejectsOccupiedDestination(): void { $match = new MatchState( new Battlefield(8, 8), [ $this->unit('alpha-1', 'alpha', new Position(0, 0)), $this->unit('alpha-2', 'alpha', new Position(1, 0)), $this->unit('bravo-1', 'bravo', new Position(7, 7)), ], 'alpha', ); $this->expectException(CombatException::class); $this->expectExceptionMessage('Destination is not reachable.'); (new CombatEngine())->move($match, 'alpha-1', new Position(1, 0)); } private function unit(string $id, string $team, Position $position): UnitState { return new UnitState($id, $team, $position, 10, 10, 4, 2, 4, 2); } }