Sunday, December 15, 2013

Unity sanity check

The last several months have seen me subsequently looking for new work and then settling down into a new position.  As such, work on game development was slow to non existent.  I originally hoped to release Shattered Throne onto XBLIG this summer, but that window has obviously come and gone.

Also during that time, two key things happened.  The first was the release of the Xbox One and the exit of XBLIG Community Games.  I really wanted to get Shattered Throne submitted well before this happened, but alas, it just didn't work out.  As such, one of the primary reasons for working in XNA has expired.

Seeing this on the horizon a while back, I had been keeping a pulse on the Unity game engine, seeing it as a likely engine to use in my next project.  And it was in early November that the second key event took place, one I had been eagerly awaiting, Unity released its own build in 2D support.

So one event was a disappointment, the other exciting.  One small lesson I have learned in being a hobbyist game developer, is that you need to keep excited in order to keep moving forward.  So I gave in and played around with the new Unity release.  First following a nice pong tutorial, and the next jumping in and trying to puzzle out a tower defense game (which my son had designed on paper).

Feeling pretty good about Unity, what it could do, and how to use it, I gave myself a short quest, spend 2 weeks moving Shattered Throne to Unity, and see how far I could get.  It was the obvious question that had been in the back of my mind after all.  I was very nervous however, as Shattered Throne has been in my head for many years, and inevitably started and stopped on each new platform/engine that struck my fancy.  I was so close to release, had put my own money into art assets, I could not afford to start over.

But I could not let it go, so I gave myself two weeks.  Prove I could reuse a lot of what I had already written, or realize I would have to basically start over.

Things started out on the right foot.  I had been very good in keeping my presentation and game engine layer separate, and within 3 hours, I had the entire game engine data objects and gameplay engine moved over and compiling in Unity.  Nothing was hooked up to the display or player controls, but I had all the pathfinding, rewind, gamestate management, levels, unit and map data, combat calculations, game systems and AI sitting there, ready to be used.

Then I started work on the game play display.  In the simple act of putting the map display together, I realized that the new Unity 2D tools were not going to work out for me.  While Unity has a really good automatic sprite sheet splitter built in, there seems to be no way to switch sprite images within these multi-sprites from within code.  Such as when building a map from data loaded from disk....

Sure, it is possible to create a prefab for every terrain type, and then assemble a map from those, but this would end up being a large number of prefabs when all the water edge and corner transition overlays were counted.  Also I was likely to have the same issue with the Units themselves.

Some research led me to the 2D Toolkit.  Turns out it has been updated to work with the new built in 2D unity objects, and still provides additional value, such as Sprite Atlasing (automatically assembling individual images into a single texture to reduce draw calls), a built in Tilemap object, and ability to dynamically change sprite images in code.  I made the purchase, and have been very pleased with the results.  The author is extremely responsive on his forums, answering questions from users extremely quick.



So the two weeks are gone...what is the end result?

Things have worked out quite well.  The game is mostly playable, there are still some rough or missing animations and effects, but you can move and attack with the units.  Also a few button hookups away from Ending a turn and undoing actions.  I had planned on redoing all the ingame menus and screens (such as building units) anyways, so I will not be losing time redoing those elements, and they will likely come together quicker due to the built in support such elements have in Unity, as well as the NGUI toolkit, which I purchased when it was on sale for a song.




So for now, I will continue to work in Unity, at least for the near future.  I must say that there is some relief in not worrying about getting it finished before XBLIG completely evaporates.  Using Unity also opens up several additional platforms, such as Linux, Mac, iOS, and PS4 which I am all interested in.  Of course, the biggest hurdle on releasing to additional environments is testing, which is already a difficult task to undertake with just a PC release.



Thursday, October 10, 2013

Silent but Busy

So much for the commitment to make regular updates...

Been quite busy on Shattered Throne.  I am happy to say that I think I found some keys to scenario design and game play tweaks that have really improved the game:


  • Updated unit movement speeds, slowing them down a bit to allow positioning to play a more central role
  • Added roads to allow units to move from the player's reserves to where they are needed quicker
  • Change to use campaign map to influence the design of a map, instead of completely define it.  As my campaign map only has major features shown, maps were turning out a bit boring, especially a lack of water tiles.  A simple mindset change to think of the campaign map as showing only large/major features (instead of everything) has lead to a lot more interesting maps.
  • Arrangement of terrain in clumps to create choke points.  This works well now that difficult terrain is much more restrictive to movement than before
  • Updates to scenarios to allow key locations to be defined and include more generic events and triggers to handle adding units in addition to conversations as before.  This has allowed for some more interesting victory conditions to be setup.
  • Scenarios now have an AI flavor, which can be used to turn off certain AI modules.  In particular, I often turn off my multi-turn strategic module such that ai units only react to your units when they move close.  While this seems like a step backwards, for certain scenarios, it allows for a different flow to the map that is sometimes desirable.


I have been avoiding finishing up my series of AI articles, as I am still making tweaks to the AI here and there.  But it is really feeling solid now, so should be back to wrap up that series of articles.

And just kicks, here is the list of checkin changes to the project over the last month or two:

2013-10-1016:11
2013-10-0916:22
2013-10-0816:52
2013-10-0716:48
2013-10-0620:16
2013-10-0516:45
2013-10-0217:04
2013-10-0115:16
15:15
2013-09-3015:50
2013-09-2016:45
2013-09-1920:22
2013-09-1817:39
2013-09-1717:26
2013-09-1618:42
2013-09-1117:01
2013-09-0215:52
2013-08-3016:20
2013-08-2916:17
2013-08-2617:45
2013-08-2314:45
2013-08-2022:23
2013-08-1622:15
2013-08-0713:38

Saturday, August 17, 2013

But is it any fun?

This is a question I have been asking myself a lot of lately.  Is the game any fun?  It can be a very difficult question to answer yourself, elbow deep in the guts, a tweak here, a kick there.  I have been building the campaign scenarios, and in play testing them, I have started to get very nervous.  Is it too complicated to make sense of?

That is a funny question to have, as the game is much less complex than the original plan, which included a global morale mechanic in addition to upgrade-able settlements.  There is one major mechanic that sets Shattered Throne apart from similar games, the combo star system, in which every attack against the same unit in a row becomes more powerful.  This idea was born a long time ago when I wanted to make a formation based war game that modeled units losing coherency as they engaged in battle, while at the same time being enamored of how playfully fun and engaging of a game Peggle was.  It seemed a perfect solution, and it has come to define Shattered Throne.

I think the Combo system as a whole is solid, what I am worrying about is all the other structures I built up around it.  Specifically the various unit traits and leader powers.  Each unit in Shattered Throne has 2 traits that make it unique.  A lot of these traits interact with the combo system and each other in significant ways.  It may be that the traits have grown too big, and will cloud the player's ability to make meaningful choices.  I must admit, that even as the game's designer, I sometimes cannot easily know how much damage a unit will take in an attack.  Now that is a rather large red flag.

So what am I to do, rip everything apart and find a better way to put it back together?  I really should, but I would lose a lot of momentum and risk the project never getting done.  Though I also do not want to release something I am not really proud of.  Again I might just be over reacting, being too close to the project.  But I do not feel as proud of the game as it currently stands as I did when dreaming it up.

What I have decided to do, is to finish up with the first pass of the campaign game, a task that has been very difficult.  Then get it some play testing, see what other people think.  I need some serious brutal honesty.  I think that I also need to play Advance Wars again, with a more analytic eye, pick out why exactly makes me love that game so much, and whether I am missing a key ingredient.

Friday, August 2, 2013

A little here, a little there

This is more of a stream of consciousness post, something a bit different, but something I want to try to do more of. It seems the longer I go without posting something, the more I feel that my blog post needs to be extra special, which is a bad feedback loop the ensures I do nothing at all. Hoping that making it a point to regularly post updates, even of small relevance will make it easier to keep up to date.

As far as this hobby goes, I typically work in cycles, in which I get a whole lot done on a project in a short time frame, and then it fades into the background for a while. I suppose I should at least be thankful that I have been able to suppress the urge to constantly start new projects, a tendency from the past that has sealed the fate of many half finished projects.

Shattered Throne had reached the point in development that I absolutely needed to nail the story down, something that has proven to be very painful to put together. I am not a story person. I know a lot of developers see the story as one of the primary areas that drives them to make games. Not this guy, I love tinkering with the game mechanics and getting the engine up and running.

I really wish I knew someone who was passionate about writing stories, and not so much into the mechanics. And major bonus points if they could draw and/or design UIs. As well as being both responsive, and relaxed about the project, seeing it as just a hobby like myself.

That said, I finally was able to put together a story script. I had a few requirements:

  • Must be concise, I hate endless walls of text to read through 
  • Must progress in such a way that the player has a chance to play each of the 3 factions against all the others in a roughly equal amount 
  • Must work around the Kingdom map I currently have, with battles spread out nicely 
  • Must not be too generic (or else why bother at all)
  • Must work in all the character and map art I already had commissioned and done (a big no no, the story should have been done first, but in my defense, I had a story before commissioning the artwork, it just ended up not working out when moving from outline to detail form. A lesson learned for the future)


So I finally got something working that mostly fits, sent it out for some feedback.  And it does not really appear to be exciting anyone.  I am starting to think that a generic approach might be best after all...

I also added in a slot machine mini game, because I needed a mini game and had a short obsession with Slot Machines and the desire to program one.  So took advantage of the desire to belt it out.

Who doesn't like mini games?

I have also turned some attention to balancing the factions, leaders, and units out.  Some of the comments I received from the beta build, was that the scenarios in which you build up an army by spending gold, didn't work out so well.  They just dragged on.  I increased the costs and power of the higher end units, such that instead of a cost range of 10g to 40g, after several rounds of tweaking, I ended up with a range of 10g to 100g, which feels better.  The higher end units are much rarer, and worth saving up for, but each unit retains its niche and purpose, and none ever become obsolete.

The change has also resulted in creating the type of game I was hoping for, one of large lines of infantry using formation to control the map.  Of course, it could just be my own desire for playing such a game that has biased me.

Sunday, April 21, 2013

Programming a Turn Based Strategy Game AI part 11

This time I want to take a look at the Tactical AI Module, which is responsible for making attacks. While quite an important module, this ended up being one of the most straight forward and simple modules to put together. It uses a simple brute force method that looks at every possible attack for every AI unit, and chooses the best move.

The general structure is simply:
  • Loop through all units able to move
    • Loop through every space this unit may move to
      • Loop through every valid target per space
        • Simulate the attack and score the results
      • Return the highest scoring attack action for this unit
    • Return the highest scoring attack action from all units
  • If the attack score is greater than a minimum threshold
    • Perform the action
  • Else
    • Signal that we have no more moves

If a valid attack move is generated, it will be performed, and then the whole process will start over again based on this updated state.

        public override AiAction GetNextMove(GameDetail gd)
        {
            AiAction ret = null;
            List possibleActions = GetUnitActions(gd.CurrentState, gd.Map);

            if (possibleActions.Count > 0)
            {
                float bestScore = MINIMAL_SCORE;
                foreach (AiAction aa in possibleActions)
                {
                    if (aa.ActionScore > bestScore)
                    {
                        bestScore = aa.ActionScore;
                        ret = aa;
                    }
                }
            }
            // if did not generate any moves, set done flag
            if(ret == null)
            {
                _noMoreMoves = true;
            }

            return ret;
        }

        private List GetUnitActions(GameState gs, GameMap gm)
        {
            List ret = new List();

            // get units to move
            List units = GetUnitsLeftToMove(gs.CurrentPlayer, gs);
            // loop through available units to find best move
            foreach (UnitStatus u in units)
            {
                // get best move for this unit
                AiAction act = GetBestMoveForUnit(u, gs, gm);
                // is this move better than our current best move?
                if (act != null)
                    ret.Add(act);
            }

            return ret;
        }

        private AiAction GetBestMoveForUnit(UnitStatus u, GameState gs, GameMap gm)
        {
            float currentBestScore = MINIMAL_SCORE; // always want to do something
            Point currentBestMoveLocation = new Point(u.X, u.Y); // by default go nowhere
            CombatAction currentBestCombatAction = new CombatAction(u.X, u.Y, CombatActionType.IDLE); // and do nothing

            // get all possible move locations for this unit
            List moveLocs = _pather.GetValidMoveLocations(u, gm, gs);
            foreach (Point loc in moveLocs)
            {
                // get all possible actions at this location
                List acts = _combat.GetCombatActionsByUnitAtLocation(u, gm, gs, loc);
                // loop through all possible actions at this location
                foreach (CombatAction ca in acts)
                {
                    // score this action
                    float actionScore = ScoreActionForUnit(loc, u, ca, gs, gm);
                    // is this our best score so far?
                    if (actionScore > currentBestScore)
                    {
                        // set this as our best possible move
                        currentBestScore = actionScore;
                        currentBestMoveLocation = loc;
                        currentBestCombatAction = ca;
                    }
                }
            }

            return new AiAction(AiAction.AiActionType.UNIT_MOVE, u.UnitID, currentBestMoveLocation, currentBestCombatAction, currentBestScore);
        }


As usual, it all comes down to how we score each possible attack.  Shattered Throne is a perfect information game (all players have complete knowledge of the current game state) and also features no randomness in combat.

This allows me to pass each attack the combat engine, which returns a list of outcomes.  Each outcome is then given a score, and the total score is returned.

I chose to base my scoring algorithm on the value of a single health point.  This is computed for each unit as that units total cost divided by the unit's max health value.  Non damage related effects that resulted from combat I tried to estimate their value best I could.

        private float ScoreAttackForUnit(UnitStatus attacker, Point attackLocation, Point targetLocation, GameMap m, GameState gs)
        {
            float ret = MINIMAL_SCORE;
            UnitStatus target;

            List results = _combat.GetAttackResults(attacker, attackLocation, targetLocation, m, gs);
            // loop through all returned results
            foreach (AttackResult ar in results)
            {
                // subtract damage caused to friends, add damage caused to enemies
                if (gs.AreFriendly(attacker.Owner, ar.TargetOwnerID))
                {
                    // will this kill ourselves?
                    if (!attacker.CanSurviveDamage(ar.Damage))
                    {
                        ret -= attacker.Cost;
                    }
                    else
                    {
                        ret -= (float)ar.Damage * attacker.UnitGoldToHpRatio;
                    }
                }
                else
                {
                    // will this kill the target?
                    target = gs.GetUnitAtLocation(ar.TargetLocation);
                    if (target != null)
                    {
                        if (!target.CanSurviveDamage(ar.Damage))
                        {
                            // reward killing blow
                            ret += target.Cost;
                            // if attacking unit has raise dead, score extra points
                            if (attacker.HasTrait(UnitTrait.RAISE_DEAD))
                            {
                                ret += 10;
                            }
                            // score points if attacking unit has rampage and will refresh
                            if (attacker.HasTrait(UnitTrait.RAGE))
                            {
                                ret += attacker.Cost / 3;
                            }
                        }
                        else
                        {
                            // award score based on damage caused to target
                            ret += (float)ar.Damage * target.UnitGoldToHpRatio;
                            // does this attack add any conditions?
                            if (ar.AddedCondition != null && ar.AddedCondition.IsNegative)
                            {
                                ret += 3;
                            }
                        }
                    }
                }
            }

            return ret;
        }


In addition to attack actions, several units have support actions which affect friendly units.  These are each dealt with in the same manner.

Empire priestess units can heal and remove negative conditions from friends:

        private float ScoreHealForUnit(UnitStatus actor, Point targetLoc, GameState gs)
        {
            float ret = MINIMAL_SCORE;

            // get target unit
            UnitStatus u = gs.GetUnitAtLocation(targetLoc);
            if (u != null)
            {
                // determine how much the unit is healed
                int amtHealed = (u.CurrentDamage < GameEngine.HEAL_AMOUNT_HEALED) ? u.CurrentDamage : GameEngine.HEAL_AMOUNT_HEALED;
                // multiply amount healed by unit hp to cost ratio to get amount this action scores
                ret = (float)amtHealed * u.UnitGoldToHpRatio;
                ret += u.NegativeConditionCount * 3;
            }

            return ret;
        }


Fey druids can grant a friendly unit regeneration over multiple turns:

        private float ScoreRegenForUnit(UnitStatus actor, Point targetLoc, GameState gs)
        {
            float ret = MINIMAL_SCORE;

            // get target unit
            UnitStatus u = gs.GetUnitAtLocation(targetLoc);
            if (u != null)
            {
                // no score if unit already has regeneration
                if (!u.HasCondition(Condition.ConditionType.REGEN))
                {
                    // score points based on how damage to unit, and it's value
                    ret = u.UnitGoldToHpRatio * u.CurrentDamage;
                }
            }

            return ret;
        }


Necromancer units can explode friendly zombies to cause damage to surrounding units, which can in turn then explode themselves if this defeats them, in a massive chain reaction.  As this is a much more complicated manuever, I have a special component to generate a list of resulting effects, which are then used to compute the total value of the action.

        private float ScoreZomboomForUnit(UnitStatus actor, Point targetLoc, GameMap gm, GameState gs)
        {
            float ret = MINIMAL_SCORE;
            UnitStatus boomUnit;

            // get target unit
            UnitStatus u = gs.GetUnitAtLocation(targetLoc);
            if (u != null)
            {
                // negative points for the unit being sacrificed
                ret = u.CurrentHP * u.UnitGoldToHpRatio * (-1);
                // get zomboom results
                List booms = _boomerManager.DoExplosion(targetLoc, gm, gs);
                // tally up score from result
                foreach (ZomboomActionItem zai in booms)
                {
                    if (zai.BoomEffect == ZomboomActionItem.BoomEffects.DAMAGE)
                    {
                        boomUnit = gs.GetUnitAtLocation(zai.Location);
                        if (boomUnit != null)
                        {
                            ret += zai.Value * boomUnit.UnitGoldToHpRatio;
                        }
                    }
                } // next boom
            }

            return ret;
        }


The usefulness of having each game engine component generate a list of effects, instead of updating the game state themselves cannot be understated.  It is extremely handy using the same routines the game engine itself uses.  It also means consistent results.

Most strategy games have random elements to combat results.  To score such, the combat should be run multiple times and the results collated into an average expected outcome.  Another method would be to compute the average outcome by multiplying the effect by the chance of that effect happening.  Such that a unit that had a 60% to hit for 1d8+1 damage would have the average effect of dealing 3.3 damage.

Average value of 1d8: (8 + 1) / 8 = 4.5
Average value of a successful hit:  <avg value of 1d8> + 1 = 5.5
Average value of an attack: <avg value of success> * <chance of success> = 5.5 * .60 = 3.3

The disadvantage of computing the average in this manner is that it can be inaccurate because of the effects of the extremes.  For example, imagine in the example above, that the target had armor that reduced all damage against it by 6.  Taking this into account, the above calculation would generate an average damage value of 0, which is incorrect, as we would actually still score damage if the d8 roll was 6+.

Looking through the code presented here, it is obvious it is still a bit rough.  For instance, there is no consideration given to the position a unit ends up in, just that the move has a net positive effect.  I have noticed this AI send a unit into a very dangerous situation just to finish off a low cost unit.

Another option I have not talked about, but which I have been thinking a lot about, is looking at multiple moves ahead.  Especially with the Combo star system in Shattered Throne, in which each attack makes any followup attack against the same unit more effective.

It would be ideal to have the AI not only compute the best move for each unit, but think one or more moves ahead.  For each potential move, I could then get the best follow up move, and then score both moves together.  This would improve the AI's intelligence quite a bit.

I may add in this capability in the end, but for now I find myself hesitating.  My goal is not to create an unbeatable AI, but rather a fun game.  In writing my first game, Dark Delve, I found that with each version I got lots of comments that the game was too hard.  I found myself consistently dialing back the difficulty to find the proper level of difficulty.

I worry about the same thing here, especially as I have been finding it difficult to explain the Combo Stars game mechanic that is central to the whole game.  It might just be right to let the player who spends the time to figure out that game system to be rewarded with an advantage the AI does not fully take advantage of, rather than being pounded into the dirt right from the start by this rather confusing game element.

In the end I want the AI to put up a good fight, and just not make any obviously stupid or nonsensical moves.

Next time I will introduce the module that may not make the final cut, the Consolidation Module.

And as usual, here is the complete code listing of the TacticalModule.

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

namespace ShatteredThrone.AI.Modules
{
    class TacticalModule : CommandModule
    {
        Pathfinder _pather;
        CombatEngine _combat;
        Zomboom _boomerManager;

        // state information
        bool _noMoreMoves;

        const float MINIMAL_SCORE = 0f;

        public TacticalModule()
        {
            _pather = new Pathfinder();
            _combat = new CombatEngine();
            _boomerManager = new Zomboom();

            _noMoreMoves = true;
        }

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

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

        public override AiAction GetNextMove(GameDetail gd)
        {
            AiAction ret = null;
            List possibleActions = GetUnitActions(gd.CurrentState, gd.Map);

            if (possibleActions.Count > 0)
            {
                float bestScore = MINIMAL_SCORE;
                foreach (AiAction aa in possibleActions)
                {
                    if (aa.ActionScore > bestScore)
                    {
                        bestScore = aa.ActionScore;
                        ret = aa;
                    }
                }
            }
            // if did not generate any moves, set done flag
            if(ret == null)
            {
                _noMoreMoves = true;
            }

            return ret;
        }

        private List GetUnitActions(GameState gs, GameMap gm)
        {
            List ret = new List();

            // get units to move
            List units = GetUnitsLeftToMove(gs.CurrentPlayer, gs);
            // loop through available units to find best move
            foreach (UnitStatus u in units)
            {
                // get best move for this unit
                AiAction act = GetBestMoveForUnit(u, gs, gm);
                // is this move better than our current best move?
                if (act != null)
                    ret.Add(act);
            }

            return ret;
        }

        private AiAction GetBestMoveForUnit(UnitStatus u, GameState gs, GameMap gm)
        {
            float currentBestScore = MINIMAL_SCORE; // always want to do something
            Point currentBestMoveLocation = new Point(u.X, u.Y); // by default go nowhere
            CombatAction currentBestCombatAction = new CombatAction(u.X, u.Y, CombatActionType.IDLE); // and do nothing

            // get all possible move locations for this unit
            List moveLocs = _pather.GetValidMoveLocations(u, gm, gs);
            foreach (Point loc in moveLocs)
            {
                // get all possible actions at this location
                List acts = _combat.GetCombatActionsByUnitAtLocation(u, gm, gs, loc);
                // loop through all possible actions at this location
                foreach (CombatAction ca in acts)
                {
                    // score this action
                    float actionScore = ScoreActionForUnit(loc, u, ca, gs, gm);
                    // is this our best score so far?
                    if (actionScore > currentBestScore)
                    {
                        // set this as our best possible move
                        currentBestScore = actionScore;
                        currentBestMoveLocation = loc;
                        currentBestCombatAction = ca;
                    }
                }
            }

            return new AiAction(AiAction.AiActionType.UNIT_MOVE, u.UnitID, currentBestMoveLocation, currentBestCombatAction, currentBestScore);
        }

        private float ScoreActionForUnit(Point loc, UnitStatus u, CombatAction ca, GameState gs, GameMap gm)
        {
            float ret = MINIMAL_SCORE;
            
            switch (ca.ActionType)
            {
                case CombatActionType.ATTACK:
                    ret = ScoreAttackForUnit(u, loc, ca.Location, gm, gs);
                    break;
                case CombatActionType.HELP:
                    // is this a zomboom?
                    switch (u.FriendSpell)
                    {
                        case FriendlyTargetEffect.ZOMBOOM:
                            ret = ScoreZomboomForUnit(u, ca.Location, gm, gs);
                            break;
                        case FriendlyTargetEffect.REJUVINATE:
                            ret = ScoreRegenForUnit(u, ca.Location, gs);
                            break;
                        case FriendlyTargetEffect.HEAL:
                            ret = ScoreHealForUnit(u, ca.Location, gs);
                            break;
                    } // end help inner switch
                    break;
            } // end switch

            return ret;
        }

        private float ScoreHealForUnit(UnitStatus actor, Point targetLoc, GameState gs)
        {
            float ret = MINIMAL_SCORE;

            // get target unit
            UnitStatus u = gs.GetUnitAtLocation(targetLoc);
            if (u != null)
            {
                // determine how much the unit is healed
                int amtHealed = (u.CurrentDamage < GameEngine.HEAL_AMOUNT_HEALED) ? u.CurrentDamage : GameEngine.HEAL_AMOUNT_HEALED;
                // multiply amount healed by unit hp to cost ratio to get amount this action scores
                ret = (float)amtHealed * u.UnitGoldToHpRatio;
                ret += u.NegativeConditionCount * 3;
            }

            return ret;
        }

        private float ScoreRegenForUnit(UnitStatus actor, Point targetLoc, GameState gs)
        {
            float ret = MINIMAL_SCORE;

            // get target unit
            UnitStatus u = gs.GetUnitAtLocation(targetLoc);
            if (u != null)
            {
                // no score if unit already has regeneration
                if (!u.HasCondition(Condition.ConditionType.REGEN))
                {
                    // score points based on how damage to unit, and it's value
                    ret = u.UnitGoldToHpRatio * u.CurrentDamage;
                }
            }

            return ret;
        }

        private float ScoreZomboomForUnit(UnitStatus actor, Point targetLoc, GameMap gm, GameState gs)
        {
            float ret = MINIMAL_SCORE;
            UnitStatus boomUnit;

            // get target unit
            UnitStatus u = gs.GetUnitAtLocation(targetLoc);
            if (u != null)
            {
                // negative points for the unit being sacrificed
                ret = u.CurrentHP * u.UnitGoldToHpRatio * (-1);
                // get zomboom results
                List booms = _boomerManager.DoExplosion(targetLoc, gm, gs);
                // tally up score from result
                foreach (ZomboomActionItem zai in booms)
                {
                    if (zai.BoomEffect == ZomboomActionItem.BoomEffects.DAMAGE)
                    {
                        boomUnit = gs.GetUnitAtLocation(zai.Location);
                        if (boomUnit != null)
                        {
                            ret += zai.Value * boomUnit.UnitGoldToHpRatio;
                        }
                    }
                } // next boom
            }

            return ret;
        }

        // TODO: Give bonus points for mana gained via kill
        private float ScoreAttackForUnit(UnitStatus attacker, Point attackLocation, Point targetLocation, GameMap m, GameState gs)
        {
            float ret = MINIMAL_SCORE;
            UnitStatus target;

            List results = _combat.GetAttackResults(attacker, attackLocation, targetLocation, m, gs);
            // loop through all returned results
            foreach (AttackResult ar in results)
            {
                // subtract damage caused to friends, add damage caused to enemies
                if (gs.AreFriendly(attacker.Owner, ar.TargetOwnerID))
                {
                    // will this kill ourselves?
                    if (!attacker.CanSurviveDamage(ar.Damage))
                    {
                        ret -= attacker.Cost;
                    }
                    else
                    {
                        ret -= (float)ar.Damage * attacker.UnitGoldToHpRatio;
                    }
                }
                else
                {
                    // will this kill the target?
                    target = gs.GetUnitAtLocation(ar.TargetLocation);
                    if (target != null)
                    {
                        if (!target.CanSurviveDamage(ar.Damage))
                        {
                            // reward killing blow
                            ret += target.Cost;
                            // if attacking unit has raise dead, score extra points
                            if (attacker.HasTrait(UnitTrait.RAISE_DEAD))
                            {
                                ret += 10;
                            }
                            // score points if attacking unit has rampage and will refresh
                            if (attacker.HasTrait(UnitTrait.RAGE))
                            {
                                ret += attacker.Cost / 3;
                            }
                        }
                        else
                        {
                            // award score based on damage caused to target
                            ret += (float)ar.Damage * target.UnitGoldToHpRatio;
                            // does this attack add any conditions?
                            if (ar.AddedCondition != null && ar.AddedCondition.IsNegative)
                            {
                                ret += 3;
                            }
                        }
                    }
                }
            }

            return ret;
        }

    }
}

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

    }
}