Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional win condition feature #34

Merged
merged 11 commits into from
May 30, 2024
3 changes: 2 additions & 1 deletion apollo/ActionMap.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,5 +171,6 @@
["BuySkill", [39, ["type", "from", "skill", "player"]]],
["ActivatePower", [40, ["type", "skill"]]],
["PreviousTurnGameOver", [41, ["type", "fromPlayer"]]],
["SecretDiscovered", [42, ["type", "condition"]]]
["SecretDiscovered", [42, ["type", "condition"]]],
["OptionalWin", [43, ["type", "condition", "conditionId", "toPlayer"]]]
]
89 changes: 73 additions & 16 deletions apollo/GameOver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,20 @@ export type GameEndActionResponse = Readonly<{
type: 'GameEnd';
}>;

export type OptionalWinActionResponse = Readonly<{
condition: WinCondition;
conditionId: number;
toPlayer: PlayerID;
type: 'OptionalWin';
}>;

export type GameOverActionResponses =
| AttackUnitGameOverActionResponse
| BeginTurnGameOverActionResponse
| CaptureGameOverActionResponse
| GameEndActionResponse
| PreviousTurnGameOverActionResponse;
| PreviousTurnGameOverActionResponse
| OptionalWinActionResponse;

function check(
previousMap: MapData,
Expand Down Expand Up @@ -90,9 +98,12 @@ const pickWinningPlayer = (
if (condition.type === WinCriteria.DefeatAmount) {
return (
condition.players?.length ? condition.players : activeMap.active
).find(
(playerID) =>
activeMap.getPlayer(playerID).stats.destroyedUnits >= condition.amount,
).find((playerID) =>
!condition.optional
? activeMap.getPlayer(playerID).stats.destroyedUnits >= condition.amount
: !condition.completed?.has(playerID) &&
activeMap.getPlayer(playerID).stats.destroyedUnits >=
condition.amount,
);
}

Expand Down Expand Up @@ -138,19 +149,20 @@ export function checkGameOverConditions(
const gameState: MutableGameState = actionResponse
? [[actionResponse, map]]
: [];
const gameEndResponse = condition
? ({
condition,
conditionId: activeMap.config.winConditions.indexOf(condition),
toPlayer: pickWinningPlayer(
previousMap,
activeMap,
lastActionResponse,

const winningPlayer = condition
? pickWinningPlayer(previousMap, activeMap, lastActionResponse, condition)
: undefined;

const gameEndResponse =
condition?.optional === false
? ({
condition,
),
type: 'GameEnd',
} as const)
: checkGameEnd(map);
conditionId: activeMap.config.winConditions.indexOf(condition),
toPlayer: winningPlayer,
type: 'GameEnd',
} as const)
: checkGameEnd(map);

if (gameEndResponse) {
let newGameState: GameState = [];
Expand All @@ -162,6 +174,36 @@ export function checkGameOverConditions(
];
}

const optionalWinResponse =
condition?.optional === true &&
winningPlayer &&
!condition.completed?.has(winningPlayer)
? ({
condition,
conditionId: activeMap.config.winConditions.indexOf(condition),
toPlayer: winningPlayer,
type: 'OptionalWin',
} as const)
: null;

if (optionalWinResponse) {
let newGameState: GameState = [];
[newGameState, map] = processRewards(map, optionalWinResponse);
map = applyGameOverActionResponse(map, optionalWinResponse);
return [
...gameState,
...newGameState,
[
// update `optionalWinResponse.condition` with the new `map.config` updated in `applyGameOverActionResponse()`
{
...optionalWinResponse,
condition: map.config.winConditions[optionalWinResponse.conditionId],
},
map,
],
];
}

if (
actionResponse?.type === 'AttackUnitGameOver' ||
actionResponse?.type === 'BeginTurnGameOver'
Expand Down Expand Up @@ -231,6 +273,21 @@ export function applyGameOverActionResponse(
}
case 'GameEnd':
return map;
case 'OptionalWin': {
const { condition, conditionId, toPlayer } = actionResponse;
const winConditions = Array.from(map.config.winConditions);
winConditions[conditionId] = {
...condition,
completed: condition.completed
? new Set([...condition.completed, toPlayer])
: new Set([toPlayer]),
};
return map.copy({
config: map.config.copy({
winConditions,
}),
});
}
default: {
actionResponse satisfies never;
throw new UnknownTypeError('applyGameOverActionResponse', type);
Expand Down
1 change: 1 addition & 0 deletions apollo/actions/applyActionResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ export default function applyActionResponse(
}
case 'BeginGame':
case 'SecretDiscovered':
case 'OptionalWin':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will not actually apply the action properly! You need to move this case up to where case 'GameEnd': is to make sure it calls applyGameOverActionResponse.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see. Just to make sure we're on the same page though, this is for the consumers of applyActionResponse(), right? Would it be possible for that consumer to call applyGameOverActionResponse() instead? Or maybe it's impossible to do so in some cases?

case 'Start':
return map;
default: {
Expand Down
1 change: 1 addition & 0 deletions apollo/lib/computeVisibleActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ const VisibleActionModifiers: Record<
MoveUnit: {
Source: true,
},
OptionalWin: true,
PreviousTurnGameOver: true,
ReceiveReward: true,
Rescue: {
Expand Down
1 change: 1 addition & 0 deletions apollo/lib/dropLabelsFromActionResponse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default function dropLabelsFromActionResponse(
case 'PreviousTurnGameOver':
case 'ReceiveReward':
case 'SecretDiscovered':
case 'OptionalWin':
case 'SetViewer':
case 'Start':
return actionResponse;
Expand Down
1 change: 1 addition & 0 deletions apollo/lib/getActionResponseVectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export default function getActionResponseVectors(
case 'PreviousTurnGameOver':
case 'ReceiveReward':
case 'SecretDiscovered':
case 'OptionalWin':
case 'SetViewer':
case 'Start':
break;
Expand Down
7 changes: 5 additions & 2 deletions apollo/lib/getWinningTeam.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { PlayerID } from '@deities/athena/map/Player.tsx';
import MapData from '@deities/athena/MapData.tsx';
import { GameEndActionResponse } from '../GameOver.tsx';
import {
GameEndActionResponse,
OptionalWinActionResponse,
} from '../GameOver.tsx';

export default function getWinningTeam(
map: MapData,
actionResponse: GameEndActionResponse,
actionResponse: GameEndActionResponse | OptionalWinActionResponse,
): 'draw' | PlayerID {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this is funky since the function name/module name no longer makes sense now. Maybe we should rename this to getMatchingTeam since it gives us the team matching the ActionResponse?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just my opinion, but I see processRewards() is the sole consumer of this getWinningTeam() function in the entire codebase, and it checks if the return value is 'draw' (i.e., winningTeam !== 'draw').

It just occurred to me that it might be a little awkward to have matchingTeam !== 'draw'? In the processRewards() perspective, I think it makes more sense to call it winningTeam indeed, since the winner takes the rewards?

Maybe we could wait until there are other use cases that calling getWinningTeam() wouldn't make sense at all?

Or maybe we could bring getWinningTeam() into processRewards.tsx as a local function and worry about other use cases later?

const isDraw = !actionResponse.toPlayer;
return isDraw
Expand Down
13 changes: 7 additions & 6 deletions apollo/lib/processRewards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ import MapData from '@deities/athena/MapData.tsx';
import { WinCriteria } from '@deities/athena/WinConditions.tsx';
import isPresent from '@deities/hephaestus/isPresent.tsx';
import applyActionResponse from '../actions/applyActionResponse.tsx';
import { GameEndActionResponse } from '../GameOver.tsx';
import {
GameEndActionResponse,
OptionalWinActionResponse,
} from '../GameOver.tsx';
import { GameState, MutableGameState } from '../Types.tsx';
import getWinningTeam from './getWinningTeam.tsx';

export function processRewards(
map: MapData,
gameEndResponse: GameEndActionResponse,
actionResponse: GameEndActionResponse | OptionalWinActionResponse,
): [GameState, MapData] {
const gameState: MutableGameState = [];
const winningTeam = getWinningTeam(map, gameEndResponse);
const winningTeam = getWinningTeam(map, actionResponse);
if (winningTeam !== 'draw') {
const rewards = new Set(
[
'condition' in gameEndResponse
? gameEndResponse.condition?.reward
: null,
'condition' in actionResponse ? actionResponse.condition?.reward : null,
map.config.winConditions.find(
(condition) => condition.type === WinCriteria.Default,
)?.reward,
Expand Down
4 changes: 3 additions & 1 deletion athena/MapData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,9 @@ export default class MapData {
data.config.biome,
(data.config.winConditions
? decodeWinConditions(data.config.winConditions)
: null) || [{ hidden: false, type: WinCriteria.Default }],
: null) || [
{ hidden: false, optional: false, type: WinCriteria.Default },
],
),
size,
toPlayerID(data.currentPlayer),
Expand Down
Loading
Loading