Skip to content

Commit

Permalink
Fix bug where units out of supplies were destroyed despite being supp…
Browse files Browse the repository at this point in the history
…lied by a unit in fog at the beginning of a turn. (#41)

GitOrigin-RevId: 3d532fb94b9e61e95ce203ce30806061bf7ce8aa
  • Loading branch information
cpojer committed May 30, 2024
1 parent cb75645 commit f7530da
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 98 deletions.
7 changes: 5 additions & 2 deletions apollo/actions/applyEndTurnActionResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ export default function applyEndTurnActionResponse(
}

let teams = updatePlayers(map.teams, [nextPlayer, currentPlayer]);
const supplyVectors = new Set(supply);
const destroyedUnits = map
.subtractFuel(nextPlayer.id)
.units.filter((unit, vector) =>
shouldRemoveUnit(map, vector, unit, nextPlayer.id),
.units.filter(
(unit, vector) =>
!supplyVectors.has(vector) &&
shouldRemoveUnit(map, vector, unit, nextPlayer.id),
).size;

if (destroyedUnits > 0) {
Expand Down
2 changes: 1 addition & 1 deletion athena/lib/shouldRemoveUnit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export default function shouldRemoveUnit(
player: PlayerID,
) {
return (
map.matchesPlayer(unit, player) &&
!unit.hasFuel() &&
map.matchesPlayer(unit, player) &&
isFuelConsumingUnit(unit, map.getTileInfo(vector))
);
}
33 changes: 6 additions & 27 deletions hera/GameMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import dropLabels from '@deities/athena/lib/dropLabels.tsx';
import getAverageVector from '@deities/athena/lib/getAverageVector.tsx';
import getDecoratorsAtField from '@deities/athena/lib/getDecoratorsAtField.tsx';
import getFirstHumanPlayer from '@deities/athena/lib/getFirstHumanPlayer.tsx';
import getUnitsByPositions from '@deities/athena/lib/getUnitsByPositions.tsx';
import isPvP from '@deities/athena/lib/isPvP.tsx';
import updatePlayers from '@deities/athena/lib/updatePlayers.tsx';
import {
Expand All @@ -24,10 +23,7 @@ import {
} from '@deities/athena/map/Configuration.tsx';
import { PlayerID, toPlayerID } from '@deities/athena/map/Player.tsx';
import vec from '@deities/athena/map/vec.tsx';
import Vector, {
sortByVectorKey,
VectorLike,
} from '@deities/athena/map/Vector.tsx';
import Vector, { VectorLike } from '@deities/athena/map/Vector.tsx';
import type MapData from '@deities/athena/MapData.tsx';
import { RadiusItem } from '@deities/athena/Radius.tsx';
import { winConditionHasVectors } from '@deities/athena/WinConditions.tsx';
Expand Down Expand Up @@ -65,7 +61,6 @@ import Cursor from './Cursor.tsx';
import MapEditorExtraCursors from './editor/MapEditorMirrorCursors.tsx';
import { EditorState } from './editor/Types.tsx';
import addEndTurnAnimations from './lib/addEndTurnAnimations.tsx';
import animateSupply from './lib/animateSupply.tsx';
import isInView from './lib/isInView.tsx';
import maskClassName, { MaskPointerClassName } from './lib/maskClassName.tsx';
import sleep from './lib/sleep.tsx';
Expand Down Expand Up @@ -201,6 +196,7 @@ const getInitialState = (props: Props) => {
? new baseBehavior()
: new BaseBehavior();

const vision = getVision(map, currentViewer, spectatorCodes);
const newState = {
additionalRadius: null,
animationConfig:
Expand All @@ -220,7 +216,7 @@ const getInitialState = (props: Props) => {
inlineUI: getInlineUIState(map, tileSize, scale),
lastActionResponse: lastActionResponse || null,
lastActionTime: lastActionTime || undefined,
map: isEditor ? map : dropLabels(map),
map: isEditor ? map : vision.apply(dropLabels(map)),
mapName,
namedPositions: null,
navigationDirection: null,
Expand All @@ -244,7 +240,7 @@ const getInitialState = (props: Props) => {
tileSize,
timeout: timeout || null,
unitSize,
vision: getVision(map, currentViewer, spectatorCodes),
vision,
winConditionRadius: getWinConditionRadius(map, isEditor),
zIndex: getLayer(map.size.height + 1, 'top') + 10,
};
Expand Down Expand Up @@ -411,14 +407,14 @@ export default class GameMap extends Component<Props, State> {
type: 'EndTurn',
},
this.state,
null,
(state) => {
resolve();
return {
...state,
...resetBehavior(this.props.behavior),
};
},
[],
),
}),
);
Expand Down Expand Up @@ -920,29 +916,12 @@ export default class GameMap extends Component<Props, State> {
}
} else if (actionResponse.type === 'EndTurn') {
const { map } = this.state;
const { current, next, supply } = actionResponse;
const { current, next } = actionResponse;

// The turn was likely ended through a turn timeout.
if (map.getCurrentPlayer().id !== next.player) {
state = await this._processActionResponses([self]);
} else {
if (supply) {
state = await new Promise((resolve) => {
this._update({
lastActionResponse: actionResponse,
...animateSupply(
state,
sortByVectorKey(getUnitsByPositions(map, supply)),
(state) => {
resolve(state);
return null;
},
),
lastActionTime: dateNow(),
});
});
}

const currentPlayer = map.getPlayer(current.player);
const nextPlayer = map.getPlayer(next.player);
if (
Expand Down
4 changes: 2 additions & 2 deletions hera/action-response/processActionResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ async function processActionResponse(
actions,
actionResponse,
state,
actionResponse.supply || null,
(state) => {
// All updates are handled elsewhere in this case.
requestFrame(() => resolve(null));
Expand All @@ -261,7 +262,6 @@ async function processActionResponse(
map: isFakeEndTurn(actionResponse) ? state.map : newMap,
};
},
actionResponse.supply,
),
}));
break;
Expand Down Expand Up @@ -596,8 +596,8 @@ async function processActionResponse(
type: 'EndTurn',
},
state,
null,
resolveWithNull,
[],
),
}));
break;
Expand Down
61 changes: 42 additions & 19 deletions hera/behavior/endTurn/endTurnAction.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { EndTurnAction } from '@deities/apollo/action-mutators/ActionMutators.tsx';
import { EndTurnActionResponse } from '@deities/apollo/ActionResponse.tsx';
import applyActionResponse from '@deities/apollo/actions/applyActionResponse.tsx';
import getActionResponseVectors from '@deities/apollo/lib/getActionResponseVectors.tsx';
import { GameActionResponse } from '@deities/apollo/Types.tsx';
import dateNow from '@deities/hephaestus/dateNow.tsx';
import addEndTurnAnimations from '../../lib/addEndTurnAnimations.tsx';
import { Actions, State } from '../../Types.tsx';
import { resetBehavior } from '../Behavior.tsx';
import NullBehavior from '../NullBehavior.tsx';

const getEndTurnActionResponse = (
gameActionResponse: GameActionResponse,
): EndTurnActionResponse | null => {
const actionResponse = gameActionResponse.self?.actionResponse;
return actionResponse?.type === 'EndTurn' ? actionResponse : null;
};

export default async function endTurnAction(actions: Actions, state: State) {
const { action, processGameActionResponse, update } = actions;
const { map } = state;
Expand All @@ -24,25 +33,39 @@ export default async function endTurnAction(actions: Actions, state: State) {
map: nextMap,
});
await update({
...addEndTurnAnimations(actions, actionResponse, state, (state) => {
const newState = {
...state,
map: applyActionResponse(
nextMap.copy({
currentPlayer: current.player,
}),
state.vision,
actionResponse,
),
};
remoteAction.then(async (gameActionResponse) => {
const state = await processGameActionResponse(gameActionResponse);
if (state.lastActionResponse?.type !== 'GameEnd') {
await update(resetBehavior());
}
});
return newState;
}),
...addEndTurnAnimations(
actions,
actionResponse,
state,
remoteAction.then(
(gameActionResponse) =>
getEndTurnActionResponse(gameActionResponse)?.supply || null,
),
(state) => {
remoteAction.then(async (gameActionResponse) => {
const endTurnActionResponse =
getEndTurnActionResponse(gameActionResponse) || actionResponse;
await update({
...state,
map: applyActionResponse(
nextMap.copy({
currentPlayer: current.player,
}),
state.vision,
endTurnActionResponse,
),
});

const newState =
await processGameActionResponse(gameActionResponse);
if (newState.lastActionResponse?.type !== 'GameEnd') {
await update(resetBehavior());
}
});

return state;
},
),
...resetBehavior(),
behavior: new NullBehavior(),
lastActionResponse: actionResponse,
Expand Down
97 changes: 54 additions & 43 deletions hera/lib/addEndTurnAnimations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ export default function addEndTurnAnimations(
actions: Actions,
actionResponse: EndTurnActionResponse,
state: State,
maybeExtraPositions:
| Promise<ReadonlyArray<Vector> | null>
| ReadonlyArray<Vector>
| null,
onComplete: StateToStateLike,
extraPositions?: ReadonlyArray<Vector>,
) {
const { requestFrame, update } = actions;
const {
current: { player: currentPlayer },
next: { player: nextPlayer },
Expand All @@ -49,52 +53,59 @@ export default function addEndTurnAnimations(
color: nextPlayer,
length: 'short',
onComplete: (state) => {
const { map, vision } = state;
const newMap = map.subtractFuel(nextPlayer);
const [unitsToHeal, unitsToRefill] = isFake
? [emptyUnitMap, emptyUnitMap]
: partitionUnitsToHeal(getUnitsToHealOnBuildings(map, nextPlayer));
requestFrame(async () => {
const { map, vision } = state;
const newMap = map.subtractFuel(nextPlayer);
const [unitsToHeal, unitsToRefill] = isFake
? [emptyUnitMap, emptyUnitMap]
: partitionUnitsToHeal(getUnitsToHealOnBuildings(map, nextPlayer));

const unitsToSupply = new Map([
...(isFake
? emptyUnitMap
: getAllUnitsToRefill(
newMap,
vision,
state.map.getPlayer(nextPlayer),
)),
...(extraPositions ? getUnitsByPositions(map, extraPositions) : []),
...unitsToRefill,
]);
const extraPositions = await maybeExtraPositions;
const unitsToSupply = new Map([
...(isFake
? emptyUnitMap
: getAllUnitsToRefill(
newMap,
vision,
state.map.getPlayer(nextPlayer),
)),
...(extraPositions ? getUnitsByPositions(map, extraPositions) : []),
...unitsToRefill,
]);

const explodeUnitsWithoutFuel = (state: State) =>
explodeUnits(
actions,
state,
// Identify units that are out of fuel without applying the fuel adjustment, which is
// applied later in `applyEndTurnActionResponse`.
sortVectors([
...newMap.units
.filter(
(unit, vector) =>
shouldRemoveUnit(newMap, vector, unit, nextPlayer) &&
!unitsToSupply.has(vector) &&
!unitsToHeal.has(vector),
)
.keys(),
]),
onComplete,
const explodeUnitsWithoutFuel = (state: State) =>
explodeUnits(
actions,
state,
// Identify units that are out of fuel without applying the fuel adjustment, which is
// applied later in `applyEndTurnActionResponse`.
sortVectors([
...newMap.units
.filter(
(unit, vector) =>
!unitsToSupply.has(vector) &&
!unitsToHeal.has(vector) &&
shouldRemoveUnit(newMap, vector, unit, nextPlayer),
)
.keys(),
]),
onComplete,
);

await update(
animateHeal(state, sortByVectorKey(unitsToHeal), (state) =>
unitsToSupply.size
? animateSupply(
state,
sortByVectorKey(unitsToSupply),
explodeUnitsWithoutFuel,
)
: explodeUnitsWithoutFuel(state),
),
);
});

return animateHeal(state, sortByVectorKey(unitsToHeal), (state) =>
unitsToSupply.size
? animateSupply(
state,
sortByVectorKey(unitsToSupply),
explodeUnitsWithoutFuel,
)
: explodeUnitsWithoutFuel(state),
);
return state;
},
player: nextPlayer,
sound: 'UI/Start',
Expand Down
1 change: 0 additions & 1 deletion tests/__tests__/Fog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ test('units that will be supplied by a hidden adjacent supply unit are not destr
EndTurn { current: { funds: 500, player: 2 }, next: { funds: 500, player: 1 }, round: 2, rotatePlayers: false, supply: null, miss: false }"
`);

expect(gameActionResponse[0]![2]).toBeUndefined();
gameActionResponse[1]!.map(([, , units]) => expect(units).toBeUndefined());
});

Expand Down
4 changes: 2 additions & 2 deletions tests/__tests__/Statistics.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,12 +332,12 @@ test('tracks statistics for players of the same team in fog', async () => {
[EndTurnAction(), AttackUnitAction(vecC, vecB)],
);

const { others, self } = decodeGameActionResponse(encodedGameActionResponse);
const { others } = decodeGameActionResponse(encodedGameActionResponse);

let fogMap = applyActionResponse(
vision.apply(initialMap),
vision,
self!.actionResponse,
others![0].actionResponse,
);
for (const { actionResponse, buildings, units } of others!) {
fogMap = updateVisibleEntities(
Expand Down
2 changes: 1 addition & 1 deletion tests/executeGameActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default function executeGameActions(
map.createVisionObject(map.getCurrentPlayer()),
gameState,
null,
gameState[0][0],
null,
),
newEffects,
];
Expand Down

0 comments on commit f7530da

Please sign in to comment.