Why does this happen in way too many games...?

Pengu

Prince
Joined
Apr 9, 2011
Messages
458
It's just ridicolous how much the Ai cheats really. 50% is never 50%. In my last game I started a war vs the Duth with cavalry and he had pikes, knight and longbows etc. My first 19 fights with odds and outcome:

65.2 loss
65.2 win
66.6 loss
71.1 win
83.1 win
72.5 win
94.4 win
85.2 loss
85.2 win
75.1 win
75.1 win
88.1 win
88.7 win
82.7 loss
88.9 win
63.3 loss
66.4 win
73.6 loss
79.3 loss

S, on average I had a 73,35% to win every fightwhich means I should be losing 3.8 times. BUT instead I am losing 7 fights, which is double! And thiis is NOT an exception and it happens in so many games. This is just blatant cheating by the AI imo....

Moderator Action: Moved to General Sub-Forum....cheers-lymond (and keep up the good fight)
 
Last edited by a moderator:
Do you expect that in a tiny, selection biased sample of 19 rolls you'd somehow break even? Variance doesn't work like that. It's possible that you've just ran bad for your entire civving career, I'm not saying that. It's actually very obvious that there are players who are running hundreds if not thousands of units under expected value, because that's the way randomness works and is supposed to work. If you don't like randomness go play something else or world builder your units back. Nobody gives a fudge.

btw, wrong forum. S&T is not for this nonsense.
 
Sometimes i win 10x 60% odds in a row
Sometimes i lose 4 out of 5 90% fights.
It's random and varies depending on the seed. If you got a super unlucky seed and don't want to accept it, just reload and attack with a differently promoted unit first and it will change the entire outcome.
 
I believe this has been discussed countless times and it has been conclusively proven by folks who have dived into the code; there is absolutely no chance the AI is cheating. There is absolutely nothing in the game's code to show the AI receiving an advantage, even a tiny one. You've just had a string of bad luck.

Kind regards,
Ita Bear
 
This thread caught me on a day I forgot my calculator at home, so worked it out the lazy way on Excel. Some people on this forum are mathematicians and will point out the errors I am committing. I assumed that your list of probabilities could be replaced by a series of 19 attacks with 77.35% probability of success. Here is my list:

The average you quoted was 73.35%. I calculated 77.35%. Did you make a typo?
I calculated 22.65% of 19 to be 4.3. You mentioned 3.8.
7 is less than double 3.8 and certainly less than double 4.3.
People who think in terms of numbers are going to be very picky about stuff like this. The last line in your OP appears to have two errors.
It might make more sense to calculate an expected average and standard deviation, then express the difference in standard deviation.

Probability of 4 or more losses: 65.4%
Probability of 5 or more losses: 43.7%
Probability of 6 or more losses: 24.7%
Probability of 7 or more losses: 11.7%
Probability of 8 or more losses: 4.6%

Here are my ideas:
It is a higher than expected loss rate and it occurs about 1 in 9 times. You are very likely to notice when it happens. You are more likely to remember when it does happen. This will introduce a bias. The bias is you will remember more unlucky streaks than normal streaks.
The random seed is normally fixed. So when you got your unlucky streak, you can reload and replay exactly as it was and then take your data.

You say 50% is not 50%. You can test this by going to worldbuilder and stacking 100 of your units against 100 of their units and testing the result.
If you use the BUG mod, you can check your combat log to determine what is going on. This will give more data than overall wins and losses. You will see how combat works.

Is it possible the tool tip gives the wrong answer based on bad data? I do not know. The code divers say no.

Regarding your situation, I can think of two options:
One is bring a larger stack. If you were attacking 19 AI units and expecting 4 losses and brought 23, then you did not kill the enemy stack.
Another is to play with a new random seed on reload. It is an option in the custom game. If you don't like your luck in attacking a stack, then just reload. It only feels bad for a little while.

It's random and varies depending on the seed.

Also, is it possible the random seed is streaky?
 
Is it possible the tool tip gives the wrong answer based on bad data? I do not know. The code divers say no.
Combat odds are well known to undervalue the effect First Strikes have in combat rounds.

I have also noticed in my time playing the game, there is heavier bias towards base power than high mods, likely because of the round-based combat system.

Actually, both issues seem to be directly because of combat rounds if you think about it. First Strikes allow a lost round (or multiple) to just be thrown out, which of course skews overall results towards victories.
 
Combat in CIV is a pointlessly painfully complicated thing. And I don't understand it fully my self. But I'll try and explain it as simply as I can.

In a lot of games like say XCOM combat is resolved based on a single dice roll. That is to say you have an X% chance to kill the enemy, the game rolls a dice to get a number between 1 and 100 and that's that. In these games the percentage chance of success shown to the player is actually the chance to succeed. The two are mechanically linked.

Civ 4 does not work like that. Instead each times units engage in combat in this game they fight a series of combat rounds. In each round both units roll dice to see how much (if at all) they damage the other. In addition to this you also have first strikes which basically give the unit with more of them them an extra round (or several) in which it can hit the enemy but the enemy can't hit back. All this and other complications (seriously, look up the messed up way strength promotions work) mean that the percentage shown to you as "combat odds" has no direct mathematical connection to the actual outcome of the combat. Rather the number shown to you is just an estimate, a semi educated guess if you will, that the game produces to give the player a rough idea of what to expect.

So in conclusion it's not that the AI is cheating but rather that the odds the game tells you are basically it eyeballing the odds and might as well be sentences along the lines of "Yea, dude, you got this." or "You'll probably loose, don't bother". instead of real numbers.
 
Last edited:
Honestly, I've felt like you're feeling many times... until I actually grabbed a piece of paper and kept track of some common odds ranges over the course of a game, and it generally is close to the stated odds. Confirmation bias is for real, man.
 
Without having done any calculations, but having made quite a few changes in the code, including the attack/defend part, I think the main issue with the predictions is that they are not accurate.

Firstly there are many modifiers. Some are straightforward giving an increased percentage, while others, like first strikes, acts a bit different.
Spoiler Modifiers :

pCombatDetails->iExtraCombatPercent = 0;
pCombatDetails->iAnimalCombatModifierTA = 0;
pCombatDetails->iAIAnimalCombatModifierTA = 0;
pCombatDetails->iAnimalCombatModifierAA = 0;
pCombatDetails->iAIAnimalCombatModifierAA = 0;
pCombatDetails->iBarbarianCombatModifierTB = 0;
pCombatDetails->iAIBarbarianCombatModifierTB = 0;
pCombatDetails->iBarbarianCombatModifierAB = 0;
pCombatDetails->iAIBarbarianCombatModifierAB = 0;
pCombatDetails->iPlotDefenseModifier = 0;
pCombatDetails->iFortifyModifier = 0;
pCombatDetails->iCityDefenseModifier = 0;
pCombatDetails->iHillsAttackModifier = 0;
pCombatDetails->iHillsDefenseModifier = 0;
pCombatDetails->iFeatureAttackModifier = 0;
pCombatDetails->iFeatureDefenseModifier = 0;
pCombatDetails->iTerrainAttackModifier = 0;
pCombatDetails->iTerrainDefenseModifier = 0;
pCombatDetails->iCityAttackModifier = 0;
pCombatDetails->iDomainDefenseModifier = 0;
pCombatDetails->iCityBarbarianDefenseModifier = 0;
pCombatDetails->iClassDefenseModifier = 0;
pCombatDetails->iClassAttackModifier = 0;
pCombatDetails->iCombatModifierA = 0;
pCombatDetails->iCombatModifierT = 0;
pCombatDetails->iDomainModifierA = 0;
pCombatDetails->iDomainModifierT = 0;
pCombatDetails->iAnimalCombatModifierA = 0;
pCombatDetails->iAnimalCombatModifierT = 0;
pCombatDetails->iRiverAttackModifier = 0;
pCombatDetails->iAmphibAttackModifier = 0;
pCombatDetails->iKamikazeModifier = 0;
pCombatDetails->iModifierTotal = 0;
pCombatDetails->iBaseCombatStr = 0;
pCombatDetails->iCombat = 0;
pCombatDetails->iMaxCombatStr = 0;
pCombatDetails->iCurrHitPoints = 0;
pCombatDetails->iMaxHitPoints = 0;
pCombatDetails->iCurrCombatStr = 0;


