-
-
Notifications
You must be signed in to change notification settings - Fork 119
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
[Feature] Win Condition to Fulfill Multiple Optional Conditions #35
Comments
I updated the implementation from #34 to make it so that optional conditions can fire before the game ends with one condition. I think this makes the whole implementation from this issue possible, but also slightly more complex. Most likely the new win condition needs to be checked right here: See 9d400bc for the changes. |
Here is the implementation of |
@cpojer Hey since we have access to case WinCriteria.OptionalObjectiveAmount: {
if (!validateAmount(condition.amount)) {
return false;
}
const optionalObjectiveCount = map.config.winConditions.filter(
(condition) =>
condition.type !== WinCriteria.Default && condition.optional,
).length;
if (condition.amount > optionalObjectiveCount) {
return false;
}
return true;
} |
Could you also update the link below? 🙏
Also, what if I do something like this in if (
actionResponse.type === 'OptionalObjective' &&
winConditions.some(
(condition) => condition.type === WinCriteria.OptionalObjectiveAmount,
)
) {
const completedObjectiveCount = getCompletedObjectives(
map,
actionResponse.toPlayer,
).filter(
(conditionIndex) =>
winConditions[conditionIndex].type !==
WinCriteria.OptionalObjectiveAmount,
).length;
for (const condition of winConditions) {
if (
condition.type === WinCriteria.OptionalObjectiveAmount &&
completedObjectiveCount >= condition.amount
) {
return condition;
}
}
} |
Updated the link 👍
Oh yes, good point, we don't have to change the function params.
This is where it gets funky. You are right for any type of win condition, and I think we should actually just go for this solution. I'm not 100% sure if it will work right away. If it does, great, let's do it. If not, you'll probably need to call |
Haha now I'm not as confident as before 😂. Let me write some more tests and see if there's any oddities or edge cases with this implementation. But for a simple case, the snapshot generated from
This result was with one optional |
If it works, that's great! I am wondering if the second optional objective has a reward attached to it, if it will be applied correctly of if something goes wrong. If you add a test for that, and it passes, then great. If it fails, then I can fix that up. |
test('optional objective amount', async () => {
const v1 = vec(1, 1);
const v2 = vec(1, 2);
const v3 = vec(2, 1);
const v4 = vec(2, 2);
const initialMap = map.copy({
buildings: map.buildings
.set(v1, House.create(player1))
.set(v2, House.create(player2)),
config: map.config.copy({
winConditions: [
{
amount: 1,
hidden: false,
optional: true,
reward: {
skill: Skill.BuyUnitBazookaBear,
type: 'skill',
},
type: WinCriteria.DefeatAmount,
},
{
amount: 1,
hidden: false,
optional: true,
type: WinCriteria.CaptureAmount,
},
{
amount: 2,
hidden: false,
optional: false,
type: WinCriteria.OptionalObjectiveAmount,
},
],
}),
units: map.units
.set(v2, Pioneer.create(player1).capture())
.set(v3, Flamethrower.create(player1))
.set(v4, Pioneer.create(player2)),
});
expect(validateWinConditions(initialMap)).toBe(true);
const [, gameActionResponse] = executeGameActions(initialMap, [
CaptureAction(v2),
AttackUnitAction(v3, v4),
]);
expect(snapshotEncodedActionResponse(gameActionResponse))
.toMatchInlineSnapshot(`
"Capture (1,2) { building: House { id: 2, health: 100, player: 1 }, player: 2 }
OptionalObjective { condition: { amount: 1, completed: Set(1) { 1 }, hidden: false, optional: true, players: [], reward: null, type: 2 }, conditionId: 1, toPlayer: 1 }
AttackUnit (2,1 → 2,2) { hasCounterAttack: false, playerA: 1, playerB: 2, unitA: DryUnit { health: 100, ammo: [ [ 1, 3 ] ] }, unitB: null, chargeA: 33, chargeB: 100 }
OptionalObjective { condition: { amount: 1, completed: Set(1) { 1 }, hidden: false, optional: true, players: [], reward: { skill: 12, type: 'skill' }, type: 9 }, conditionId: 0, toPlayer: 1 }
ReceiveReward { player: 1, reward: 'Reward { skill: 12 }' }
GameEnd { condition: { amount: 2, completed: Set(0) {}, hidden: false, optional: false, players: [], reward: null, type: 13 }, conditionId: 2, toPlayer: 1 }"
`);
}); Fortunately for us, it looks okay..? By the way, I was trying to come up with a scenario where |
Yay! Glad the reconciliation engine can figure it out just fine.
Oh wow, yeah that is indeed a problem! I would expect in that case that the game ends and player1 loses (or rather player2 wins). |
Got it. So eventually in that scenario, player2 should win since there's no way for player1 to achieve the win condition. Should player2 be awarded with |
player2 shouldn't be awarded with an objective if it doesn't apply to them and they denied it for somebody else. The game should also only end if there is no other way for player1 to win, ie. there are no other win conditions without |
Yeah sorry I asked that question because I was looking at this test, and it looks like athena-crisis/tests/__tests__/WinConditions.test.tsx Lines 397 to 481 in 677aed3
So I was wondering if it should also be the case for
|
Oh good find. This makes sense for required objectives since losing one of those automatically means the other player wins. However, these should not apply when they are optional! Sorry, I missed this when reviewing the previous PR, but all the cases where the opposing player is awarded an optional condition for denying a player should be changed, basically by changing |
Ha, things could get a little too complicated depending on the existence of If every condition specifies test.only('optional objective amount fails when one of the target objectives is no longer achievable', async () => {
const v1 = vec(1, 1);
const v2 = vec(1, 2);
const v3 = vec(2, 1);
const v4 = vec(2, 2);
const initialMap = map.copy({
buildings: map.buildings.set(
v1,
House.create(0, { label: 1 }).setHealth(1),
),
config: map.config.copy({
winConditions: [
{
amount: 1,
hidden: false,
optional: true,
players: [1],
type: WinCriteria.DefeatAmount,
},
{
hidden: false,
label: new Set([1]),
optional: true,
players: [1],
type: WinCriteria.CaptureLabel,
},
{
amount: 2,
hidden: false,
optional: false,
players: [1],
type: WinCriteria.OptionalObjectiveAmount,
},
],
}),
units: map.units
.set(v2, HeavyTank.create(player2))
.set(v3, Flamethrower.create(player1))
.set(v4, Pioneer.create(player2)),
});
expect(validateWinConditions(initialMap)).toBe(true);
const [, gameActionResponseA] = executeGameActions(initialMap, [
AttackUnitAction(v3, v4),
EndTurnAction(),
AttackBuildingAction(v2, v1),
]);
expect(snapshotEncodedActionResponse(gameActionResponseA))
.toMatchInlineSnapshot(`
"AttackUnit (2,1 → 2,2) { hasCounterAttack: false, playerA: 1, playerB: 2, unitA: DryUnit { health: 100, ammo: [ [ 1, 3 ] ] }, unitB: null, chargeA: 33, chargeB: 100 }
OptionalObjective { condition: { amount: 1, completed: Set(1) { 1 }, hidden: false, optional: true, players: [ 1 ], reward: null, type: 9 }, conditionId: 0, toPlayer: 1 }
EndTurn { current: { funds: 500, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false }
AttackBuilding (1,2 → 1,1) { hasCounterAttack: false, playerA: 2, building: null, playerC: null, unitA: DryUnit { health: 100, ammo: [ [ 1, 9 ] ] }, unitC: null, chargeA: null, chargeB: null, chargeC: null }
OptionalObjective { condition: { completed: Set(1) { 2 }, hidden: false, label: [ 1 ], optional: true, players: [ 1 ], reward: null, type: 1 }, conditionId: 1, toPlayer: 2 }"
`);
}); If, however, one of the optional objectives doesn't specify test.only('optional objective amount fails when one of the target objectives is no longer achievable', async () => {
const v1 = vec(1, 1);
const v2 = vec(1, 2);
const v3 = vec(2, 1);
const v4 = vec(2, 2);
const initialMap = map.copy({
buildings: map.buildings.set(
v1,
House.create(0, { label: 1 }).setHealth(1),
),
config: map.config.copy({
winConditions: [
{
amount: 1,
hidden: false,
optional: true,
// players: [1],
type: WinCriteria.DefeatAmount,
},
{
hidden: false,
label: new Set([1]),
optional: true,
players: [1],
type: WinCriteria.CaptureLabel,
},
{
amount: 2,
hidden: false,
optional: false,
players: [1],
type: WinCriteria.OptionalObjectiveAmount,
},
],
}),
units: map.units
.set(v2, HeavyTank.create(player2))
.set(v3, Flamethrower.create(player1))
.set(v4, Pioneer.create(player2)),
});
expect(validateWinConditions(initialMap)).toBe(true);
const [, gameActionResponseA] = executeGameActions(initialMap, [
AttackUnitAction(v3, v4),
EndTurnAction(),
AttackBuildingAction(v2, v1),
]);
expect(snapshotEncodedActionResponse(gameActionResponseA))
.toMatchInlineSnapshot(`
"AttackUnit (2,1 → 2,2) { hasCounterAttack: false, playerA: 1, playerB: 2, unitA: DryUnit { health: 100, ammo: [ [ 1, 3 ] ] }, unitB: null, chargeA: 33, chargeB: 100 }
OptionalObjective { condition: { amount: 1, completed: Set(1) { 1 }, hidden: false, optional: true, players: [], reward: null, type: 9 }, conditionId: 0, toPlayer: 1 }
EndTurn { current: { funds: 500, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false }
AttackBuilding (1,2 → 1,1) { hasCounterAttack: false, playerA: 2, building: null, playerC: null, unitA: DryUnit { health: 100, ammo: [ [ 1, 9 ] ] }, unitC: null, chargeA: null, chargeB: null, chargeC: null }"
`);
}); This is because a destructive action (condition.type === WinCriteria.DefeatAmount &&
matchesPlayer &&
(condition.players?.length ? condition.players : map.active).find(
(playerID) =>
map.getPlayer(playerID).stats.destroyedUnits >= condition.amount,
)) It'll be true for subsequent calls for a destructive action even if said destructive action has nothing to do with This got me thinking that maybe the current logic inside |
Yeah I'll see to this. |
We should be able to use the |
Yes exactly. So I added this if statement temporarily in if (
condition.type !== WinCriteria.Default &&
condition.optional &&
condition.completed?.has(player)
) {
return false;
} This didn't work, however, by the time when player2 did I'm not 100% sure but it might work out nicely if we don't allow awarding the opponent when an optional objective designated to one player (say, player1) fails, as you mentioned. I hope by banning this, things get a little less complicated 😅 |
I've been thinking about this scenario where there's only one non-optional win condition for player1, which happens to be this new test('optional objective amount fails when one of the target objectives is no longer achievable', async () => {
const v1 = vec(1, 1);
const v2 = vec(1, 2);
const v3 = vec(2, 1);
const v4 = vec(2, 2);
const initialMap = map.copy({
buildings: map.buildings.set(
v1,
House.create(0, { label: 1 }).setHealth(1),
),
config: map.config.copy({
winConditions: [
{
amount: 1,
hidden: false,
optional: true,
players: [1],
type: WinCriteria.DefeatAmount,
},
{
hidden: false,
label: new Set([1]),
optional: true,
players: [1],
type: WinCriteria.CaptureLabel,
},
{
amount: 2,
hidden: false,
optional: false,
players: [1],
type: WinCriteria.OptionalObjectiveAmount,
},
],
}),
units: map.units
.set(v2, HeavyTank.create(player2))
.set(v3, Flamethrower.create(player1))
.set(v4, Pioneer.create(player2)),
});
expect(validateWinConditions(initialMap)).toBe(true);
const [, gameActionResponseA] = executeGameActions(initialMap, [
AttackUnitAction(v3, v4),
EndTurnAction(),
AttackBuildingAction(v2, v1),
]);
expect(snapshotEncodedActionResponse(gameActionResponseA))
.toMatchInlineSnapshot();
}); And I'm having a hard time coming up with how to check whether the number of currently available optional missions becomes less than the amount set for Here's one idea: should we add |
Yes, this is exactly the issue I was trying to point out earlier, however it is important that the game doesn't get into an undefined state. It's fine to add I would suggest something like:
I think that should work. |
Maybe I'm missing something here, but how do we figure out an optional condition that is returned by |
Please check the steps I shared in the previous comment again. Those are meant to outline how to support this feature. tl;dr: split those specific conditions out, and add a param to either ignore them if they are optional or to return them, and add a new ActionResponse to note that an optional objective was denied. Feel free to DM me on Discord and I can walk you through. |
Once support for optional win conditions (see #17) is merged via #34, we can start building on top of the feature by adding a win condition to reach multiple optional conditions. This is fun because we can set up optional conditions like:
And then define a win condition that requires reaching three optional conditions to win the game. Together with secret conditions, this opens up exciting possibilities for map scenarios where you might have to discover hidden optional conditions to win the game.
Code & Steps
This task is about implementing a new win criteria and condition called "OptionalConditionAmount" which should have a
players
andamount
field (next to the default fields likereward
andhidden
). I believe that based on #34, this condition should also be possible to beoptional
, even though that might be slightly confusing when used.WinConditions.tsx
for data structures and where to add a new win condition. Check out other win conditions that have an "amount" field. Ideally the validation for this win condition should verify that the number of optional conditions defined for the map is higher or equal to the "amount" specified on the win condition. For thatvalidateWinCondition
should be changed to take the number of win conditions of the map.checkWinCondition.tsx
for checking whether a condition was meet.checkWinCondition
should look for an ActionResponse of typeOptionalCondition
and then go through all win conditions to check if the player or any player within the teamcompleted
enough win conditions.PlayerCard
to show how many optional conditions were reached by the team compared to the amount, similar to the other conditions that are shown there. Feel free to pick an icon from https://icones.js.org/collection/pixelarticons or otherwise use a placeholder and I'll make a fitting one.TypeScript via
pnpm tsc
should guide you through adding various pieces of code once you add the win condition.Note: This win condition should be triggered if enough optional conditions are fulfilled by the player or anyone on the same team. Therefore the check in
checkWinCondition
should look for each condition'scompleted
set and check if the player id match the same team as the player who unlocked the optional condition viamap.matchesTeam(actionResponse.toPlayer, completedPlayerID)
Funding
The text was updated successfully, but these errors were encountered: