diff --git a/apollo/Action.tsx b/apollo/Action.tsx index 6cd6139c..3f9b2676 100644 --- a/apollo/Action.tsx +++ b/apollo/Action.tsx @@ -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'; @@ -174,6 +175,7 @@ type SabotageAction = Readonly<{ }>; export type SpawnEffectAction = Readonly<{ + buildings?: ImmutableMap; player?: DynamicPlayerID; teams?: Teams; type: 'SpawnEffect'; @@ -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(); let newUnits = ImmutableMap(); for (const [vector, unit] of units) { @@ -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), diff --git a/apollo/ActionMap.json b/apollo/ActionMap.json index 12e19c22..299e044b 100644 --- a/apollo/ActionMap.json +++ b/apollo/ActionMap.json @@ -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"]]], @@ -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"]]], @@ -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"]] diff --git a/apollo/ActionResponse.tsx b/apollo/ActionResponse.tsx index a913ac5f..06ecf921 100644 --- a/apollo/ActionResponse.tsx +++ b/apollo/ActionResponse.tsx @@ -80,6 +80,7 @@ export type DropUnitActionResponse = Readonly<{ export type CreateBuildingActionResponse = Readonly<{ building: Building; + free?: boolean; from: Vector; type: 'CreateBuilding'; }>; @@ -141,6 +142,7 @@ export type ToggleLightningActionResponse = Readonly<{ }>; export type SpawnActionResponse = Readonly<{ + buildings?: ImmutableMap; teams?: Teams; type: 'Spawn'; units: ImmutableMap; diff --git a/apollo/__tests__/Action.test.tsx b/apollo/__tests__/Action.test.tsx index 3a988cbc..9e6c1c57 100644 --- a/apollo/__tests__/Action.test.tsx +++ b/apollo/__tests__/Action.test.tsx @@ -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); @@ -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( @@ -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( @@ -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( @@ -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 }"`, ); }); diff --git a/apollo/actions/applyActionResponse.tsx b/apollo/actions/applyActionResponse.tsx index 181766d7..e7ca6c0c 100644 --- a/apollo/actions/applyActionResponse.tsx +++ b/apollo/actions/applyActionResponse.tsx @@ -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'; @@ -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({ @@ -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), }); diff --git a/athena/lib/calculateEmptyClusters.tsx b/athena/lib/calculateEmptyClusters.tsx index d3cee24c..46aa557a 100644 --- a/athena/lib/calculateEmptyClusters.tsx +++ b/athena/lib/calculateEmptyClusters.tsx @@ -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, diff --git a/athena/lib/couldSpawnBuilding.tsx b/athena/lib/couldSpawnBuilding.tsx new file mode 100644 index 00000000..76b19a9f --- /dev/null +++ b/athena/lib/couldSpawnBuilding.tsx @@ -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) + ); +} diff --git a/athena/lib/spawnBuildings.tsx b/athena/lib/spawnBuildings.tsx new file mode 100644 index 00000000..fefb7f7c --- /dev/null +++ b/athena/lib/spawnBuildings.tsx @@ -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 | 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; +} diff --git a/hera/action-response/processActionResponse.tsx b/hera/action-response/processActionResponse.tsx index b31aeb14..3c911a4a 100644 --- a/hera/action-response/processActionResponse.tsx +++ b/hera/action-response/processActionResponse.tsx @@ -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'; @@ -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; }, ), diff --git a/hera/behavior/createBuilding/createBuildingAction.tsx b/hera/behavior/createBuilding/createBuildingAction.tsx index 6cd431f9..451b00ae 100644 --- a/hera/behavior/createBuilding/createBuildingAction.tsx +++ b/hera/behavior/createBuilding/createBuildingAction.tsx @@ -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'; @@ -73,3 +76,43 @@ export function addCreateBuildingAnimation( ...resetBehavior(NullBehavior), }; } + +export function animateCreateBuilding( + { requestFrame, update }: Actions, + state: State, + actionResponse: CreateBuildingActionResponse, +): Promise { + 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), + }), + ); +} diff --git a/hera/ui/Notice.tsx b/hera/ui/Notice.tsx index eef3fc0d..6e4398c1 100644 --- a/hera/ui/Notice.tsx +++ b/hera/ui/Notice.tsx @@ -53,7 +53,6 @@ export default function Notice( }, opacity: { duration: 0.15, - ease: 'ease', }, }} > diff --git a/tests/__tests__/AIBehavior.test.tsx b/tests/__tests__/AIBehavior.test.tsx index 78e32245..667fd07b 100644 --- a/tests/__tests__/AIBehavior.test.tsx +++ b/tests/__tests__/AIBehavior.test.tsx @@ -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 }" `); @@ -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 }" `); diff --git a/tests/__tests__/CustomObjectives.test.tsx b/tests/__tests__/CustomObjectives.test.tsx index 52c17a55..821a1e6c 100644 --- a/tests/__tests__/CustomObjectives.test.tsx +++ b/tests/__tests__/CustomObjectives.test.tsx @@ -313,7 +313,7 @@ test('capture amount win criteria also works when creating buildings', async () .toMatchInlineSnapshot(` "Capture (1,1) { building: House { id: 2, health: 100, player: 1 }, player: 2 } Capture (2,2) { building: House { id: 2, health: 100, player: 1 }, player: 2 } - CreateBuilding (3,1) { building: Factory { id: 3, health: 100, player: 1, completed: true } } + CreateBuilding (3,1) { building: Factory { id: 3, health: 100, player: 1, completed: true }, free: false } GameEnd { objective: { amount: 3, bonus: undefined, completed: Set(0) {}, hidden: false, optional: false, players: [], reward: null, type: 2 }, objectiveId: 0, toPlayer: 1, chaosStars: null }" `); @@ -334,7 +334,7 @@ test('capture amount win criteria also works when creating buildings', async () .toMatchInlineSnapshot(` "Capture (1,1) { building: House { id: 2, health: 100, player: 1 }, player: 2 } Capture (2,2) { building: House { id: 2, health: 100, player: 1 }, player: 2 } - CreateBuilding (3,1) { building: Factory { id: 3, health: 100, player: 1, completed: true } } + CreateBuilding (3,1) { building: Factory { id: 3, health: 100, player: 1, completed: true }, free: false } OptionalObjective { objective: { amount: 3, bonus: undefined, completed: Set(1) { 1 }, hidden: false, optional: true, players: [], reward: null, type: 2 }, objectiveId: 0, toPlayer: 1 }" `); @@ -3312,7 +3312,7 @@ test('multiple optional objectives have their effects applied correctly', async .toMatchInlineSnapshot(` "Capture (1,2) { building: House { id: 2, health: 100, player: 1, label: 4 }, player: 2 } OptionalObjective { objective: { bonus: undefined, completed: Set(1) { 1 }, hidden: false, label: [ 4 ], optional: true, players: [], reward: null, type: 1 }, objectiveId: 1, toPlayer: 1 } - Spawn { units: [1,3 → Flamethrower { id: 15, health: 100, player: 0, fuel: 30, ammo: [ [ 1, 4 ] ], name: 'Uli' }], teams: null } + Spawn { units: [1,3 → Flamethrower { id: 15, health: 100, player: 0, fuel: 30, ammo: [ [ 1, 4 ] ], name: 'Uli' }], teams: null, buildings: [] } OptionalObjective { objective: { amount: 1, bonus: undefined, completed: Set(1) { 1 }, hidden: false, optional: true, players: [], reward: null, type: 2 }, objectiveId: 2, toPlayer: 1 }" `); diff --git a/tests/__tests__/Effects.test.tsx b/tests/__tests__/Effects.test.tsx index 87ec0a05..47d820eb 100644 --- a/tests/__tests__/Effects.test.tsx +++ b/tests/__tests__/Effects.test.tsx @@ -301,7 +301,7 @@ test('spawns an additional unit', async () => { expect(snapshotEncodedActionResponse(gameActionResponse)) .toMatchInlineSnapshot(` "EndTurn { current: { funds: 10000, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } - Spawn { units: [3,3 → Flamethrower { id: 15, health: 100, player: 2, fuel: 30, ammo: [ [ 1, 4 ] ], name: 'Yuki' }], teams: null } + Spawn { units: [3,3 → Flamethrower { id: 15, health: 100, player: 2, fuel: 30, ammo: [ [ 1, 4 ] ], name: 'Yuki' }], teams: null, buildings: [] } Move (3,3 → 2,1) { fuel: 27, completed: false, path: [3,2 → 2,2 → 2,1] } AttackUnit (2,1 → 1,1) { hasCounterAttack: false, playerA: 2, playerB: 1, unitA: DryUnit { health: 100, ammo: [ [ 1, 3 ] ] }, unitB: null, chargeA: 33, chargeB: 100 } CompleteUnit (1,3) @@ -348,7 +348,7 @@ test('spawns a neutral unit', async () => { expect(snapshotEncodedActionResponse(gameActionResponse)) .toMatchInlineSnapshot(` "EndTurn { current: { funds: 10000, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } - Spawn { units: [3,3 → Flamethrower { id: 15, health: 100, player: 0, fuel: 30, ammo: [ [ 1, 4 ] ], name: 'Casey' }], teams: null } + Spawn { units: [3,3 → Flamethrower { id: 15, health: 100, player: 0, fuel: 30, ammo: [ [ 1, 4 ] ], name: 'Casey' }], teams: null, buildings: [] } Move (1,3 → 2,3) { fuel: 39, completed: false, path: [2,3] } Rescue (2,3 → 3,3) { player: 2, name: null } EndTurn { current: { funds: 500, player: 2 }, next: { funds: 10000, player: 1 }, round: 2, rotatePlayers: false, supply: null, miss: false }" @@ -660,7 +660,7 @@ test('a unit spawns instead of ending the game', async () => { .toMatchInlineSnapshot(` "Move (1,1 → 2,3) { fuel: 27, completed: false, path: [2,1 → 2,2 → 2,3] } AttackUnit (2,3 → 3,3) { hasCounterAttack: false, playerA: 1, playerB: 2, unitA: DryUnit { health: 100, ammo: [ [ 1, 6 ] ] }, unitB: null, chargeA: 0, chargeB: 1 } - Spawn { units: [1,1 → Flamethrower { id: 15, health: 100, player: 2, fuel: 30, ammo: [ [ 1, 4 ] ], name: 'Yuki' }], teams: null }" + Spawn { units: [1,1 → Flamethrower { id: 15, health: 100, player: 2, fuel: 30, ammo: [ [ 1, 4 ] ], name: 'Yuki' }], teams: null, buildings: [] }" `); expect(newEffects?.get('AttackUnitGameOver')).toBeUndefined(); @@ -704,7 +704,7 @@ test('spawns a new unit when a player loses their last unit at the beginning of expect(snapshotEncodedActionResponse(gameActionResponse)) .toMatchInlineSnapshot(` "EndTurn { current: { funds: 10000, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } - Spawn { units: [2,3 → Flamethrower { id: 15, health: 100, player: 2, fuel: 30, ammo: [ [ 1, 4 ] ], name: 'Yuki' }], teams: null } + Spawn { units: [2,3 → Flamethrower { id: 15, health: 100, player: 2, fuel: 30, ammo: [ [ 1, 4 ] ], name: 'Yuki' }], teams: null, buildings: [] } Move (2,3 → 2,1) { fuel: 28, completed: false, path: [2,2 → 2,1] } AttackUnit (2,1 → 1,1) { hasCounterAttack: true, playerA: 2, playerB: 1, unitA: DryUnit { health: 67, ammo: [ [ 1, 3 ] ] }, unitB: DryUnit { health: 67, ammo: [ [ 1, 6 ] ] }, chargeA: 172, chargeB: 123 } CreateUnit (1,3 → 1,2) { unit: Rocket Launcher { id: 3, health: 100, player: 2, fuel: 40, ammo: [ [ 1, 4 ] ], moved: true, name: 'Davide', completed: true }, free: false, skipBehaviorRotation: false } @@ -794,9 +794,9 @@ test('triggers effects for actions generated by effects', async () => { expect(snapshotEncodedActionResponse(gameActionResponse)) .toMatchInlineSnapshot(` - "Move (1,1 → 3,1) { fuel: 38, completed: false, path: [2,1 → 3,1] } - OptionalObjective { objective: { amount: 1, bonus: undefined, completed: Set(1) { 1 }, hidden: false, label: [], optional: true, players: [ 1 ], reward: null, type: 6, vectors: [ '3,1' ] }, objectiveId: 1, toPlayer: 1 } - Spawn { units: [3,3 → Pioneer { id: 1, health: 100, player: 1, fuel: 40, name: 'Sam' }], teams: null } - GameEnd { objective: { amount: 1, bonus: undefined, completed: Set(0) {}, hidden: false, label: [], optional: false, players: [ 1 ], reward: null, type: 6, vectors: [ '3,3' ] }, objectiveId: 2, toPlayer: 1, chaosStars: null }" - `); + "Move (1,1 → 3,1) { fuel: 38, completed: false, path: [2,1 → 3,1] } + OptionalObjective { objective: { amount: 1, bonus: undefined, completed: Set(1) { 1 }, hidden: false, label: [], optional: true, players: [ 1 ], reward: null, type: 6, vectors: [ '3,1' ] }, objectiveId: 1, toPlayer: 1 } + Spawn { units: [3,3 → Pioneer { id: 1, health: 100, player: 1, fuel: 40, name: 'Sam' }], teams: null, buildings: [] } + GameEnd { objective: { amount: 1, bonus: undefined, completed: Set(0) {}, hidden: false, label: [], optional: false, players: [ 1 ], reward: null, type: 6, vectors: [ '3,3' ] }, objectiveId: 2, toPlayer: 1, chaosStars: null }" + `); }); diff --git a/tests/__tests__/EntityLabel.test.tsx b/tests/__tests__/EntityLabel.test.tsx index bfae8b9d..8d104d97 100644 --- a/tests/__tests__/EntityLabel.test.tsx +++ b/tests/__tests__/EntityLabel.test.tsx @@ -74,7 +74,7 @@ test('carries labels forward when creating buildings or units', async () => { ` "Move (1,1 → 2,1) { fuel: 39, completed: false, path: [2,1] } CreateUnit (1,1 → 1,1) { unit: Infantry { id: 2, health: 100, player: 1, fuel: 50, moved: true, name: 'Valentin', completed: true, label: 1 }, free: false, skipBehaviorRotation: false } - CreateBuilding (2,1) { building: Factory { id: 3, health: 100, player: 1, completed: true, label: 3 } } + CreateBuilding (2,1) { building: Factory { id: 3, health: 100, player: 1, completed: true, label: 3 }, free: false } EndTurn { current: { funds: 4550, player: 1 }, next: { funds: 5000, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } EndTurn { current: { funds: 5000, player: 2 }, next: { funds: 4550, player: 1 }, round: 2, rotatePlayers: false, supply: null, miss: false } CreateUnit (2,1 → 2,1) { unit: APU { id: 4, health: 100, player: 1, fuel: 40, ammo: [ [ 1, 6 ] ], moved: true, name: 'Nora', completed: true, label: 3 }, free: false, skipBehaviorRotation: false }" diff --git a/tests/__tests__/FormatActions.test.tsx b/tests/__tests__/FormatActions.test.tsx index 8509c32a..7dbe8dc4 100644 --- a/tests/__tests__/FormatActions.test.tsx +++ b/tests/__tests__/FormatActions.test.tsx @@ -117,32 +117,32 @@ test('create building and create unit actions', async () => { expect(snapshotGameState(gameState)).toMatchInlineSnapshot(` "EndTurn { current: { funds: 10000, player: 1 }, next: { funds: 10000, player: 2 }, round: 1, rotatePlayers: null, supply: null, miss: null } Move (5,4 → 5,2) { fuel: 38, completed: null, path: [5,3 → 5,2] } - CreateBuilding (5,2) { building: Barracks { id: 12, health: 100, player: 2, completed: true } } + CreateBuilding (5,2) { building: Barracks { id: 12, health: 100, player: 2, completed: true }, free: null } Move (4,5 → 2,5) { fuel: 38, completed: null, path: [3,5 → 2,5] } - CreateBuilding (2,5) { building: Barracks { id: 12, health: 100, player: 2, completed: true } } + CreateBuilding (2,5) { building: Barracks { id: 12, health: 100, player: 2, completed: true }, free: null } CreateUnit (5,5 → 5,4) { unit: Pioneer { id: 1, health: 100, player: 2, fuel: 40, moved: true, name: 'Sam', completed: true }, free: false, skipBehaviorRotation: false } EndTurn { current: { funds: 9600, player: 2 }, next: { funds: 10000, player: 1 }, round: 2, rotatePlayers: null, supply: null, miss: null } - CreateBuilding (2,1) { building: Barracks { id: 12, health: 100, player: 1, completed: true } } - CreateBuilding (1,2) { building: Barracks { id: 12, health: 100, player: 1, completed: true } } + CreateBuilding (2,1) { building: Barracks { id: 12, health: 100, player: 1, completed: true }, free: null } + CreateBuilding (1,2) { building: Barracks { id: 12, health: 100, player: 1, completed: true }, free: null } CreateUnit (1,1 → 2,1) { unit: Pioneer { id: 1, health: 100, player: 1, fuel: 40, moved: true, name: 'Sam', completed: true }, free: false, skipBehaviorRotation: false } EndTurn { current: { funds: 9600, player: 1 }, next: { funds: 9600, player: 2 }, round: 2, rotatePlayers: null, supply: null, miss: null } - CreateBuilding (5,4) { building: Barracks { id: 12, health: 100, player: 2, completed: true } } + CreateBuilding (5,4) { building: Barracks { id: 12, health: 100, player: 2, completed: true }, free: null } CreateUnit (5,2 → 5,1) { unit: Pioneer { id: 1, health: 100, player: 2, fuel: 40, moved: true, name: 'Sam', completed: true }, free: false, skipBehaviorRotation: false } CreateUnit (2,5 → 2,4) { unit: Pioneer { id: 1, health: 100, player: 2, fuel: 40, moved: true, name: 'Rick', completed: true }, free: false, skipBehaviorRotation: false } CreateUnit (5,5 → 5,4) { unit: Pioneer { id: 1, health: 100, player: 2, fuel: 40, moved: true, name: 'Idris', completed: true }, free: false, skipBehaviorRotation: false } EndTurn { current: { funds: 9150, player: 2 }, next: { funds: 9600, player: 1 }, round: 3, rotatePlayers: null, supply: null, miss: null } Move (2,1 → 4,1) { fuel: 38, completed: null, path: [3,1 → 4,1] } - CreateBuilding (4,1) { building: Barracks { id: 12, health: 100, player: 1, completed: true } } + CreateBuilding (4,1) { building: Barracks { id: 12, health: 100, player: 1, completed: true }, free: null } CreateUnit (2,1 → 2,2) { unit: Pioneer { id: 1, health: 100, player: 1, fuel: 40, moved: true, name: 'Sam', completed: true }, free: false, skipBehaviorRotation: false } CreateUnit (1,2 → 1,3) { unit: Pioneer { id: 1, health: 100, player: 1, fuel: 40, moved: true, name: 'Liam', completed: true }, free: false, skipBehaviorRotation: false } CreateUnit (1,1 → 2,1) { unit: Infantry { id: 2, health: 100, player: 1, fuel: 50, moved: true, name: 'Valentin', completed: true }, free: false, skipBehaviorRotation: false } EndTurn { current: { funds: 9050, player: 1 }, next: { funds: 9150, player: 2 }, round: 3, rotatePlayers: null, supply: null, miss: null } Move (5,4 → 4,5) { fuel: 38, completed: null, path: [4,4 → 4,5] } - CreateBuilding (4,5) { building: Factory { id: 3, health: 100, player: 2, completed: true } } + CreateBuilding (4,5) { building: Factory { id: 3, health: 100, player: 2, completed: true }, free: null } Move (5,1 → 4,1) { fuel: 39, completed: null, path: [4,1] } Capture (4,1) Move (2,4 → 1,4) { fuel: 39, completed: null, path: [1,4] } - CreateBuilding (1,4) { building: Factory { id: 3, health: 100, player: 2, completed: true } } + CreateBuilding (1,4) { building: Factory { id: 3, health: 100, player: 2, completed: true }, free: null } CreateUnit (5,2 → 4,2) { unit: Infantry { id: 2, health: 100, player: 2, fuel: 50, moved: true, name: 'Valentin', completed: true }, free: false, skipBehaviorRotation: false } EndTurn { current: { funds: 8450, player: 2 }, next: { funds: 9050, player: 1 }, round: 4, rotatePlayers: null, supply: null, miss: null } Move (2,1 → 3,2) { fuel: 48, completed: null, path: [3,1 → 3,2] } @@ -178,26 +178,26 @@ test('create building and create unit actions', async () => { expect(snapshotEncodedActionResponse(gameActionResponse)) .toMatchInlineSnapshot(` "EndTurn { current: { funds: 10000, player: 1 }, next: { funds: 10000, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } - CreateBuilding (5,2) { building: Barracks { id: 12, health: 100, player: 0 } } - CreateBuilding (2,5) { building: Barracks { id: 12, health: 100, player: 0 } } + CreateBuilding (5,2) { building: Barracks { id: 12, health: 100, player: 0 }, free: false } + CreateBuilding (2,5) { building: Barracks { id: 12, health: 100, player: 0 }, free: false } EndTurn { current: { funds: 9600, player: 2 }, next: { funds: 10000, player: 1 }, round: 2, rotatePlayers: false, supply: null, miss: false } - CreateBuilding (2,1) { building: Barracks { id: 12, health: 100, player: 1, completed: true } } - CreateBuilding (1,2) { building: Barracks { id: 12, health: 100, player: 1, completed: true } } + CreateBuilding (2,1) { building: Barracks { id: 12, health: 100, player: 1, completed: true }, free: false } + CreateBuilding (1,2) { building: Barracks { id: 12, health: 100, player: 1, completed: true }, free: false } CreateUnit (1,1 → 2,1) { unit: Pioneer { id: 1, health: 100, player: 1, fuel: 40, moved: true, name: 'Sam', completed: true }, free: false, skipBehaviorRotation: false } EndTurn { current: { funds: 9600, player: 1 }, next: { funds: 9600, player: 2 }, round: 2, rotatePlayers: false, supply: null, miss: false } - CreateBuilding (5,4) { building: Barracks { id: 12, health: 100, player: 0 } } + CreateBuilding (5,4) { building: Barracks { id: 12, health: 100, player: 0 }, free: false } EndTurn { current: { funds: 9150, player: 2 }, next: { funds: 9600, player: 1 }, round: 3, rotatePlayers: false, supply: null, miss: false } Move (2,1 → 4,1) { fuel: 38, completed: false, path: [3,1 → 4,1] } - CreateBuilding (4,1) { building: Barracks { id: 12, health: 100, player: 1, completed: true } } + CreateBuilding (4,1) { building: Barracks { id: 12, health: 100, player: 1, completed: true }, free: false } CreateUnit (2,1 → 2,2) { unit: Pioneer { id: 1, health: 100, player: 1, fuel: 40, moved: true, name: 'Sam', completed: true }, free: false, skipBehaviorRotation: false } CreateUnit (1,2 → 1,3) { unit: Pioneer { id: 1, health: 100, player: 1, fuel: 40, moved: true, name: 'Liam', completed: true }, free: false, skipBehaviorRotation: false } CreateUnit (1,1 → 2,1) { unit: Infantry { id: 2, health: 100, player: 1, fuel: 50, moved: true, name: 'Valentin', completed: true }, free: false, skipBehaviorRotation: false } EndTurn { current: { funds: 9050, player: 1 }, next: { funds: 9150, player: 2 }, round: 3, rotatePlayers: false, supply: null, miss: false } - CreateBuilding (4,5) { building: Factory { id: 3, health: 100, player: 0 } } + CreateBuilding (4,5) { building: Factory { id: 3, health: 100, player: 0 }, free: false } Move (5,1 → 4,1) { fuel: 39, completed: false, path: [4,1] } Capture (4,1) Move (2,4 → 1,4) { fuel: 39, completed: false, path: [1,4] } - CreateBuilding (1,4) { building: Factory { id: 3, health: 100, player: 2, completed: true } } + CreateBuilding (1,4) { building: Factory { id: 3, health: 100, player: 2, completed: true }, free: false } HiddenMove { path: [5,2 → 4,2], completed: false, fuel: null, unit: Infantry { id: 2, health: 100, player: 2, fuel: 50, moved: true, name: 'Valentin', completed: true } } EndTurn { current: { funds: 8450, player: 2 }, next: { funds: 9050, player: 1 }, round: 4, rotatePlayers: false, supply: null, miss: false } Move (2,1 → 3,2) { fuel: 48, completed: false, path: [3,1 → 3,2] } @@ -242,6 +242,6 @@ test('format spawn actions', () => { { colors: false }, ), ).toMatchInlineSnapshot( - `"Spawn { units: [2,1 → Pioneer { id: 1, health: 100, player: 1, fuel: 40 }, 1,2 → Pioneer { id: 1, health: 100, player: 1, fuel: 40 }, 5,4 → Pioneer { id: 1, health: 100, player: 2, fuel: 40 }, 4,5 → Pioneer { id: 1, health: 100, player: 2, fuel: 40 }], teams: [ { id: 1, name: '', players: [ { activeSkills: [], ai: undefined, charge: 0, crystal: undefined, funds: 10000, id: 1, misses: 0, skills: [], stats: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], userId: '1' } ] }, { id: 2, name: '', players: [ { activeSkills: [], ai: undefined, charge: 0, crystal: undefined, funds: 10000, id: 2, misses: 0, skills: [], stats: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], userId: '2' } ] } ] }"`, + `"Spawn { units: [2,1 → Pioneer { id: 1, health: 100, player: 1, fuel: 40 }, 1,2 → Pioneer { id: 1, health: 100, player: 1, fuel: 40 }, 5,4 → Pioneer { id: 1, health: 100, player: 2, fuel: 40 }, 4,5 → Pioneer { id: 1, health: 100, player: 2, fuel: 40 }], teams: [ { id: 1, name: '', players: [ { activeSkills: [], ai: undefined, charge: 0, crystal: undefined, funds: 10000, id: 1, misses: 0, skills: [], stats: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], userId: '1' } ] }, { id: 2, name: '', players: [ { activeSkills: [], ai: undefined, charge: 0, crystal: undefined, funds: 10000, id: 2, misses: 0, skills: [], stats: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], userId: '2' } ] } ], buildings: null }"`, ); }); diff --git a/tests/__tests__/HiddenAction.test.tsx b/tests/__tests__/HiddenAction.test.tsx index ce0976f0..a515719e 100644 --- a/tests/__tests__/HiddenAction.test.tsx +++ b/tests/__tests__/HiddenAction.test.tsx @@ -149,18 +149,18 @@ test('create building and create unit actions', async () => { expect(snapshotGameState(gameState)).toMatchInlineSnapshot(` "EndTurn { current: { funds: 500, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: null, supply: null, miss: null } - CreateBuilding (5,4) { building: House { id: 2, health: 100, player: 2, completed: true } } + CreateBuilding (5,4) { building: House { id: 2, health: 100, player: 2, completed: true }, free: null } CompleteUnit (1,1) - CreateBuilding (4,5) { building: House { id: 2, health: 100, player: 2, completed: true } } + CreateBuilding (4,5) { building: House { id: 2, health: 100, player: 2, completed: true }, free: null } CreateUnit (5,5 → 5,5) { unit: Pioneer { id: 1, health: 100, player: 2, fuel: 40, moved: true, name: 'Sam', completed: true }, free: false, skipBehaviorRotation: false } EndTurn { current: { funds: 200, player: 2 }, next: { funds: 500, player: 1 }, round: 2, rotatePlayers: null, supply: null, miss: null }" `); expect(snapshotEncodedActionResponse(gameActionResponse)) .toMatchInlineSnapshot(` "EndTurn { current: { funds: 500, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } - CreateBuilding (5,4) { building: House { id: 2, health: 100, player: 0 } } + CreateBuilding (5,4) { building: House { id: 2, health: 100, player: 0 }, free: false } CompleteUnit (1,1) - CreateBuilding (4,5) { building: House { id: 2, health: 100, player: 0 } } + CreateBuilding (4,5) { building: House { id: 2, health: 100, player: 0 }, free: false } EndTurn { current: { funds: 200, player: 2 }, next: { funds: 500, player: 1 }, round: 2, rotatePlayers: false, supply: null, miss: false }" `); diff --git a/tests/__tests__/Spawn.test.tsx b/tests/__tests__/Spawn.test.tsx index e006204f..23c91f0e 100644 --- a/tests/__tests__/Spawn.test.tsx +++ b/tests/__tests__/Spawn.test.tsx @@ -123,7 +123,7 @@ test('spawns units and adds new players', async () => { expect( snapshotEncodedActionResponse(encodedGameActionResponse), ).toMatchInlineSnapshot( - `"Spawn { units: [2,2 → Fighter Jet { id: 18, health: 100, player: 1, fuel: 50, ammo: [ [ 1, 8 ] ], name: 'Titan' }, 1,3 → Bomber { id: 19, health: 100, player: 4, fuel: 40, ammo: [ [ 1, 5 ] ], name: 'Léon' }, 3,2 → Bomber { id: 19, health: 100, player: 2, fuel: 40, ammo: [ [ 1, 5 ] ], name: 'Léon' }], teams: [ { id: 1, name: '', players: [ { activeSkills: [], ai: undefined, charge: 0, funds: 500, id: 4, misses: 0, name: 'Test Player', skills: [], stats: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] } ] }, { id: 5, name: '', players: [ { activeSkills: [], ai: undefined, charge: 0, funds: 500, id: 5, misses: 0, name: 'Test Player', skills: [], stats: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] } ] } ] }"`, + `"Spawn { units: [2,2 → Fighter Jet { id: 18, health: 100, player: 1, fuel: 50, ammo: [ [ 1, 8 ] ], name: 'Titan' }, 1,3 → Bomber { id: 19, health: 100, player: 4, fuel: 40, ammo: [ [ 1, 5 ] ], name: 'Léon' }, 3,2 → Bomber { id: 19, health: 100, player: 2, fuel: 40, ammo: [ [ 1, 5 ] ], name: 'Léon' }], teams: [ { id: 1, name: '', players: [ { activeSkills: [], ai: undefined, charge: 0, funds: 500, id: 4, misses: 0, name: 'Test Player', skills: [], stats: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] } ] }, { id: 5, name: '', players: [ { activeSkills: [], ai: undefined, charge: 0, funds: 500, id: 5, misses: 0, name: 'Test Player', skills: [], stats: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] } ] } ], buildings: [] }"`, ); printGameState('Last State', screenshot); @@ -177,7 +177,7 @@ test('spawns new units at adjacent fields if necessary', async () => { expect(snapshotEncodedActionResponse(gameActionResponse)) .toMatchInlineSnapshot(` "EndTurn { current: { funds: 500, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } - Spawn { units: [3,2 → Flamethrower { id: 15, health: 100, player: 0, fuel: 30, ammo: [ [ 1, 4 ] ], name: 'Zephyr' }], teams: null } + Spawn { units: [3,2 → Flamethrower { id: 15, health: 100, player: 0, fuel: 30, ammo: [ [ 1, 4 ] ], name: 'Zephyr' }], teams: null, buildings: [] } EndTurn { current: { funds: 500, player: 2 }, next: { funds: 500, player: 1 }, round: 2, rotatePlayers: false, supply: null, miss: false }" `); }); @@ -258,11 +258,11 @@ test('stops capturing if there is nothing to capture on that field', async () => expect(snapshotEncodedActionResponse(gameActionResponseA)) .toMatchInlineSnapshot(` - "EndTurn { current: { funds: 500, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } - Spawn { units: [2,2 → Pioneer { id: 1, health: 100, player: 2, fuel: 0, name: 'Sam', capturing: true }], teams: null } - Capture (2,2) { building: House { id: 2, health: 100, player: 2 }, player: 1 } - EndTurn { current: { funds: 500, player: 2 }, next: { funds: 500, player: 1 }, round: 2, rotatePlayers: false, supply: null, miss: false }" - `); + "EndTurn { current: { funds: 500, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } + Spawn { units: [2,2 → Pioneer { id: 1, health: 100, player: 2, fuel: 0, name: 'Sam', capturing: true }], teams: null, buildings: [] } + Capture (2,2) { building: House { id: 2, health: 100, player: 2 }, player: 1 } + EndTurn { current: { funds: 500, player: 2 }, next: { funds: 500, player: 1 }, round: 2, rotatePlayers: false, supply: null, miss: false }" + `); const mapB = mapA.copy({ buildings: map.buildings.delete(vecB), @@ -276,10 +276,10 @@ test('stops capturing if there is nothing to capture on that field', async () => expect(snapshotEncodedActionResponse(gameActionResponseB)) .toMatchInlineSnapshot(` - "EndTurn { current: { funds: 500, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } - Spawn { units: [2,2 → Pioneer { id: 1, health: 100, player: 2, fuel: 0, name: 'Sam' }], teams: null } - EndTurn { current: { funds: 500, player: 2 }, next: { funds: 500, player: 1 }, round: 2, rotatePlayers: false, supply: null, miss: false }" - `); + "EndTurn { current: { funds: 500, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } + Spawn { units: [2,2 → Pioneer { id: 1, health: 100, player: 2, fuel: 0, name: 'Sam' }], teams: null, buildings: [] } + EndTurn { current: { funds: 500, player: 2 }, next: { funds: 500, player: 1 }, round: 2, rotatePlayers: false, supply: null, miss: false }" + `); const mapC = mapA.copy({ buildings: map.buildings.set(vecB, House.create(2)), @@ -293,10 +293,10 @@ test('stops capturing if there is nothing to capture on that field', async () => expect(snapshotEncodedActionResponse(gameActionResponseC)) .toMatchInlineSnapshot(` - "EndTurn { current: { funds: 500, player: 1 }, next: { funds: 600, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } - Spawn { units: [2,2 → Pioneer { id: 1, health: 100, player: 2, fuel: 0, name: 'Sam' }], teams: null } - EndTurn { current: { funds: 600, player: 2 }, next: { funds: 500, player: 1 }, round: 2, rotatePlayers: false, supply: null, miss: false }" - `); + "EndTurn { current: { funds: 500, player: 1 }, next: { funds: 600, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false } + Spawn { units: [2,2 → Pioneer { id: 1, health: 100, player: 2, fuel: 0, name: 'Sam' }], teams: null, buildings: [] } + EndTurn { current: { funds: 600, player: 2 }, next: { funds: 500, player: 1 }, round: 2, rotatePlayers: false, supply: null, miss: false }" + `); }); test('correctly keeps track of active players', async () => {