But as you can see in the following code, the odds are not just an A vs B calculation. Combat consists of several rounds, that also are affected by withdrawal, first strikes etc.
So my estimate is that its the odds calculator that are not precise. Not the AI cheating.

Spoiler Calculate Odds code :

Code:
int getCombatOdds(CvUnit* pAttacker, CvUnit* pDefender)
{
    float fOddsEvent;
    float fOddsAfterEvent;
    int iAttackerStrength;
    int iAttackerFirepower;
    int iDefenderStrength;
    int iDefenderFirepower;
    int iDefenderOdds;
    int iAttackerOdds;
    int iStrengthFactor;
    int iDamageToAttacker;
    int iDamageToDefender;
    int iNeededRoundsAttacker;
    int iNeededRoundsDefender;
    int iMaxRounds;
    int iAttackerLowFS;
    int iAttackerHighFS;
    int iDefenderLowFS;
    int iDefenderHighFS;
    int iFirstStrikes;
    int iDefenderHitLimit;
    int iI;
    int iJ;
    int iI3;
    int iI4;
    int iOdds = 0;

    // setup battle, calculate strengths and odds
    //////

    //Added ST
    iAttackerStrength = pAttacker->currCombatStr(NULL, NULL);
    iAttackerFirepower = pAttacker->currFirepower(NULL, NULL);

    iDefenderStrength = pDefender->currCombatStr(pDefender->plot(), pAttacker);
    iDefenderFirepower = pDefender->currFirepower(pDefender->plot(), pAttacker);

    FAssert((iAttackerStrength + iDefenderStrength) > 0);
    FAssert((iAttackerFirepower + iDefenderFirepower) > 0);

    iDefenderOdds = ((GC.getDefineINT("COMBAT_DIE_SIDES") * iDefenderStrength) / (iAttackerStrength + iDefenderStrength));
    if (iDefenderOdds == 0)
    {
        return 1000;
    }
    iAttackerOdds = GC.getDefineINT("COMBAT_DIE_SIDES") - iDefenderOdds;  
    if (iAttackerOdds == 0)
    {
        return 0;
    }

    iStrengthFactor = ((iAttackerFirepower + iDefenderFirepower + 1) / 2);

    // calculate damage done in one round
    //////

    iDamageToAttacker = std::max(1,((GC.getDefineINT("COMBAT_DAMAGE") * (iDefenderFirepower + iStrengthFactor)) / (iAttackerFirepower + iStrengthFactor)));
    iDamageToDefender = std::max(1,((GC.getDefineINT("COMBAT_DAMAGE") * (iAttackerFirepower + iStrengthFactor)) / (iDefenderFirepower + iStrengthFactor)));

    // calculate needed rounds.
    // Needed rounds = round_up(health/damage)
    //////

    iDefenderHitLimit = pDefender->maxHitPoints() - pAttacker->combatLimit();

    iNeededRoundsAttacker = (std::max(0, pDefender->currHitPoints() - iDefenderHitLimit) + iDamageToDefender - 1 ) / iDamageToDefender;
    iNeededRoundsDefender = (pAttacker->currHitPoints() + iDamageToAttacker - 1 ) / iDamageToAttacker;
    iMaxRounds = iNeededRoundsAttacker + iNeededRoundsDefender - 1;

    // calculate possible first strikes distribution.
    // We can't use the getCombatFirstStrikes() function (only one result,
    // no distribution), so we need to mimic it.
    //////

    iAttackerLowFS = (pDefender->immuneToFirstStrikes()) ? 0 : pAttacker->firstStrikes();
    iAttackerHighFS = (pDefender->immuneToFirstStrikes()) ? 0 : (pAttacker->firstStrikes() + pAttacker->chanceFirstStrikes());

    iDefenderLowFS = (pAttacker->immuneToFirstStrikes()) ? 0 : pDefender->firstStrikes();
    iDefenderHighFS = (pAttacker->immuneToFirstStrikes()) ? 0 : (pDefender->firstStrikes() + pDefender->chanceFirstStrikes());

    // For every possible first strike event, calculate the odds of combat.
    // Then, add these to the total, weighted to the chance of that first
    // strike event occurring
    //////

    for (iI = iAttackerLowFS; iI < iAttackerHighFS + 1; iI++)
    {
        for (iJ = iDefenderLowFS; iJ < iDefenderHighFS + 1; iJ++)
        {
            // for every possible combination of fs results, calculate the chance

            if (iI >= iJ)
            {
                // Attacker gets more or equal first strikes than defender

                iFirstStrikes = iI - iJ;

                // For every possible first strike getting hit, calculate both
                // the chance of that event happening, as well as the rest of
                // the chance assuming the event has happened. Multiply these
                // together to get the total chance (Bayes rule).
                // iI3 counts the number of successful first strikes
                //////

                for (iI3 = 0; iI3 < (iFirstStrikes + 1); iI3++)
                {
                    // event: iI3 first strikes hit the defender

                    // calculate chance of iI3 first strikes hitting: fOddsEvent
                    // f(k;n,p)=C(n,k)*(p^k)*((1-p)^(n-k))
                    // this needs to be in floating point math
                    //////

                    fOddsEvent = ((float)getBinomialCoefficient(iFirstStrikes, iI3)) * pow((((float)iAttackerOdds) / GC.getDefineINT("COMBAT_DIE_SIDES")), iI3) * pow((1.0f - (((float)iAttackerOdds) / GC.getDefineINT("COMBAT_DIE_SIDES"))), (iFirstStrikes - iI3));

                    // calculate chance assuming iI3 first strike hits: fOddsAfterEvent
                    //////

                    if (iI3 >= iNeededRoundsAttacker)
                    {
                        fOddsAfterEvent = 1;
                    }
                    else
                    {
                        fOddsAfterEvent = 0;

                        // odds for _at_least_ (iNeededRoundsAttacker - iI3) (the remaining hits
                        // the attacker needs to make) out of (iMaxRounds - iI3) (the left over
                        // rounds) is the sum of each _exact_ draw
                        //////

                        for (iI4 = (iNeededRoundsAttacker - iI3); iI4 < (iMaxRounds - iI3 + 1); iI4++)
                        {
                            // odds of exactly iI4 out of (iMaxRounds - iI3) draws.
                            // f(k;n,p)=C(n,k)*(p^k)*((1-p)^(n-k))
                            // this needs to be in floating point math
                            //////

                            fOddsAfterEvent += ((float)getBinomialCoefficient((iMaxRounds - iI3), iI4)) * pow((((float)iAttackerOdds) / GC.getDefineINT("COMBAT_DIE_SIDES")), iI4) * pow((1.0f - (((float)iAttackerOdds) / GC.getDefineINT("COMBAT_DIE_SIDES"))), ((iMaxRounds - iI3) - iI4));
                        }
                    }

                    // Multiply these together, round them properly, and add
                    // the result to the total iOdds
                    //////

                    iOdds += ((int)(1000.0 * (fOddsEvent*fOddsAfterEvent + 0.0005)));
                }
            }
            else // (iI < iJ)
            {
                // Attacker gets less first strikes than defender

                iFirstStrikes = iJ - iI;

                // For every possible first strike getting hit, calculate both
                // the chance of that event happening, as well as the rest of
                // the chance assuming the event has happened. Multiply these
                // together to get the total chance (Bayes rule).
                // iI3 counts the number of successful first strikes
                //////

                for (iI3 = 0; iI3 < (iFirstStrikes + 1); iI3++)
                {
                    // event: iI3 first strikes hit the defender

                    // First of all, check if the attacker is still alive.
                    // Otherwise, no further calculations need to occur
                    /////

                    if (iI3 < iNeededRoundsDefender)
                    {
                        // calculate chance of iI3 first strikes hitting: fOddsEvent
                        // f(k;n,p)=C(n,k)*(p^k)*((1-p)^(n-k))
                        // this needs to be in floating point math
                        //////

                        fOddsEvent = ((float)getBinomialCoefficient(iFirstStrikes, iI3)) * pow((((float)iDefenderOdds) / GC.getDefineINT("COMBAT_DIE_SIDES")), iI3) * pow((1.0f - (((float)iDefenderOdds) / GC.getDefineINT("COMBAT_DIE_SIDES"))), (iFirstStrikes - iI3));

                        // calculate chance assuming iI3 first strike hits: fOddsAfterEvent
                        //////

                        fOddsAfterEvent = 0;

                        // odds for _at_least_ iNeededRoundsAttacker (the remaining hits
                        // the attacker needs to make) out of (iMaxRounds - iI3) (the left over
                        // rounds) is the sum of each _exact_ draw
                        //////

                        for (iI4 = iNeededRoundsAttacker; iI4 < (iMaxRounds - iI3 + 1); iI4++)
                        {

                            // odds of exactly iI4 out of (iMaxRounds - iI3) draws.
                            // f(k;n,p)=C(n,k)*(p^k)*((1-p)^(n-k))
                            // this needs to be in floating point math
                            //////

                            fOddsAfterEvent += ((float)getBinomialCoefficient((iMaxRounds - iI3), iI4)) * pow((((float)iAttackerOdds) / GC.getDefineINT("COMBAT_DIE_SIDES")), iI4) * pow((1.0f - (((float)iAttackerOdds) / GC.getDefineINT("COMBAT_DIE_SIDES"))), ((iMaxRounds - iI3) - iI4));
                        }

                        // Multiply these together, round them properly, and add
                        // the result to the total iOdds
                        //////

                        iOdds += ((int)(1000.0 * (fOddsEvent*fOddsAfterEvent + 0.0005)));
                    }
                }              
            }
        }
    }

    // Weigh the total to the number of possible combinations of first strikes events
    // note: the integer math breaks down when #FS > 656 (with a die size of 1000)
    //////

    iOdds /= (((pDefender->immuneToFirstStrikes()) ? 0 : pAttacker->chanceFirstStrikes()) + 1) * (((pAttacker->immuneToFirstStrikes()) ? 0 : pDefender->chanceFirstStrikes()) + 1);

    // finished!
    //////

    return iOdds;
}


Edit. Oh, didn't see PPQ_Purple's answer. At least we seems to agree on the issue :goodjob:
 
Last edited:
Firstly there are many modifiers. Some are straightforward giving an increased percentage, while others, like first strikes, acts a bit different. [...] But as you can see in the following code, the odds are not just an A vs B calculation. Combat consists of several rounds, that also are affected by withdrawal, first strikes etc.
So my estimate is that its the odds calculator that are not precise. Not the AI cheating. [...]
A lot could go wrong unnoticed – especially when a mod adds new combat abilities –, so I've implemented a test last year: I've copied the combat resolution procedure into a function that only simulates combat resolution (code on GitHub); used 500 000 executions of that function for an estimate of the combat odds (e.g. if the attacker wins in 400 000 simulations, the estimated odds are 80%); ran a game on AI Auto Play, comparing all combat odds calculated by the AI (same function as for the displayed combat odds) with simulation-based estimates, reporting differences of 0.2 percentage points or greater as potential errors. Found exactly one issue this way – which had already been fixed by the Advanced Combat Odds mod.

My conclusion is that – at worst – the displayed odds predict combat outcomes with high accuracy in almost all situations.

The frequency of streaks of bad luck or good luck in Civ 4 don't feel different to me than those in dice rolls in boardgames. (Although, if Civ 4 were by some measure twice as streaky, I don't know if I would be able to tell the difference. And it's admittedly been a while since I've played a boardgame with frequent dice rolls.)
 
Top Bottom