feat(combat): water traversal costs 3 and ends the turn
This commit is contained in:
@@ -249,6 +249,59 @@ final class PostMatchActionTest extends TestCase
|
||||
self::assertSame(409, $response->status);
|
||||
}
|
||||
|
||||
public function testMoveIntoWaterEndsTurn(): void
|
||||
{
|
||||
// Build a match where alpha-1 is on dry ground next to a water tile;
|
||||
// alpha-2 and alpha-3 are parked out of the way so the move path is
|
||||
// clear, and the move target (0,1) becomes a water tile whose cost (3)
|
||||
// matches alpha-1's speed budget.
|
||||
$match = $this->freshMatch();
|
||||
$matchArray = ScenarioSerializer::matchToArray($match);
|
||||
$matchArray['units'][0]['position'] = ['x' => 0, 'y' => 0];
|
||||
$matchArray['units'][0]['speed'] = 3;
|
||||
$matchArray['units'][1]['position'] = ['x' => 0, 'y' => 3]; // alpha-2 clear of alpha-1's path
|
||||
$matchArray['units'][2]['position'] = ['x' => 0, 'y' => 2]; // alpha-3 clear of alpha-1's path
|
||||
$matchArray['units'][3]['position'] = ['x' => 7, 'y' => 7]; // bravo-1 stays put
|
||||
$matchArray['units'][4]['position'] = ['x' => 6, 'y' => 7]; // bravo-2
|
||||
$matchArray['units'][5]['position'] = ['x' => 5, 'y' => 7]; // bravo-3
|
||||
$matchArray['battlefield']['terrain'] = ['0:1' => 'water'];
|
||||
$match = ScenarioSerializer::matchFromArray($matchArray);
|
||||
|
||||
$token = $this->tokenFor($match);
|
||||
[$csrf, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
|
||||
$response = (new PostMatchMove(self::SECRET))->handle(
|
||||
$this->buildRequest(
|
||||
match: $match,
|
||||
csrf: $csrf,
|
||||
turnToken: $token,
|
||||
cookies: ['__csrf' => $cookie, '__csrf_token' => $token],
|
||||
body: [
|
||||
'matchId' => self::MATCH_ID,
|
||||
'match' => ScenarioSerializer::matchToArray($match),
|
||||
'unitId' => 'alpha-1',
|
||||
'x' => 0,
|
||||
'y' => 1,
|
||||
],
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
self::assertSame(200, $response->status);
|
||||
|
||||
$body = json_decode($response->body, true);
|
||||
$moved = null;
|
||||
foreach ($body['match']['units'] as $u) {
|
||||
if ($u['id'] === 'alpha-1') {
|
||||
$moved = $u;
|
||||
}
|
||||
}
|
||||
self::assertNotNull($moved);
|
||||
self::assertSame(['x' => 0, 'y' => 1], $moved['position']);
|
||||
self::assertSame(0, $moved['actionsRemaining'], 'wading ends the turn');
|
||||
self::assertTrue($moved['wadedThisTurn'], 'wade flag is set after water entry');
|
||||
}
|
||||
|
||||
public function testEndTurnHappyPathSwitchesTheActiveTeamAndIncrementsTheRoundAfterBothSides(): void
|
||||
{
|
||||
$match = $this->freshMatch();
|
||||
|
||||
@@ -56,7 +56,6 @@ final class MatchStateTest extends TestCase
|
||||
*/
|
||||
public static function impassableTerrainProvider(): iterable
|
||||
{
|
||||
yield 'water' => [Terrain::Water];
|
||||
yield 'blocking' => [Terrain::Blocking];
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ final class ScenarioValidatorTest extends TestCase
|
||||
public function testItRejectsAnObjectiveOnImpassableTerrain(): void
|
||||
{
|
||||
$scenario = $this->validScenario(
|
||||
terrain: ['4:4' => Terrain::Water],
|
||||
terrain: ['4:4' => Terrain::Blocking],
|
||||
objectivePosition: new Position(4, 4),
|
||||
);
|
||||
|
||||
@@ -81,7 +81,7 @@ final class ScenarioValidatorTest extends TestCase
|
||||
public function testItRejectsDeploymentPositionsOnImpassableTerrain(): void
|
||||
{
|
||||
$units = $this->unitSet();
|
||||
$battlefield = new Battlefield(8, 8, ['0:0' => Terrain::Water]);
|
||||
$battlefield = new Battlefield(8, 8, ['0:0' => Terrain::Blocking]);
|
||||
$scenario = new Scenario(
|
||||
id: 'demo',
|
||||
name: 'Demo',
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BattleForge\Tests\Unit\Domain;
|
||||
|
||||
use BattleForge\Domain\Terrain;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class TerrainTest extends TestCase
|
||||
{
|
||||
public function testOpenCostsOne(): void
|
||||
{
|
||||
self::assertSame(1, Terrain::Open->movementCost());
|
||||
self::assertSame(0, Terrain::Open->defenseBonus());
|
||||
}
|
||||
|
||||
public function testForestCostsTwoAndGivesOneDefense(): void
|
||||
{
|
||||
self::assertSame(2, Terrain::Forest->movementCost());
|
||||
self::assertSame(1, Terrain::Forest->defenseBonus());
|
||||
}
|
||||
|
||||
public function testRoughCostsTwoAndGivesNoDefense(): void
|
||||
{
|
||||
self::assertSame(2, Terrain::Rough->movementCost());
|
||||
self::assertSame(0, Terrain::Rough->defenseBonus());
|
||||
}
|
||||
|
||||
public function testWaterCostsThreeAndGivesNoDefense(): void
|
||||
{
|
||||
self::assertSame(3, Terrain::Water->movementCost());
|
||||
self::assertSame(0, Terrain::Water->defenseBonus());
|
||||
}
|
||||
|
||||
public function testBlockingIsImpassable(): void
|
||||
{
|
||||
self::assertNull(Terrain::Blocking->movementCost());
|
||||
}
|
||||
}
|
||||
@@ -229,12 +229,32 @@ final class UnitStateTest extends TestCase
|
||||
self::assertFalse($started->hasUsedAbility);
|
||||
}
|
||||
|
||||
public function testStartTurnResetsWadedFlag(): void
|
||||
{
|
||||
$unit = $this->unit(wadedThisTurn: true);
|
||||
$started = $unit->startTurn();
|
||||
self::assertFalse($started->wadedThisTurn);
|
||||
}
|
||||
|
||||
public function testWithWadedSetsAndClearsFlag(): void
|
||||
{
|
||||
$unit = $this->unit();
|
||||
self::assertFalse($unit->wadedThisTurn);
|
||||
|
||||
$wading = $unit->withWaded(true);
|
||||
self::assertTrue($wading->wadedThisTurn);
|
||||
|
||||
$dry = $wading->withWaded(false);
|
||||
self::assertFalse($dry->wadedThisTurn);
|
||||
}
|
||||
|
||||
private function unit(
|
||||
int $health = 10,
|
||||
int $actionsRemaining = 2,
|
||||
bool $hasAttacked = false,
|
||||
int $attackBonus = 0,
|
||||
bool $hasUsedAbility = false,
|
||||
bool $wadedThisTurn = false,
|
||||
): UnitState {
|
||||
return new UnitState(
|
||||
'unit-1',
|
||||
|
||||
Reference in New Issue
Block a user