feat: validate and resolve unit movement

This commit is contained in:
Keith Solomon
2026-07-04 15:37:05 -05:00
parent 5297b41208
commit 1550e2920b
3 changed files with 125 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace BattleForge\Domain;
final class CombatEngine
{
public function move(MatchState $match, string $unitId, Position $destination): MatchState
{
$this->assertMatchActive($match);
$unit = $match->unit($unitId);
$this->assertCanAct($match, $unit);
$occupied = [];
foreach ($match->units as $otherUnit) {
if ($otherUnit->id !== $unit->id && !$otherUnit->isDefeated()) {
$occupied[] = $otherUnit->position;
}
}
$reachable = $match->battlefield->reachable($unit->position, $unit->speed, $occupied);
if ($destination->key() === $unit->position->key() || !isset($reachable[$destination->key()])) {
throw new CombatException('Destination is not reachable.');
}
$movedUnit = $unit->moveTo($destination)->spendAction();
$next = $match->withUnit($movedUnit);
$actionLog = [...$match->actionLog, "{$unit->id} moved to {$destination->key()}"];
return $next->copy(actionLog: $actionLog);
}
private function assertMatchActive(MatchState $match): void
{
if ($match->winnerTeamId !== null) {
throw new CombatException('The match is already complete.');
}
}
private function assertCanAct(MatchState $match, UnitState $unit): void
{
if ($unit->teamId !== $match->activeTeamId) {
throw new CombatException("It is not this unit's turn.");
}
if ($unit->isDefeated()) {
throw new CombatException('Defeated units cannot act.');
}
if ($unit->actionsRemaining === 0) {
throw new CombatException('Unit has no actions remaining.');
}
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace BattleForge\Domain;
use DomainException;
final class CombatException extends DomainException
{
}