Monday, May 7, 2012

Turn Based Strategy Game AI part 6

Continued from part 5
This weeks changes were pretty minimal, but ended up having a large impact.  First up was some cleanup.  I created a new map that was symetrical to give a more balanced way of measuring AI skill differences.  I then also moved all constant weights used to determine AI behavior scoring into the DispositionProfile class, and made the DispositionProfile accessed through a property.

This change, enables different behaviors to be swapped in with minimal work.  While this week's AI "Frank" does not make use of this feature, it is in the works for the next week.  After doing this, I tweaked a few of the values to give less value to the Banding ability of the Spearmen units which has been causing them to stack up together instead of moving across the map.

The final change of note was to enable the AI to make use of Leader skills.  In Shattered Throne, when a unit is defeated, the number of stars (combo points) on the unit is added to the killing player as mana points that can be used to fuel special abilities unique to the player's chosen leader.  In this game, the blue player is using the High Priestess.  Each leader has both a minor, and major ability.  The High Priestess's minor ability is called "Mercy" and converts all active stars on enemy units into gold.  Her major ability is currently called "Blessing" and heals all friendly units by 2 points.

The abilities are so specific to each leader that I could not think of a generic way to handle the logic for using the abilities.  As such, I would have to code special logic for every leader skill in the game.  I did take note of a few things that would help out.  All the Major skills I have currently planned, are of use only at the beginning or end of a turn.  As such, I make a check at those two points on whether the Leader's Major skill should be used or not.

As there is a maximum amount of mana/stars that can be stored at once, which is not much higher than the cost of major skills.  If we can afford to cast a major skill, it is in the AI's best interest to use it as soon as possible, assuming it is not completely wasted.

I created a new class to encapsulate all the specific leader skill use checks called LeaderPowerCalculator.  When checking whether the Priestess should use her "Blessing" skill, I count the number of HP that could be healed by using the skill, and based on how many points would be healed, a chance to cast is generated, which quickly climbs to 100%.

        private int GetChanceToUseHealPower(GameState gs)
        {
            int dmgAmt = 0;

            // get amount of damage on our units
            foreach (UnitStatus u in gs.Units)
            {
                if (u.IsAlive && gs.AreFriendly(u.Owner, gs.CurrentPlayer))
                    dmgAmt += (u.CurrentDamage > 2) ? 2 : u.CurrentDamage; // do not count more than 2 points per unit
            }

            return (dmgAmt * 15) - 30;
        }

        public bool UsePowerAtTurnEnd(Faction.LeaderSpecialEffect power, GameState gs)
        {
            int useChance = 0;

            // determine chance to use power 
            switch (power)
            {
                case Faction.LeaderSpecialEffect.BLESSING:
                    useChance = GetChanceToUseHealPower(gs);
                    break;
            }

            return (useChance > _roller.Next(100));
        }

        public bool UsePowerAtTurnStart(Faction.LeaderSpecialEffect power, GameState gs)
        {
            int useChance = 0;

            // determine chance to use power 
            switch (power)
            {
                case Faction.LeaderSpecialEffect.BLESSING:
                    useChance = GetChanceToUseHealPower(gs);
                    break;
            }

            return (useChance > _roller.Next(100));
        }

The minor skills are a lot more tricky, and unlike the major skills, are best use throughout the turn, intermixed with the player's normal actions.  Because of this, I make a call to see if the AI should use their minor power after the AI has chosen a move, but before it is executed.  If the calculation specifies that the minor ability of the leader should be used, the chosen move is discarded in favor of using the minor ability.

For "Mercy", the key in determining if it should be used is whenever the next move would not continue any combo chains.  Making a move that does not include attacking a unit with combo points (stars) causes those combo points to disappear.  This is the perfect time to use Mercy, to cash in the otherwise wasted combo points into Gold.   The decision to use the ability is based on how many combo points would be converted into Gold.  We also tend towards not using this ability, as saving up mana to use for Blessing is also a good move.

        private int GetChanceToUseMercyPower(UnitMove um, GameState gs)
        {
            int numCombos = 0;

            // no chance if this does not extend combo
            if (!um.ExtendsCombo)
            {
                // get number of combo points that would be converted
                foreach (UnitStatus u in gs.Units)
                {
                    if (u.IsAlive)
                        numCombos += u.ComboPoints;
                }
            }
            
            // return chance based on num combos that would be converted
            return (numCombos * 25) - 40;
        }

        public bool UsePowerBeforeMove(Faction.LeaderSpecialEffect power, UnitMove proposedMove, GameState gs)
        {
            int useChance = 0;

            // determine chance to use power 
            switch (power)
            {
                case Faction.LeaderSpecialEffect.MERCY:
                    useChance = GetChanceToUseMercyPower(proposedMove, gs);
                    break;
            }

            return (useChance > _roller.Next(100));
        }

This updates the main Think() logic of our new AI player to the following:

        public override void Think(GameDetail gd, PlayerController pc)
        {
            // build influence map
            _influenceMap.CreateMap(gd.Map, gd.CurrentState);

            // is this the start of the turn?
            if (_lastTurn != gd.CurrentState.CurrentTurn)
            {
                StartOfTurn(gd);
                // check for use of Major leader power at turn start
                if (gd.CanAffordMajorSpell())
                {
                    if (_leaderPowerCalc.UsePowerAtTurnStart(gd.CurrentPlayer.LeaderMajorId, gd.CurrentState))
                    {
                        SubmitLeaderMajor(pc);
                        return;
                    }
                }
            }

            // get units to move
            List units = GetUnitsLeftToMove(pc.PlayerIndex, gd.CurrentState);

            // loop through available units to find best move
            UnitMove bestMove = null;
            foreach (UnitStatus u in units)
            {
                // get best move for this unit
                UnitMove thisMove = GetBestMoveForUnit(u, gd);
                // is this move better than our current best move?
                if (thisMove.IsBetter(bestMove))
                {
                    // set this as the new best move
                    bestMove = thisMove;
                }
            }

            // did we generate a move action?
            if (bestMove != null)
            {
                if (gd.CanAffordMinorSpell())
                {
                    // check for pre move minor power use
                    if (_leaderPowerCalc.UsePowerBeforeMove(gd.CurrentPlayer.LeaderMinorId, bestMove, gd.CurrentState))
                    {
                        SubmitLeaderMinor(pc);
                        return;
                    }
                }
                SubmitUnitMove(bestMove, gd, pc);
            }
            else
            {
                // check for end of turn power use
                if (gd.CanAffordMajorSpell())
                {
                    if (_leaderPowerCalc.UsePowerAtTurnEnd(gd.CurrentPlayer.LeaderMajorId, gd.CurrentState))
                    {
                        SubmitLeaderMajor(pc);
                        return;
                    }
                }

                // submit builds
                if (!SubmitBuildActions(gd, pc)) // returns false if did not build anything
                {
                    // nothing left to do except end our turn
                    SubmitEndTurn(pc);
                }
            }
        }

The results are telling, as Frank makes short work of Eva from last week, despite only changing the weight constant for Banding and allowing the use of Leader special abilities.


Continued in Part 7

No comments: