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

[Feature] Win Condition to Fulfill Multiple Optional Conditions #35

Open
cpojer opened this issue May 29, 2024 · 21 comments
Open

[Feature] Win Condition to Fulfill Multiple Optional Conditions #35

cpojer opened this issue May 29, 2024 · 21 comments
Labels

Comments

@cpojer
Copy link
Contributor

cpojer commented May 29, 2024

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:

  • Defeat unit by label
  • Capture 5 buildings
  • Escort 1 unit

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 and amount field (next to the default fields like reward and hidden). I believe that based on #34, this condition should also be possible to be optional, 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 that validateWinCondition 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 type OptionalCondition and then go through all win conditions to check if the player or any player within the team completed 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.
  • Please add a test to verify the win condition works as expected.

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's completed set and check if the player id match the same team as the player who unlocked the optional condition via map.matchesTeam(actionResponse.toPlayer, completedPlayerID)

Funding

  • We're using Polar.sh to distribute funds.
  • You receive the reward once the issue is completed & confirmed by Nakazawa Tech.
Fund with Polar
@cpojer
Copy link
Contributor Author

cpojer commented May 31, 2024

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: Objective instead of in checkWinCondition. I think it makes sense to add a separate function to checkWinCondition.tsx that verifies if all optional conditions have been fulfilled when an "OptionalCondition" is triggered.

See 9d400bc for the changes.

@cpojer
Copy link
Contributor Author

cpojer commented May 31, 2024

Here is the implementation of getCompletedObjectives which can be used to get the amount of conditions that have been fulfilled by the player's team.

@sookmax
Copy link
Contributor

sookmax commented Jun 1, 2024

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 that validateWinCondition should be changed to take the number of win conditions of the map.

@cpojer Hey since we have access to map inside validateWinCondition(), could we do something like this?

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;
}

@sookmax
Copy link
Contributor

sookmax commented Jun 1, 2024

Could you also update the link below? 🙏

Most likely the new win condition needs to be checked right here: https://github.com/nkzw-tech/athena-crisis/blob/main/apollo/GameOver.tsx#L172 instead of in checkWinCondition.

Also, what if I do something like this in checkWinConditions() instead?

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;
    }
  }
}

@cpojer
Copy link
Contributor Author

cpojer commented Jun 1, 2024

Updated the link 👍

Hey since we have access to map inside validateWinCondition(), could we do something like this?

Oh yes, good point, we don't have to change the function params.

Also, what if I do something like this in checkWinConditions() instead?

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 checkWinConditions(previousMap, activeMap, optionalObjective) in the if-block here: Objective and add the required handling, otherwise this condition would only be checked from the next action onwards instead of this one. Ignore this if your solution works already.

@sookmax
Copy link
Contributor

sookmax commented Jun 2, 2024

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 checkWinConditions(previousMap, activeMap, optionalObjective) in the if-block here

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 snapshotEncodedActionResponse() looks okay:

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: null, type: 9 }, conditionId: 0, toPlayer: 1 }
GameEnd { condition: { amount: 2, completed: Set(0) {}, hidden: false, optional: false, players: [], reward: null, type: 13 }, conditionId: 2, toPlayer: 1 }

This result was with one optional WinCriteria.DefeatAmount and one optional WinCriteria.CaptureAmount with the amount for WinCriteria.OptionalObjectiveAmount being 2. (type 13 in GameEnd is WinCriteria.OptionalObjectiveAmount)

@cpojer
Copy link
Contributor Author

cpojer commented Jun 2, 2024

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.

@sookmax
Copy link
Contributor

sookmax commented Jun 2, 2024

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 OptionalObjectiveAmount is specified only for player1, and one of the optional objectives is, say, CaptureLabel, but lets say, player2 destroys the labeled building so there's no way for player1 to achieve OptionalObjectiveAmount anymore since CaptureLabel for player1 has failed. What do you think should happen at the end?

@cpojer
Copy link
Contributor Author

cpojer commented Jun 2, 2024

Fortunately for us, it looks okay..?

Yay! Glad the reconciliation engine can figure it out just fine.

By the way, I was trying to come up with a scenario where OptionalObjectiveAmount is specified only for player1, and one of the optional objectives is, say, CaptureLabel, but lets say, player2 destroys the labeled building so there's no way for player1 to achieve OptionalObjectiveAmount anymore since CaptureLabel for player1 has failed. What do you think should happen at the end?

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).

@sookmax
Copy link
Contributor

sookmax commented Jun 2, 2024

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 OptionalObjective before that because the capture mission for player1 has failed?

@cpojer
Copy link
Contributor Author

cpojer commented Jun 2, 2024

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 players or without player1 being part of the players array. I'm not sure if it's easy to add that in without too much overhead, tbh.

@sookmax
Copy link
Contributor

sookmax commented Jun 2, 2024

player2 shouldn't be awarded with an objective if it doesn't apply to them and they denied it for somebody else.

Yeah sorry I asked that question because I was looking at this test, and it looks like OptionalObjective should be awarded to player2 automatically when the capture mission for player1 failed?

test('capture label win criteria fails because building is destroyed', async () => {
const v1 = vec(1, 3);
const v2 = vec(2, 3);
const initialMap = map.copy({
buildings: map.buildings.set(
v1,
House.create(0, { label: 1 }).setHealth(1),
),
config: map.config.copy({
winConditions: [
{
hidden: false,
label: new Set([1]),
optional: false,
players: [1],
type: WinCriteria.CaptureLabel,
},
],
}),
units: map.units.set(v2, HeavyTank.create(player1)),
});
expect(validateWinConditions(initialMap)).toBe(true);
const [, gameActionResponseA] = executeGameActions(initialMap, [
AttackBuildingAction(v2, v1),
]);
expect(
snapshotEncodedActionResponse(gameActionResponseA),
).toMatchInlineSnapshot(
`
"AttackBuilding (2,3 → 1,3) { hasCounterAttack: false, playerA: 1, building: null, playerC: null, unitA: DryUnit { health: 100, ammo: [ [ 1, 9 ] ] }, unitC: null, chargeA: null, chargeB: null, chargeC: null }
GameEnd { condition: { completed: Set(0) {}, hidden: false, label: [ 1 ], optional: false, players: [ 1 ], reward: null, type: 1 }, conditionId: 0, toPlayer: 2 }"
`,
);
const mapWithOptionalObjectives = optional(initialMap);
expect(validateWinConditions(mapWithOptionalObjectives)).toBe(true);
const [gameStateA_2, gameActionResponseA_2] = executeGameActions(
mapWithOptionalObjectives,
[AttackBuildingAction(v2, v1)],
);
expect(snapshotEncodedActionResponse(gameActionResponseA_2))
.toMatchInlineSnapshot(`
"AttackBuilding (2,3 → 1,3) { hasCounterAttack: false, playerA: 1, 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: 0, toPlayer: 2 }"
`);
expect(gameHasEnded(gameStateA_2)).toBe(false);
const [, gameActionResponseB] = executeGameActions(
initialMap.copy({ units: map.units.set(v2, HeavyTank.create(player2)) }),
[EndTurnAction(), AttackBuildingAction(v2, v1)],
);
expect(
snapshotEncodedActionResponse(gameActionResponseB),
).toMatchInlineSnapshot(
`
"EndTurn { current: { funds: 500, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false }
AttackBuilding (2,3 → 1,3) { hasCounterAttack: false, playerA: 2, building: null, playerC: null, unitA: DryUnit { health: 100, ammo: [ [ 1, 9 ] ] }, unitC: null, chargeA: null, chargeB: null, chargeC: null }
GameEnd { condition: { completed: Set(0) {}, hidden: false, label: [ 1 ], optional: false, players: [ 1 ], reward: null, type: 1 }, conditionId: 0, toPlayer: 2 }"
`,
);
const [gameStateB_2, gameActionResponseB_2] = executeGameActions(
mapWithOptionalObjectives.copy({
units: map.units.set(v2, HeavyTank.create(player2)),
}),
[EndTurnAction(), AttackBuildingAction(v2, v1)],
);
expect(snapshotEncodedActionResponse(gameActionResponseB_2))
.toMatchInlineSnapshot(`
"EndTurn { current: { funds: 500, player: 1 }, next: { funds: 500, player: 2 }, round: 1, rotatePlayers: false, supply: null, miss: false }
AttackBuilding (2,3 → 1,3) { 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: 0, toPlayer: 2 }"
`);
expect(gameHasEnded(gameStateB_2)).toBe(false);
});

So I was wondering if it should also be the case for OptionalObjectiveAmount. In other words, should it be something like:

player1 ends turn
player2 attacks and destroys the labeled building
OptionalObjective for player2 since player1's capture label failed
GameEnd for player2 since player1's OptionalObjectiveAmount is no longer possible

@cpojer
Copy link
Contributor Author

cpojer commented Jun 2, 2024

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 checkWinCondition to only consider opponent objectives when it is not optional.

@sookmax
Copy link
Contributor

sookmax commented Jun 2, 2024

Ha, things could get a little too complicated depending on the existence of players on a condition.

If every condition specifies players field (i.e., players.length > 0), then the result is in line with the test 'capture label win criteria fails because building is destroyed' above (i.e., the OptionalObjective is awarded to player2)

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 players field, then nothing happens after player2 destroys the labeled building:

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 AttackBuilding from player2 is not able to return the right condition CaptureLabel, and rather keeps returning a false condition DefeatAmount. And the reason why the false DefeatAmount is being returned is that it is placed before CaptureLabel condition in winConditions array and matchesPlayerList() would return true as long as condition.players is undefined (or length 0) and since once one player on the map satisfies the condition for DefeatAmount:

(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 DefeatAmount condition. So while looping over winConditions in checkWinConditions() we keep early returning DefeatAmount.

This got me thinking that maybe the current logic inside checkWinCondition() and/or checkWinConditions() are not well suited to handle our new optional objectives.

@sookmax
Copy link
Contributor

sookmax commented Jun 2, 2024

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 checkWinCondition to only consider opponent objectives when it is not optional.

Yeah I'll see to this.

@cpojer
Copy link
Contributor Author

cpojer commented Jun 2, 2024

We should be able to use the completed and optional state to skip over the ones that have already previously matched for a specific player, right?

@sookmax
Copy link
Contributor

sookmax commented Jun 2, 2024

Yes exactly. So I added this if statement temporarily in checkWinCondition in my local env:

if (
    condition.type !== WinCriteria.Default &&
    condition.optional &&
    condition.completed?.has(player)
  ) {
    return false;
  }

This didn't work, however, by the time when player2 did AttackBuilding since player2 had not completed DefeatAmount.

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 😅

@sookmax
Copy link
Contributor

sookmax commented Jun 5, 2024

The game should also only end if there is no other way for player1 to win, ie. there are no other win conditions without players or without player1 being part of the players array. I'm not sure if it's easy to add that in without too much overhead, tbh.

I've been thinking about this scenario where there's only one non-optional win condition for player1, which happens to be this new OptionalObjectiveAmount mission with amount: 2, but one of the optional objectives becomes impossible to achieve, because say, player2 destroys the labeled building. So in test, it looks something like:

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 OptionalObjectiveAmount, so that if that's the case, player2 can counter-win.

Here's one idea: should we add failed?: PlayerIDset to win conditions similar to completed field and update failed whenever an optional objective becomes impossible to complete for a particular player? Then again, where and how to update the condition.failed becomes the next question 🤔

@cpojer
Copy link
Contributor Author

cpojer commented Jun 7, 2024

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 failed if we need it, but it will need another set of changes again to be properly handled.

I would suggest something like:

  • Extract the conditions in checkWinConditions where this applies to into a separate function so they can be checked separately.
  • In Objective.tsx, add another section next to where we are checking for optional objectives on whether any of the optional conditions was denied.
  • If any matches, add a new actionResponse to the gameState to record it. Maybe something like DeniedOptionalObjectiveActionResponse?
  • Handle updating failed for the new ActionResponse in applyObjectiveActionResponse.

I think that should work.

@sookmax
Copy link
Contributor

sookmax commented Jun 8, 2024

In Objective.tsx, add another section next to where we are checking for optional objectives on whether any of the optional conditions was denied.

Maybe I'm missing something here, but how do we figure out an optional condition that is returned by checkWinConditions() is a denied condition in Objective.tsx? First of all, to even return the condition that was denied, which also happens to be optional, does that mean we need to revert 07f6651? And actually that's what I was trying to do with this change; to figure out the returned optional condition is denied by an opponent or not.

@cpojer
Copy link
Contributor Author

cpojer commented Jun 9, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants