Monday, April 8, 2013

Programming a Turn Based Strategy Game AI part 10

Last time I introduced the base CommandModule class which all AI modules inherit, this week I wanted to show the detail behind the first of these modules, the first module in charge each turn, the StartTurnLeaderModule

In Shattered Throne, each player has a Leader character which has multiple spell like powers which can be used to turn the tide of battle in their favor.  In order to use one of these powers, the player must have accumulated enough mana to power the spell, represented by the amount of highlighted stars on the upper left status banner.

This module is fired off at the start of every turn, and is responsible for giving the command to use a power if the AI player has enough mana for the power, and determines it will be beneficial to do so at this time.  Not every power is suitable for use at the start of a turn, for instance, one power allows certain units which have already moved to move a second time, not at all useful at the start of a turn when no units have yet been moved. 

As such, only powers that are considered good start of turn canidates are even considered by the AI, as determined by the following function.


        private bool IsValidStartOfTurnPower(LeaderPowers.LeaderPower lp)
        {
            bool ret = false;

            switch (lp)
            {
                case LeaderPowers.LeaderPower.RALLY:
                case LeaderPowers.LeaderPower.CHARGE:
                case LeaderPowers.LeaderPower.UPRISING:
                case LeaderPowers.LeaderPower.DARKNESS:
                case LeaderPowers.LeaderPower.FEAR:
                case LeaderPowers.LeaderPower.INFECTION:
                case LeaderPowers.LeaderPower.DEADDANCE:
                case LeaderPowers.LeaderPower.LIVING_FOREST:
                case LeaderPowers.LeaderPower.BALANCE:
                case LeaderPowers.LeaderPower.TRANQUILITY:
                case LeaderPowers.LeaderPower.GOLD_FROM_ENEMY_SETTLEMENTS:
                case LeaderPowers.LeaderPower.EAGLEEYE:
                    ret = true;
                    break;
            }

            return ret;
        }

Each leader has two powers, a Minor and a Major.  Minor powers have a less powerful effect, but a smaller cost.  Because using a Minor power means using mana that could have been saved to later fuel a Major power, the algorithm favors using Major powers over Minor powers.  In addition, because a player cannot stockpile mana beyond the cost of their Major power, a Major power should almost always be used when it can be if there is any advantage gained in doing so.

These biases can be seen in the following code which is used to determine which power, if any, should be used.

            // pick best power that exceeds threshold
            if (majorScore > minorScore && majorScore >= 12)
            {
                ret = new AiAction();
                ret.ActionType = AiAction.AiActionType.USE_POWER;
                ret.TargetId = AiAction.MAJOR_POWER_ID;
            }
            else
            {
                if (minorScore >= 16)
                {
                    ret = new AiAction();
                    ret.ActionType = AiAction.AiActionType.USE_POWER;
                    ret.TargetId = AiAction.MINOR_POWER_ID;
                }
            }

One extremely useful design decision I made early on, was to have key game engine code used to determine battle outcomes, movement paths, and the effects of using a power to be their own self contained component, which did not actually change the game state.  Instead, each of these components generates a list of game changes as a result of the battle/power.

This has been very useful in the case of writing the AI, as I can use these components to compute the outcome of an action without actually causing the action, all while using the exact same code the game engine itself will use. 

Each power available is given a score, which is generated by passing the current game state to the LeaderPowers component, and then examining the generated outcomes.  This makes scoring each individual power more consistant, as we are scoring each individual effect generated independantly.

        private int ScoreLeaderPowerResults(List results, GameState gs)
        {
            UnitStatus u;
            int ret = 0;

            foreach (LeaderPowers.PowerResult pr in results)
            {
                switch (pr.Effect)
                {
                    //case LeaderPowers.PowerEffects.ADD_COMBO:
                    case LeaderPowers.PowerEffects.ADD_CONDITION:
                        ret += 3;
                        break;
                    case LeaderPowers.PowerEffects.CREATE_UNIT:
                        ret += 10;
                        break;
                    case LeaderPowers.PowerEffects.HEAL:
                    case LeaderPowers.PowerEffects.DAMAGE:
                        ret += pr.Value * 2;
                        break;
                    case LeaderPowers.PowerEffects.GAIN_GOLD:
                        ret += pr.Value;
                        break;
                    case LeaderPowers.PowerEffects.MOVE:
                        // need to rate the new space vs the old
                        // but for now, anytime we can screw with opponent's positioning is a good thing
                        ret += 2;
                        break;
                    case LeaderPowers.PowerEffects.READY_UNIT:
                        ret += 10;
                        break;
                    case LeaderPowers.PowerEffects.REMOVE_BAD_CONDITIONS:
                        u = gs.GetUnitAtLocation(pr.X, pr.Y);
                        if (u != null)
                        {
                            if (u.HasNegativeConditions)
                                ret += 3;
                        }
                        break;
                    case LeaderPowers.PowerEffects.REMOVE_GOOD_CONDITIONS:
                        u = gs.GetUnitAtLocation(pr.X, pr.Y);
                        if (u != null)
                        {
                            if (u.HasPositiveConditions)
                                ret += 3;
                        }
                        break;
                }
            }


            return ret;
        }

There is no special method behind the number score given to each possible effect.  Because a leader only has two powers, and usually one a single one that qualifies for start of turn use, I am just scoring the value of something happening, to make the mana cost of the power worthwhile.

Nothing terribly interesting going on, this is a pretty straightforward component.  Next time we will take a look at the much more interesting Tactical module in charge of combat decisions.  Until then, here is the full code listing for the StartTurnLeaderModule:

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;

namespace ShatteredThrone.AI.Modules
{
    class StartTurnLeaderModule : CommandModule
    {

        // state information
        bool _noMoreMoves;
        LeaderPowers _powerEngine;


        public StartTurnLeaderModule()
        {
            _noMoreMoves = true;
            _powerEngine = new LeaderPowers();
        }

        public override bool IsFinished
        {
            get { return _noMoreMoves; }
        }

        public override void Initialize()
        {
            _noMoreMoves = false;
        }

        public override AiAction GetNextMove(GameDetail gd)
        {
            AiAction ret = null;
            int majorScore = 0;
            int minorScore = 0;

            // check if leader can cast major power and it is worth using
            if (gd.CanAffordMajorSpell() && IsValidStartOfTurnPower(gd.CurrentPlayer.LeaderMajorId))
            {
                List results = _powerEngine.GetLeaderPowerUseResults(gd.CurrentPlayer.LeaderMajorId, gd.CurrentState, gd.Map);
                majorScore = ScoreLeaderPowerResults(results, gd.CurrentState);
            }
            // check minor
            if (gd.CanAffordMinorSpell() && IsValidStartOfTurnPower(gd.CurrentPlayer.LeaderMinorId))
            {
                List results = _powerEngine.GetLeaderPowerUseResults(gd.CurrentPlayer.LeaderMinorId, gd.CurrentState, gd.Map);
                minorScore = ScoreLeaderPowerResults(results, gd.CurrentState);
            }

            // pick best power that exceeds threshold
            if (majorScore > minorScore && majorScore >= 12)
            {
                ret = new AiAction();
                ret.ActionType = AiAction.AiActionType.USE_POWER;
                ret.TargetId = AiAction.MAJOR_POWER_ID;
            }
            else
            {
                if (minorScore >= 16)
                {
                    ret = new AiAction();
                    ret.ActionType = AiAction.AiActionType.USE_POWER;
                    ret.TargetId = AiAction.MINOR_POWER_ID;
                }
            }

            _noMoreMoves = true;
            return ret;
        }

        private bool IsValidStartOfTurnPower(LeaderPowers.LeaderPower lp)
        {
            bool ret = false;

            switch (lp)
            {
                case LeaderPowers.LeaderPower.RALLY:
                case LeaderPowers.LeaderPower.CHARGE:
                case LeaderPowers.LeaderPower.UPRISING:
                case LeaderPowers.LeaderPower.DARKNESS:
                case LeaderPowers.LeaderPower.FEAR:
                case LeaderPowers.LeaderPower.INFECTION:
                case LeaderPowers.LeaderPower.DEADDANCE:
                case LeaderPowers.LeaderPower.LIVING_FOREST:
                case LeaderPowers.LeaderPower.BALANCE:
                case LeaderPowers.LeaderPower.TRANQUILITY:
                case LeaderPowers.LeaderPower.GOLD_FROM_ENEMY_SETTLEMENTS:
                case LeaderPowers.LeaderPower.EAGLEEYE:
                    ret = true;
                    break;
            }

            return ret;
        }

        private int ScoreLeaderPowerResults(List results, GameState gs)
        {
            UnitStatus u;
            int ret = 0;

            foreach (LeaderPowers.PowerResult pr in results)
            {
                switch (pr.Effect)
                {
                    //case LeaderPowers.PowerEffects.ADD_COMBO:
                    case LeaderPowers.PowerEffects.ADD_CONDITION:
                        ret += 3;
                        break;
                    case LeaderPowers.PowerEffects.CREATE_UNIT:
                        ret += 10;
                        break;
                    case LeaderPowers.PowerEffects.HEAL:
                    case LeaderPowers.PowerEffects.DAMAGE:
                        ret += pr.Value * 2;
                        break;
                    case LeaderPowers.PowerEffects.GAIN_GOLD:
                        ret += pr.Value;
                        break;
                    case LeaderPowers.PowerEffects.MOVE:
                        // need to rate the new space vs the old
                        // but for now, anytime we can screw with opponent's positioning is a good thing
                        ret += 2;
                        break;
                    case LeaderPowers.PowerEffects.READY_UNIT:
                        ret += 10;
                        break;
                    case LeaderPowers.PowerEffects.REMOVE_BAD_CONDITIONS:
                        u = gs.GetUnitAtLocation(pr.X, pr.Y);
                        if (u != null)
                        {
                            if (u.HasNegativeConditions)
                                ret += 3;
                        }
                        break;
                    case LeaderPowers.PowerEffects.REMOVE_GOOD_CONDITIONS:
                        u = gs.GetUnitAtLocation(pr.X, pr.Y);
                        if (u != null)
                        {
                            if (u.HasPositiveConditions)
                                ret += 3;
                        }
                        break;
                }
            }


            return ret;
        }

    }
}

No comments: