Sunday, February 3, 2013

Turn Based Strategy Game AI part 9

It has been several months since I have last updated my pursuit of developing the AI for my turn based strategy game: Shattered Throne. One of the main reasons is that I felt I had reached the point in development in which the AI could not progress until I had finished defining all the game mechanics.

During this break, I also came to realize that my current AI was suffering from over complexity. I was trying to take into account so many different variables at once. I was trying to score combat moves at the same time I was scoring support moves and attempts to capture new territory. Trying to develop a method of scoring these distinct and separate areas on some common scale was difficult.

I had also been asked a few times, something I had initially had problems with. Other developers were not having any problems getting their AI to make good attack decisions, as long as the attack was one move away. The problem was getting the AI to move across the map, to accomplish goals which were impossible in a single move.

In fact, I have played more than one strategy game in which the AI just waits in place for the player to come into range, so this did not seem an uncommon problem either. I had been able to get the AI to move across the map, but sometimes it had some very odd behavior, mostly due I believe for trying to take too much into account for every move.

A new idea presented itself and I decided to give it a try. I ended up being quite happy with the results. The general idea was to break up the AI into different modules that each only cared about a certain type of goal.

I came up with the following modules:
  • Tactical:  Concerned only with making the best combat moves
  • Consolidation: Concerned only with protecting units and settlements from imediate counter attack on our opponent's next turn
  • Strategic: Deal with our long term goals of capturing settlements and moving towards enemy units
  • Logistic: Deals with creating new units
  • Leadership: Deals with when to use Leader powers
The newest AI player therefore, simply calls each of these modules in turn.  I ended up spliting the Leadership module into two specialties, one that checks at the start of the turn, and one that starts at the end.  A few of the leader powers also allow units to move again, so I added the ability for a module to inform the main AI to restart back at the first module.

The modules are run in the following order:
  1. Start Turn Leadership: Some leader powers are more suited for the beginning of the turn, this module specifically checks if the current game condition is appropriate for one of these powers, and fires off the command if they are
  2. Tactical: The AI then orders its units to make any strong attacks that are available
  3. Logistical: The AI builds new units (this is done prior to the Consolidation module because we can defend settlements by building a unit at that location
  4. Consolidation: We now analyze the game for any immediate threats to our units and settlements, and try to move any remaining units to protect these units
  5. Strategic: Any units we have which still can move are assigned to long term goals to capture settlements and move towards enemy units
  6. Logistical: We then circle back to see if we can build any units again, because one of our castles may have been blocked by our own unit, which has since been moved off the castle by the last two modules, allowing us to build at that location
  7. End Turn Leadership: Finally we check if the game conditions are ripe for any leader powers which work well at the end of the turn.  If so, we send the command to use them.

As such, our actual code for the latest in the series of AIs is relatvely simple, simply containing the mechanisms to keep track of which module is currently active, and transfer control to the next when appropriate.

    class Ishmael : ComputerBrain
    {
        List _commanders;

        // current state details
        int _currentTurn;
        int _currentModuleIndex;

        public Ishmael()
        {
            _currentTurn = -1;
            _currentModuleIndex = 0;

            _commanders = new List();

            _commanders.Add(new Modules.StartTurnLeaderModule());
            _commanders.Add(new Modules.TacticalModule());
            _commanders.Add(new Modules.LogisticsModule());
            _commanders.Add(new Modules.ConsolidationModule());
            _commanders.Add(new Modules.StrategicModule());
            _commanders.Add(new Modules.LogisticsModule());
            _commanders.Add(new Modules.EndTurnLeaderModule());
        }

        private bool CurrentModuleIsValid
        {
            get
            {
                return (_currentModuleIndex >= 0 && _currentModuleIndex < _commanders.Count) ?
                    (_commanders[_currentModuleIndex] != null) : false;
            }
        }

        private bool CurrentCommanderFinished
        {
            get
            {
                return (CurrentModuleIsValid) ? _commanders[_currentModuleIndex].IsFinished : true;
            }
        }

        private bool AllCommandersFinished
        {
            get
            {
                return (_currentModuleIndex >= _commanders.Count);
            }
        }

        private void MoveToNextCommander()
        {
            if (!AllCommandersFinished)
            {
                _currentModuleIndex++;
                InitializeCurrentCommander();
            }
        }

        private void InitializeCurrentCommander()
        {
            if (CurrentModuleIsValid)
            {
                _commanders[_currentModuleIndex].Initialize();
            }
        }

        private void SetupForNewTurn(GameDetail gd)
        {
            _currentTurn = gd.CurrentState.CurrentTurn;
            _currentModuleIndex = 0;
            InitializeCurrentCommander();
        }

        public override void Think(GameDetail gd, PlayerController pc)
        {
            // is this is a new turn
            if (_currentTurn != gd.CurrentState.CurrentTurn)
            {
                // reset state for new turn
                SetupForNewTurn(gd);
            }

            // is the current module finished?
            if (CurrentCommanderFinished)
            {
                // check property on commander that indicates we should go back
                //   to beginning of ai process for this turn
                if (CurrentModuleIsValid && _commanders[_currentModuleIndex].RestartFromBeginning)
                {
                    SetupForNewTurn(gd);
                }
                else
                {
                    MoveToNextCommander();
                }
            }
            
            // are all modules finished?
            if (AllCommandersFinished)
            {
                // submit end of turn
                SubmitEndTurn(pc);
            }
            else
            {
                // get next command from current module
                if (CurrentModuleIsValid)
                {
                    AiAction act = _commanders[_currentModuleIndex].GetNextMove(gd);
                    if (act != null)
                    {
                        SubmitAiAction(act, gd, pc);
                    }
                }
            }
        }

    }

I want to spend a blog post examing the guts of each of these modules in turn in the coming weeks.  Work is certainly not done, there are still some wrinkles and oddities to work on, but I feel good about where it is currently at, and hope to also get some feedback of other issues with the AI that needs to be addressed.

That is right, there is no video this time, because I am making the current build of the game available so that everyone can play with it themselves.  Check out how this latest AI perform, both where it does well and where it still has issues.

Click on the Shattered Throne tab at the top of this site to find out how to download and install the latest build.  If you have any comments, leave them below, or at the new forums, also accessed from the tabs at the top of the page.

Until next time!