Skip to content

Commit

Permalink
Optimizations to spawn locations and unit counts.
Browse files Browse the repository at this point in the history
GitOrigin-RevId: cc665f4e0eac662dff639561609d05209c069942
  • Loading branch information
cpojer committed Nov 1, 2024
1 parent 8a00041 commit 20d7865
Show file tree
Hide file tree
Showing 18 changed files with 212 additions and 77 deletions.
16 changes: 14 additions & 2 deletions apollo/Action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import canDeploy from '@deities/athena/lib/canDeploy.tsx';
import canLoad from '@deities/athena/lib/canLoad.tsx';
import canPlaceLightning from '@deities/athena/lib/canPlaceLightning.tsx';
import canPlaceRailTrack from '@deities/athena/lib/canPlaceRailTrack.tsx';
import couldSpawnBuilding from '@deities/athena/lib/couldSpawnBuilding.tsx';
import followMovementPath from '@deities/athena/lib/followMovementPath.tsx';
import getAttackStatusEffect from '@deities/athena/lib/getAttackStatusEffect.tsx';
import getChargeValue from '@deities/athena/lib/getChargeValue.tsx';
Expand Down Expand Up @@ -174,6 +175,7 @@ type SabotageAction = Readonly<{
}>;

export type SpawnEffectAction = Readonly<{
buildings?: ImmutableMap<Vector, Building>;
player?: DynamicPlayerID;
teams?: Teams;
type: 'SpawnEffect';
Expand Down Expand Up @@ -857,10 +859,11 @@ function sabotage(map: MapData, { from, to }: SabotageAction) {

function spawnEffect(
map: MapData,
{ player: dynamicPlayer, teams, units }: SpawnEffectAction,
{ buildings, player: dynamicPlayer, teams, units }: SpawnEffectAction,
) {
const player =
dynamicPlayer != null ? resolveDynamicPlayerID(map, dynamicPlayer) : null;
let newBuildings = ImmutableMap<Vector, Building>();
let newUnits = ImmutableMap<Vector, Unit>();

for (const [vector, unit] of units) {
Expand All @@ -885,9 +888,18 @@ function spawnEffect(
}
}

if (buildings) {
for (const [vector, building] of buildings) {
if (couldSpawnBuilding(map, vector, building.info, building.player)) {
newBuildings = newBuildings.set(vector, building);
}
}
}

teams = maybeCreatePlayers(map, teams, newUnits);
return newUnits.size || teams
return newUnits.size || newBuildings.size || teams
? ({
buildings: newBuildings,
teams,
type: 'Spawn',
units: assignDeterministicUnitNames(map, newUnits),
Expand Down
6 changes: 3 additions & 3 deletions apollo/ActionMap.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
[5, ["type", "from", "id", "to", "unit", "free", "skipBehaviorRotation"]]
],
["DropUnit", [6, ["type", "from", "index", "to"]]],
["CreateBuilding", [7, ["type", "from", "id", "building"]]],
["CreateBuilding", [7, ["type", "from", "id", "building", "free"]]],
["Fold", [8, ["type", "from"]]],
["Unfold", [9, ["type", "from"]]],
["CompleteUnit", [10, ["type", "from"]]],
Expand Down Expand Up @@ -152,7 +152,7 @@
["HiddenFundAdjustment", [23, ["type", "funds"]]],
["ToggleLightning", [24, ["type", "from", "to", "player"]]],
["CompleteBuilding", [25, ["type", "from"]]],
["Spawn", [26, ["type", "units", "teams"]]],
["Spawn", [26, ["type", "units", "teams", "buildings"]]],
["Heal", [27, ["type", "from", "to"]]],
["MoveUnit", [28, ["type", "from"]]],
["Sabotage", [29, ["type", "from", "to"]]],
Expand All @@ -161,7 +161,7 @@
[30, ["type", "message", "player", "unitId", "variant"]]
],
["CreateTracks", [31, ["type", "from"]]],
["SpawnEffect", [32, ["type", "units", "player", "teams"]]],
["SpawnEffect", [32, ["type", "units", "player", "teams", "buildings"]]],
[
"CharacterMessageEffect",
[33, ["type", "message", "player", "unitId", "variant"]]
Expand Down
2 changes: 2 additions & 0 deletions apollo/ActionResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export type DropUnitActionResponse = Readonly<{

export type CreateBuildingActionResponse = Readonly<{
building: Building;
free?: boolean;
from: Vector;
type: 'CreateBuilding';
}>;
Expand Down Expand Up @@ -141,6 +142,7 @@ export type ToggleLightningActionResponse = Readonly<{
}>;

export type SpawnActionResponse = Readonly<{
buildings?: ImmutableMap<Vector, Building>;
teams?: Teams;
type: 'Spawn';
units: ImmutableMap<Vector, Unit>;
Expand Down
10 changes: 5 additions & 5 deletions apollo/__tests__/Action.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ test('creating buildings', () => {
expect(
formatActionResponse(responseA, { colors: false }),
).toMatchInlineSnapshot(
`"CreateBuilding (1,1) { building: Factory { id: 3, health: 100, player: 1, completed: true } }"`,
`"CreateBuilding (1,1) { building: Factory { id: 3, health: 100, player: 1, completed: true }, free: null }"`,
);

expect(execute(map, vision, CreateBuildingAction(vecA, House.id))).toBe(null);
Expand All @@ -167,7 +167,7 @@ test('creating buildings', () => {
expect(
formatActionResponse(responseB, { colors: false }),
).toMatchInlineSnapshot(
`"CreateBuilding (1,1) { building: House { id: 2, health: 100, player: 1, completed: true } }"`,
`"CreateBuilding (1,1) { building: House { id: 2, health: 100, player: 1, completed: true }, free: null }"`,
);

expect(
Expand Down Expand Up @@ -205,7 +205,7 @@ test('Radar Stations are only available if Lightning can be placed', () => {
expect(
formatActionResponse(responseC, { colors: false }),
).toMatchInlineSnapshot(
`"CreateBuilding (1,1) { building: Radar Station { id: 10, health: 100, player: 1, completed: true } }"`,
`"CreateBuilding (1,1) { building: Radar Station { id: 10, health: 100, player: 1, completed: true }, free: null }"`,
);

expect(
Expand All @@ -226,7 +226,7 @@ test('Radar Stations are only available if Lightning can be placed', () => {
expect(
formatActionResponse(responseD, { colors: false }),
).toMatchInlineSnapshot(
`"CreateBuilding (1,1) { building: Radar Station { id: 10, health: 100, player: 1, completed: true } }"`,
`"CreateBuilding (1,1) { building: Radar Station { id: 10, health: 100, player: 1, completed: true }, free: null }"`,
);

const [responseE] = execute(
Expand All @@ -240,6 +240,6 @@ test('Radar Stations are only available if Lightning can be placed', () => {
expect(
formatActionResponse(responseE, { colors: false }),
).toMatchInlineSnapshot(
`"CreateBuilding (1,1) { building: Radar Station { id: 10, health: 100, player: 1, completed: true } }"`,
`"CreateBuilding (1,1) { building: Radar Station { id: 10, health: 100, player: 1, completed: true }, free: null }"`,
);
});
16 changes: 10 additions & 6 deletions apollo/actions/applyActionResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import getUnitsToRefill from '@deities/athena/lib/getUnitsToRefill.tsx';
import maybeConvertPlayer from '@deities/athena/lib/maybeConvertPlayer.tsx';
import mergeTeams from '@deities/athena/lib/mergeTeams.tsx';
import refillUnits from '@deities/athena/lib/refillUnits.tsx';
import spawnBuildings from '@deities/athena/lib/spawnBuildings.tsx';
import updatePlayer from '@deities/athena/lib/updatePlayer.tsx';
import updatePlayers from '@deities/athena/lib/updatePlayers.tsx';
import verifyTiles from '@deities/athena/lib/verifyTiles.tsx';
Expand Down Expand Up @@ -353,14 +354,14 @@ export default function applyActionResponse(
});
}
case 'CreateBuilding': {
const { building, from } = actionResponse;
const { building, free, from } = actionResponse;
const player = map.getPlayer(building);
const teams = map.isNeutral(building)
? map.teams
: updatePlayer(
map.teams,
player
.modifyFunds(-building.info.getCostFor(player))
.modifyFunds(free ? 0 : -building.info.getCostFor(player))
.modifyStatistic('createdBuildings', 1),
);
return map.copy({
Expand Down Expand Up @@ -493,10 +494,13 @@ export default function applyActionResponse(
});
}
case 'Spawn': {
const { teams, units } = actionResponse;
const newMap = mergeTeams(map, teams).copy({
units: map.units.merge(units),
});
const { buildings, teams, units } = actionResponse;
const newMap = spawnBuildings(
mergeTeams(map, teams).copy({
units: map.units.merge(units),
}),
buildings,
);
return newMap.copy({
active: getActivePlayers(newMap, map.active),
});
Expand Down
2 changes: 1 addition & 1 deletion athena/lib/calculateEmptyClusters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function calculateEmptyClusters(map: MapData) {
[
...map.units.keys(),
...map.buildings.filter((building) => building.info.isHQ()).keys(),
].flatMap((vector) => vector.expandWithDiagonals()),
].flatMap((vector) => vector.expandStar()),
);
return calculateClusters(
map.size,
Expand Down
27 changes: 27 additions & 0 deletions athena/lib/couldSpawnBuilding.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { BuildingInfo } from '../info/Building.tsx';
import { ConstructionSite, Plain } from '../info/Tile.tsx';
import { PlayerID } from '../map/Player.tsx';
import Vector from '../map/Vector.tsx';
import MapData from '../MapData.tsx';
import writeTile from '../mutation/writeTile.tsx';
import canBuild from './canBuild.tsx';

export default function couldSpawnBuilding(
map: MapData,
vector: Vector,
building: BuildingInfo,
player: PlayerID,
) {
if (map.getTileInfo(vector) === Plain) {
const tiles = map.map.slice();
const modifiers = map.modifiers.slice();
writeTile(tiles, modifiers, map.getTileIndex(vector), ConstructionSite);
map = map.copy({
map: tiles,
});
}

return (
!map.buildings.has(vector) && canBuild(map, building, player, vector, true)
);
}
34 changes: 34 additions & 0 deletions athena/lib/spawnBuildings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import getFirstOrThrow from '@deities/hephaestus/getFirstOrThrow.tsx';
import ImmutableMap from '@nkzw/immutable-map';
import Building from '../map/Building.tsx';
import Vector from '../map/Vector.tsx';
import MapData from '../MapData.tsx';
import writeTile from '../mutation/writeTile.tsx';
import withModifiers from './withModifiers.tsx';

export default function spawnBuildings(
map: MapData,
buildings: ImmutableMap<Vector, Building> | undefined,
) {
if (buildings?.size) {
const tileMap = map.map.slice();
const modifiers = map.modifiers.slice();
for (const [vector, building] of buildings) {
const { editorPlaceOn, placeOn } = building.info.configuration;
const tiles = new Set([...(placeOn || []), ...editorPlaceOn]);
if (!tiles.has(map.getTileInfo(vector))) {
writeTile(
tileMap,
modifiers,
map.getTileIndex(vector),
getFirstOrThrow(tiles),
);
}
}
return withModifiers(
map.copy({ buildings: map.buildings.merge(buildings), map: tileMap }),
);
}

return map;
}
28 changes: 21 additions & 7 deletions hera/action-response/processActionResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ import {
} from '../behavior/attack/hiddenAttackActions.tsx';
import buySkillAction from '../behavior/buySkill/buySkillAction.tsx';
import captureAction from '../behavior/capture/captureAction.tsx';
import { addCreateBuildingAnimation } from '../behavior/createBuilding/createBuildingAction.tsx';
import {
addCreateBuildingAnimation,
animateCreateBuilding,
} from '../behavior/createBuilding/createBuildingAction.tsx';
import createTracksAction from '../behavior/createTracks/createTracksAction.tsx';
import createUnitAction from '../behavior/createUnit/createUnitAction.tsx';
import dropUnitAction from '../behavior/drop/dropUnitAction.tsx';
Expand Down Expand Up @@ -393,20 +396,31 @@ async function processActionResponse(
break;
}
case 'Spawn': {
const { buildings, teams, units } = actionResponse;
await update((state) =>
spawn(
actions,
state,
actionResponse.units.toArray(),
actionResponse.teams,
actionResponse.units.size >= 5 ? 'fast' : 'slow',
units.toArray(),
teams,
units.size >= 5 ? 'fast' : 'slow',
(state) => {
requestFrame(() =>
requestFrame(async () => {
if (buildings) {
for (const [from, building] of buildings) {
state = await animateCreateBuilding(actions, state, {
building,
free: true,
from,
type: 'CreateBuilding',
});
}
}
resolve({
...state,
map: newMap,
}),
);
});
});
return null;
},
),
Expand Down
45 changes: 44 additions & 1 deletion hera/behavior/createBuilding/createBuildingAction.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { CreateBuildingAction } from '@deities/apollo/action-mutators/ActionMutators.tsx';
import { ActionResponse } from '@deities/apollo/ActionResponse.tsx';
import {
ActionResponse,
CreateBuildingActionResponse,
} from '@deities/apollo/ActionResponse.tsx';
import applyActionResponse from '@deities/apollo/actions/applyActionResponse.tsx';
import { GameActionResponse } from '@deities/apollo/Types.tsx';
import { BuildingInfo } from '@deities/athena/info/Building.tsx';
Expand Down Expand Up @@ -73,3 +76,43 @@ export function addCreateBuildingAnimation(
...resetBehavior(NullBehavior),
};
}

export function animateCreateBuilding(
{ requestFrame, update }: Actions,
state: State,
actionResponse: CreateBuildingActionResponse,
): Promise<State> {
const { building, from } = actionResponse;
return new Promise((resolve) =>
update({
animations: state.animations.set(from, {
onComplete: (state) => {
requestFrame(async () => resolve(await update(null)));

return {
...state,
map: state.map.copy({
buildings: state.map.buildings.set(from, building),
}),
position: from,
};
},
onCreate: (state) => {
const map = applyActionResponse(
state.map,
state.vision,
actionResponse,
);
return {
map: map.copy({
buildings: map.buildings.set(from, building.recover()),
}),
};
},
type: 'createBuilding',
variant: building.player,
}),
...resetBehavior(NullBehavior),
}),
);
}
1 change: 0 additions & 1 deletion hera/ui/Notice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export default function Notice(
},
opacity: {
duration: 0.15,
ease: 'ease',
},
}}
>
Expand Down
4 changes: 2 additions & 2 deletions tests/__tests__/AIBehavior.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ test('AI behavior from buildings carries over in a round-robin fashion', async (
expect(snapshotGameState(secondGameState)).toMatchInlineSnapshot(`
"AttackUnit (2,1 → 3,3) { hasCounterAttack: false, playerA: 2, playerB: 1, unitA: DryUnit { health: 100, ammo: [ [ 1, 3 ] ] }, unitB: DryUnit { health: 14, ammo: [ [ 1, 7 ] ] }, chargeA: 106, chargeB: 322 }
Move (2,3 → 3,1) { fuel: 37, completed: null, path: [2,2 → 3,2 → 3,1] }
CreateBuilding (3,1) { building: House { id: 2, health: 100, player: 2, completed: true } }
CreateBuilding (3,1) { building: House { id: 2, health: 100, player: 2, completed: true }, free: null }
CreateUnit (1,3 → 2,3) { unit: Pioneer { id: 1, health: 100, player: 2, fuel: 40, moved: true, name: 'Sam', completed: true, behavior: 1 }, free: false, skipBehaviorRotation: false }
EndTurn { current: { funds: 50, player: 2 }, next: { funds: 0, player: 1 }, round: 3, rotatePlayers: null, supply: null, miss: null }"
`);
Expand Down Expand Up @@ -820,7 +820,7 @@ test('AI will prefer funds generating buildings over factories if it has no inco

expect(snapshotGameState(gameStateA)).toMatchInlineSnapshot(`
"Move (1,2 → 3,1) { fuel: 37, completed: null, path: [2,2 → 3,2 → 3,1] }
CreateBuilding (3,1) { building: House { id: 2, health: 100, player: 2, completed: true } }
CreateBuilding (3,1) { building: House { id: 2, health: 100, player: 2, completed: true }, free: null }
CompleteUnit (2,2)
EndTurn { current: { funds: 100, player: 2 }, next: { funds: 200, player: 1 }, round: 2, rotatePlayers: null, supply: null, miss: null }"
`);
Expand Down
Loading

0 comments on commit 20d7865

Please sign in to comment.