Skip to content

AISkirmishPlayer::acquireEnemy() applies retaliation bonus to unrelated candidates instead of the attacking candidate #2413

@diqezit

Description

@diqezit

Prerequisites

  • I have searched for similar issues and confirmed this is not a duplicate

Game Version

  • Command & Conquer Generals
  • Command & Conquer Generals: Zero Hour
  • Other (please specify below)

Bug Description

There is an error in the logic for selecting the main enemy for skirmish AI in AISkirmishPlayer::acquireEnemy().

The expected behavior is that if the current candidate curPlayer has already selected this AI player as its target via getCurrentEnemy(), then this candidate should receive a small bonus in the final evaluation. The purpose of this bonus is to make the AI slightly more likely to choose the enemy that has already chosen it.

In the old code, this does not work because the check is performed not on curPlayer but on somePlayer from the inner loop. Because of this, if some other skirmish AI has already chosen me as its target, the bonus starts to apply to the current candidate being considered, even if that candidate is not an aggressor at all. At the same time, the candidate who actually chose me as its target often does not receive this bonus at all because it is skipped by the condition if (k == i) continue.

As a result, the old formula does not increase the priority of the actual aggressor, but slightly lowers the rating of other candidates. This makes the choice of enemy incorrect, and there is also a comment that contradicts this, saying that the AI should slightly prefer the one who is already attacking it.

It does not cause crashes or break the game directly, but it makes the heuristics for choosing a strategic enemy erroneous. This is especially noticeable in skirmish FFA when choosing an enemy for the first time and when re-selecting an enemy after the old enemy has been defeated.

currentEnemy is not only used within the AI. This value is returned via Player::getCurrentEnemy() and then used in ScriptEngine::getSkirmishEnemyPlayer(), which means that the error affects not only the internal score of the candidates, but also the broader game context where the engine and scripts determine who is the current enemy of this AI player.

To verify this, I compiled an older version of the AISkirmishPlayer::acquireEnemy() function with detailed MiniLog and started a skirmish match with several AI players. After the match started, I captured the log of the first enemy selection at frame 0 to see how each AI assigns itself a currentEnemy. This log shows that after player6 is selected -> player5, player5's bonus begins to apply to other candidates rather than to player6 itself.

After that, without changing the code, I continued the match with logging enabled to see how the function behaves further. The logs show that almost all AI then go through early exit and hold the already selected enemy until it is considered inBadShape.

Then waited for the moment when player7's current enemy, player0, dropped out of the game. This can be seen in the log at frame 600 on the line:
current enemy for player7 is player0 | hasUnits=0 hasBuildFacility=0 inBadShape=1
After that, player7 recalculated the enemy and switched to player6. This confirmed that the problem concerned not only the initial selection at the start of the match, but also the subsequent re-selection of the enemy.

Thus, I tested two modes of operation for this function:
the initial selection of the enemy at the start of the match
and the re-selection of the enemy after the current enemy ceased to be a valid target.


Addition for example there i s a three players: A (our AI) B (human) C (enemy AI)

C chooses A as its target, i.e., C->getCurrentEnemy() equals A
When A evaluates candidates, the following happens

When evaluating candidate B, the inner loop iterates through all players except B, because k==i skips the current candidate. C enters this loop. The check shows that C has chosen me as its target. Therefore B receives a bonus of -625.

When evaluating candidate C the loop iterates through all players except C because k==i skips the current candidate. C itself does not enter the loop so it is skipped. The check for C does not work. C does not receive a bonus.
So B becomes 625 cheaper than C. Although it is C that attacks me, and not B.

The aggressor excludes himself from the check that should apply to him. He is the only one who cannot give himself a bonus because k==i skips him. All other candidates go through the full cycle, where the aggressor is present and gives them a bonus instead of himself.


Reproduction Steps

  1. Take the old version of AISkirmishPlayer::acquireEnemy() without the fix

  2. Add a detailed MiniLog to it inside the candidate selection loop
    There, you need to log the current AI player, the current candidate curPlayer, the player from the inner loop somePlayer, somePlayer->getCurrentEnemy(), the gang penalty trigger, the retaliation bonus trigger, the finalScore, and the moment of switching m_currentEnemy.

  3. Start a skirmish match in FFA mode with several AI players. It is best to use a match where there are more than two enemies so that there is a real choice between several candidates.

  4. Find the moment in the log when one AI has already chosen another as its target. For example, in my log, the following happened first:
    player6 switching enemy from to player5

  5. After that, see how player5 evaluates its candidates. In the old code, you can see that the retaliation bonus starts to apply not to candidate player6 but to other candidates such as player0, player1, player2, player3, player4, player7

  6. Then look at the evaluation of the aggressor candidate itself. In the old code, the bonus is not applied to candidate player6 because it is excluded by the line if (k == i) continue

  7. For additional confirmation, wait for the enemy to be reselected after the current enemy becomes inBadShape. For me, this reproduced on player7 after player0 was eliminated. After that, the function recalculated the enemy, and it was possible to check the same logic not at the start of the match, but when reselecting.

Additional Context

To put it simply... When one bot attacks another, but the latter does not respond and continues to pressure the third player. This is especially noticeable when a bot persistently attacks a human player, even though another bot is already destroying its base.
When re-selecting an enemy after defeating the current AI, it may choose someone else instead of the one who was attacking it.
Overall, the selection of enemies at the beginning of the match and after destroying targets seems less logical than it should be.

An important subtlety of the bug is that it is not visually noticeable in the game itself. The bug lies in the candidate evaluation formula. It often does not reverse the final choice because the weight of the retaliation bonus is small and the function has an early exit if the current enemy is still combat-ready. As soon as somePlayer has already chosen me as their target, the bonus begins to apply to other candidates rather than to the aggressor candidate themselves. This is the incorrect behavior.

There may be some interaction with #514

Metadata

Metadata

Assignees

No one assigned

    Labels

    AIIs AI relatedBugSomething is not working right, typically is user facingMinorSeverity: Minor < Major < Critical < Blocker

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions