Artificial intelligence and more

Recently I had a few inquiries whether I could say more about how the AI is constructed in practical terms.

In principle it is a vast topic that has as its top the logical decision making, the sequencing of events or interactions that make up an entity's behaviour. But before this can happen many layers of information have to be processed and made accessible.

The Quantomas AI doesn't use neural networks, supercomputers or machine learning, instead we use maths and logic to build large networks of interconnected data which we evaluate with well defined algorithms.

Needless to say, any reasonably complex scenario, like the maps on which Heroes games play out, creates abundant amounts of data and processing it efficiently is of paramount importance.

A large part of this processing efficiency comes from the AI itself by smartly focusing on the most critical parts of the situation at hand. But there is no way around it, to arrive at this stage we have first to evaluate the data that describes the situation.

Most real-world scenarios, and anything that resembles it to some extent like a Heroes map, have to deal with spatial relations, i.e. the distance between objects, which requires means to measure all kinds of distances. Once you have to deal with hundreds or thousands of objects as in Heroes, it becomes a critical part of your application to do this as efficiently as possible.

For a game like Heroes this function is performed by a pathfinder. This task is more complicated than simply deducing the distance between objects by its coordinates on the map. It's like in the real world, there are obstacles that block your path, some of which you can remove and others you need to walk around, different types of terrain that can be traversed at different speeds and so on. With other words we need a solution that determines the time you need to travel between two objects on a case by case basis, i.e. each path is different.

Heroes III and Heroes V have already competent pathfinders, but when I began working on the AI for Heroes V I realized that for a fast and efficient AI we need a much faster pathfinder that can process the distances between any two objects on the map in nearly constant time.

What's covered here

In terms of the architecture of the entire AI the strategic decision making sits at the top, below it is the sequencing of events to figure out paths that serve the strategic goals, which in turn depends on the pathfinder to supply the information which objects are accessible and how long it takes to reach these, and at the bottom are a lot of basic but vital functions that retrieve, assemble and efficiently structure the data needed for all these tasks.

Today we start with an overview how the data structures necessary for a powerful pathfinder can be implemented. I will also explain how you can make good use of C++ to get this work done and give you an idea how you can structure your programs.

Once we have this firmly in place, we can move on to the next topic, how the pathfinder in Heroes III was implemented.

This first part covers efficient data structures. When I wrote this article and then reviewed it, I realized that the following code samples and my brief explanations aren't easily approachable for beginners. It's tough reading and you have to make yourself familiar with a lot of concepts in a very compact format. But you can learn a lot from this, techniques, design and coding practice, but most importantly it is a good preparation to building a professional pathfinder. If you are a pro or seasoned C++ programmer the concepts should be straightforward to grasp.

So bear with me if you like to know more about the internals of the pathfinder used in connection with the advanced AI.

Structured data

Ideally we want to write code in such a style that it is self-documenting and requires minimal auxiliary comment.

Let's start with a simple structure that defines the skills of a hero, how these are handled internally by the AI, in Heroes V.

/*----------------------------------------------------------------
                    struct HeroSkills
------------------------------------------------------------------*/
public:
    struct HeroSkills
        {
        union
            {
            unsigned long       flags ;
            struct {
                unsigned        nativeTerrain      : 8 ;
                unsigned        heroLevel          : 8 ;
                unsigned        bIsPathfinder      : 1 ;
                unsigned        bIsTracker         : 1 ;
                unsigned        bHasSnatchSkill    : 1 ;
                unsigned        bHasWayfarerBoots  : 1 ;
                unsigned        bBattleBonusMove   : 1 ;
                unsigned        bKnowsMysticism    : 1 ;
                unsigned        bCanCastTownPortal : 1 ;
                unsigned        bCanCastTeleport   : 1 ;
                unsigned        bCanSummonBoat     : 1 ;
                unsigned        bCanCastAnySpells  : 1 ;
                unsigned        maxSpellLevel      : 3 ;
                unsigned        unused             : 3 ;
                };
            };

        unsigned short          nDimensionDoorMoveCost ;
        unsigned short          nMovePointsMaxCurrent ;
        long                    heroPowerBase ;

        HeroSkills() : flags(0) {}
#ifndef STRATEGIC_AI_PROCESS
        HeroSkills( IHero* hero ) { init( hero ); }

        /*============== public methods ==============*/

        void init( IHero* hero )
            {
            flags = 0 ;

            if( ! IsValid( hero ) ) return ;

            const SSpellInfo dimensionDoor( NDb::SPELL_DIMENSION_DOOR );
            const SSpellInfo summonBoat( NDb::SPELL_SUMMON_BOAT );
            const SSpellInfo townPortal( NDb::SPELL_TOWN_PORTAL );

            nativeTerrain          = 1 << MoveCost::getNativeTerrain( hero );
            heroLevel              = hero->GetLevel() ;
            heroPowerBase          = cHeroPowerBase + ( heroLevel - 1 ) * cHeroPowerIncPerLevel ;
            bIsPathfinder          = hero->GetSkillTrained( NDb::HERO_SKILL_PATHFINDING ) > NWorld::SKILL_MASTERY_NONE ;
            bIsTracker             = hero->HasSpecialization( NDb::HERO_SPEC_RASH );
            bHasSnatchSkill        = hero->GetSkillTrained( NDb::HERO_SKILL_SNATCH ) > NWorld::SKILL_MASTERY_NONE ;
            bHasWayfarerBoots      = hero->GetPutonArtifacts().Has( NDb::WAYFARER_BOOTS );
            bBattleBonusMove       = hero->GetSkillTrained( NDb::HERO_SKILL_PATH_OF_WAR ) > NWorld::SKILL_MASTERY_NONE ;
            bKnowsMysticism        = hero->GetSkillTrained( NDb::HERO_SKILL_MYSTICISM ) > NWorld::SKILL_MASTERY_NONE ;
            bCanCastTeleport       = hero->IsSpellKnown( dimensionDoor.eSpell ) && hero->CanCastSpell( dimensionDoor );
            nDimensionDoorMoveCost = bCanCastTeleport ? hero->GetMovePointsMaxCurrent() / 2 : cMaxStateDistance ;
            bCanSummonBoat         = hero->IsSpellKnown( summonBoat.eSpell ) && hero->CanCastSpell( summonBoat );
            bCanCastTownPortal     = hero->IsSpellKnown( townPortal.eSpell );
            bCanCastAnySpells      = ! hero->GetSpells().empty() ;
            nMovePointsMaxCurrent  = hero->GetMovePointsMaxCurrent() ;

            setMaxSpellLevel( hero );
            }

        void setMaxSpellLevel( IHero* hero )
            {
            maxSpellLevel = 2 ;

            if( hero->GetSkillTrained( NDb::HERO_SKILL_WISDOM ) )
                maxSpellLevel = 3 ;

            updateMaxSpellLevel( hero, NDb::HERO_SKILL_DESTRUCTIVE_MAGIC );
            updateMaxSpellLevel( hero, NDb::HERO_SKILL_DARK_MAGIC );
            updateMaxSpellLevel( hero, NDb::HERO_SKILL_LIGHT_MAGIC );
            updateMaxSpellLevel( hero, NDb::HERO_SKILL_SUMMONING_MAGIC );

            if( maxSpellLevel < 3 && (NDb::TOWN_STRONGHOLD == hero->GetHeroTown())  )
                maxSpellLevel = 1 ;
            }

        void updateMaxSpellLevel( IHero* hero, NDb::ESkillID eMagicSchool )
            {
            unsigned char nSchoolSpellLevel = 2 + hero->GetSkillTrained( eMagicSchool );

            if( maxSpellLevel < nSchoolSpellLevel )
                maxSpellLevel = nSchoolSpellLevel ;
            }
#endif
        }; // struct HeroSkills

This is a compact struct to maintain relevant data efficiently in a single place and make the code more reliable and readable. The preprocessor constant STRATEGIC_AI_PROCESS allows the same code to be used in two processes, of which only one provides the context for initialization while the second uses the data. Such a struct can be passed between processes.

It also demonstrates that in C++ you can store data with the efficiency of assembler while still maintaining well structured code.

If you are familiar with the game, you will note that the HeroSkills struct holds most of the information a pathfinder needs to calculate the movement costs for a hero to move across the map, amongst other things the nativeTerrain and bIsPathfinder flag.

The next example demonstrates techniques that you can use to process the movement cost on a tile based map. These are typical techniques to define the movement mechanics independently of the pathfinder itself.

/*----------------------------------------------------------------
                    struct TMoveCost
------------------------------------------------------------------*/

    enum TerrainClass {
        __plain = 1,
        __dirt = 2,
        __lava = 4,
        __sand = 8,
        __snow = 16,
        __subterranean = 32,
        __taiga = 64,
        __road = 128
        };

    enum TerrainClassIndex {
        __plain__ = 0,
        __dirt__,
        __lava__,
        __sand__,
        __snow__,
        __subterranean__,
        __taiga__,
        __road__,
        __terrainCnt__
        };

    static const unsigned short cTerrainMask = 0x00ff ;


    enum Directions { N = 1, NE = 2, E = 4, SE = 8, S = 16, SW = 32, W = 64, NW = 128 };

    static const unsigned int north            = NW | N | NE ;
    static const unsigned int east             = NE | E | SE ;
    static const unsigned int south            = SE | S | SW ;
    static const unsigned int west             = SW | W | NW ;

    static const unsigned long neighborBits    = 0x000000ff ;


    struct TMoveCost
        {
        unsigned short  moveCost ;

        /*============== construction ==============*/

        TMoveCost()
            {
            }

        TMoveCost( unsigned short _moveCost )
            {
            moveCost = _moveCost ;
            }

        /*============== public methods ==============*/

        void clear()
            {
            moveCost = 0 ;
            }

        TMoveCost& operator+= ( const TMoveCost& _m )
            {
            moveCost += _m.moveCost ;
            return *this ;
            }

        TMoveCost& operator= ( unsigned short _moveCost )
            {
            moveCost = _moveCost ;
            return *this ;
            }

        TMoveCost operator+ ( const TMoveCost& _m )
            {
            TMoveCost tmp = _m ;
            return tmp += *this ;
            }

        bool Improve( TMoveCost& improved )
            {
            if( improved.moveCost < moveCost )
                {
                moveCost = improved.moveCost ;
                return true ;
                }

            return false ;
            }

        void adjustMinimumDistance()
            {
            if( ! moveCost ) moveCost = 32 ;
            }

        unsigned char moveCost64() const
            {
            return moveCost64( moveCost );
            }

        static unsigned char moveCost64( unsigned short c )
            {
            if( c & 0xc000 ) return 255 ;
            else if( c & 0x2000 );
            else c += 32 ;
            return c >> 6 ;
            }

        static unsigned short fullMoveCost( unsigned char c )
            {
            return c << 6 ;
            }

        void dump() const
            {
            aiLogger() << (int)moveCost << endl ;
            }

        }; // struct TMoveCost


/*----------------------------------------------------------------
                    struct MoveCost
------------------------------------------------------------------*/

    struct MoveCost
        {
        union
            {
            unsigned long       flags ;
            struct {
                unsigned        pathThroughNeighbor : 16 ;
                unsigned        nShortestPath       : 8 ;
                unsigned        zeroThreatPath      : 1 ;
                unsigned        noTriggersInPath    : 1 ;
                unsigned        unused              : 6 ;
                };
            };

        TMoveCost       m[ __terrainCnt__ ];

        /*============== static const & variables ==============*/

        static MoveCost nilMoveCost ;

        /*============== construction ==============*/

        MoveCost() {}
        MoveCost( unsigned short c, unsigned long _flags = 0 ) { flags = _flags ; set( c ); }

        /*============== static methods ==============*/

#ifndef STRATEGIC_AI_PROCESS
        static TerrainClassIndex getNativeTerrain( IHero* hero )
            {
            if( ! IsValid( hero ) ) return __plain__ ;

            TerrainClassIndex nativeTerrain ;

            switch( hero->GetInfo().eTownType )
                {
                case NDb::TOWN_ACADEMY:
                    nativeTerrain = __sand__ ;
                    break;
                case NDb::TOWN_DUNGEON:
                    nativeTerrain = __subterranean__ ;
                    break;
                case NDb::TOWN_NECROMANCY:
                    nativeTerrain = __dirt__ ;
                    break;
                case NDb::TOWN_INFERNO:
                    nativeTerrain = __lava__ ;
                    break;
                case NDb::TOWN_FORTRESS:
                    nativeTerrain = __snow__ ;
                    break;
                case NDb::TOWN_STRONGHOLD:
                    nativeTerrain = __taiga__ ;
                    break;
                case NDb::TOWN_HEAVEN:
                case NDb::TOWN_PRESERVE:
                default:
                    nativeTerrain = __plain__ ;

                } // switch( hero->GetInfo().eTownType )

            return nativeTerrain ;
            }
#endif
        /*============== public methods ==============*/

        void assign(
            MoveCost&       accumulated,
            unsigned char   terrain,
            unsigned char   iNeighbor
            ) {
            bool            bShortMove = iNeighbor & ( N | E | S | W ) ;

            TMoveCost terrainMoveCost( bShortMove ? getMoveCost( terrain ) : getMoveCostDiag( terrain ) );
            TMoveCost nativeMoveCost( bShortMove ? 100 : 141 );

            m[__plain__]        = accumulated.m[__plain__]        +  terrainMoveCost ;
            m[__dirt__]         = accumulated.m[__dirt__]         + (terrain & __dirt         ? nativeMoveCost : terrainMoveCost) ;
            m[__lava__]         = accumulated.m[__lava__]         + (terrain & __lava         ? nativeMoveCost : terrainMoveCost) ;
            m[__sand__]         = accumulated.m[__sand__]         + (terrain & __sand         ? nativeMoveCost : terrainMoveCost) ;
            m[__snow__]         = accumulated.m[__snow__]         + (terrain & __snow         ? nativeMoveCost : terrainMoveCost) ;
            m[__subterranean__] = accumulated.m[__subterranean__] + (terrain & __subterranean ? nativeMoveCost : terrainMoveCost) ;
            m[__taiga__]        = accumulated.m[__taiga__]        + (terrain & __taiga        ? nativeMoveCost : terrainMoveCost) ;
            m[__road__]         = accumulated.m[__road__]         + (terrain & ~__road        ? nativeMoveCost : terrainMoveCost) ;

            nShortestPath = accumulated.nShortestPath + 1 ;
            }

        unsigned char add(
            MoveCost&       accumulated,
            unsigned char   terrain,
            unsigned char   iNeighbor,
            unsigned char   active
            ) {
            bool            bShortMove = iNeighbor & ( N | E | S | W ) ;

            TMoveCost terrainMoveCost( bShortMove ? getMoveCost( terrain ) : getMoveCostDiag( terrain ) );
            TMoveCost nativeMoveCost( bShortMove ? 100 : 141 );

            if( active & __plain
            &&  ! m[__plain__].Improve( accumulated.m[__plain__]  + terrainMoveCost ) )
                active &= ~__plain ;

            if( active & __dirt
            &&  ! m[__dirt__].Improve( accumulated.m[__dirt__]  + (terrain & __dirt  ? nativeMoveCost : terrainMoveCost) ) )
                active &= ~__dirt ;

            if( active & __lava
            &&  ! m[__lava__].Improve( accumulated.m[__lava__]  + (terrain & __lava  ? nativeMoveCost : terrainMoveCost) ) )
                active &= ~__lava ;

            if( active & __sand
            &&  ! m[__sand__].Improve( accumulated.m[__sand__]  + (terrain & __sand  ? nativeMoveCost : terrainMoveCost) ) )
                active &= ~__sand ;

            if( active & __snow
            &&  ! m[__snow__].Improve( accumulated.m[__snow__]  + (terrain & __snow  ? nativeMoveCost : terrainMoveCost) ) )
                active &= ~__snow ;

            if( active & __subterranean
            &&  ! m[__subterranean__].Improve( accumulated.m[__subterranean__]
                                             + (terrain & __subterranean  ? nativeMoveCost : terrainMoveCost) ) )
                active &= ~__subterranean ;

            if( active & __taiga
            &&  ! m[__taiga__].Improve( accumulated.m[__taiga__]  + (terrain & __taiga  ? nativeMoveCost : terrainMoveCost) ) )
                active &= ~__taiga ;

            if( active & __road )
                {
                if( m[__road__].Improve( accumulated.m[__road__]  + (terrain & ~__road  ? nativeMoveCost : terrainMoveCost) ) )
                    nShortestPath = accumulated.nShortestPath + 1 ;
                else
                    active &= ~__road ;
                }

            return active ;
            }

        void assign(
            MoveCost&       accumulated,
            MoveCost&       distance
            ) {
            m[__plain__]        = accumulated.m[__plain__]        +  distance.m[__plain__] ;
            m[__dirt__]         = accumulated.m[__dirt__]         +  distance.m[__dirt__] ;
            m[__lava__]         = accumulated.m[__lava__]         +  distance.m[__lava__] ;
            m[__sand__]         = accumulated.m[__sand__]         +  distance.m[__sand__] ;
            m[__snow__]         = accumulated.m[__snow__]         +  distance.m[__snow__] ;
            m[__subterranean__] = accumulated.m[__subterranean__] +  distance.m[__subterranean__] ;
            m[__taiga__]        = accumulated.m[__taiga__]        +  distance.m[__taiga__] ;
            m[__road__]         = accumulated.m[__road__]         +  distance.m[__road__] ;

            nShortestPath    = accumulated.nShortestPath +  distance.nShortestPath ;
            }

        unsigned char add(
            MoveCost&       accumulated,
            MoveCost&       distance,
            unsigned char   active
            ) {

            if( active & __plain
            &&  ! m[__plain__].Improve( accumulated.m[__plain__]  +  distance.m[__plain__] ) )
                active &= ~__plain ;

            if( active & __dirt
            &&  ! m[__dirt__].Improve( accumulated.m[__dirt__]    +  distance.m[__dirt__] ) )
                active &= ~__dirt ;

            if( active & __lava
            &&  ! m[__lava__].Improve( accumulated.m[__lava__]    +  distance.m[__lava__] ) )
                active &= ~__lava ;

            if( active & __sand
            &&  ! m[__sand__].Improve( accumulated.m[__sand__]    +  distance.m[__sand__] ) )
                active &= ~__sand ;

            if( active & __snow
            &&  ! m[__snow__].Improve( accumulated.m[__snow__]    +  distance.m[__snow__] ) )
                active &= ~__snow ;

            if( active & __subterranean
            &&  ! m[__subterranean__].Improve( accumulated.m[__subterranean__]  +  distance.m[__subterranean__] ) )
                active &= ~__subterranean ;

            if( active & __taiga
            &&  ! m[__taiga__].Improve( accumulated.m[__taiga__]  +  distance.m[__taiga__] ) )
                active &= ~__taiga ;

            if( active & __road )
                {
                if( m[__road__].Improve( accumulated.m[__road__]  +  distance.m[__road__] ) )
                    nShortestPath = accumulated.nShortestPath  +  distance.nShortestPath ;
                else
                    active &= ~__road ;
                }

#ifdef STRATEGIC_AI_DBG_MOVECOST
            aiLogger() << " ~" << print8Bit( active ) << "~ ";
#endif
            return active ;
            }

        void assignZeroThreat(
            unsigned char       _nShortestPath,
            unsigned short      distance,
            TerrainClassIndex   _terrain_
            ) {
            m[_terrain_].moveCost = distance ; 
            zeroThreatPath        = true ;
            nShortestPath         = _nShortestPath ;
            }

        bool addZeroThreat(
            unsigned char       _nShortestPath,
            unsigned short      distance,
            TerrainClassIndex   _terrain_
            ) {
            if( distance < m[_terrain_].moveCost )
                {
                m[_terrain_].moveCost = distance ;
                zeroThreatPath        = true ;
                nShortestPath         = _nShortestPath ;
                return true ;
                }
            return false ;
            }

        static unsigned short getHeroMoveCost( unsigned char terrain, bool bShortMove, HeroSkills heroSkills, int pathfinderSkill )
            {
            MoveCost c ;

            if( terrain == __plain );
            else if( terrain == __road );
            else if( heroSkills.bHasWayfarerBoots || terrain == heroSkills.nativeTerrain ) terrain = __plain ;
            else if( pathfinderSkill )
                {
                unsigned short  nMoveCost  = bShortMove ? 100 : 141 ;
                unsigned short  nExtraCost = terrain & (__sand | __snow ) ? (bShortMove ? 50 : 71) : (bShortMove ? 25 : 35 );

                return nMoveCost + nExtraCost * ( 1.f - pathfinderSkill / 100.f ) ;
                }

            return bShortMove ? c.getMoveCost( terrain ) : c.getMoveCostDiag( terrain );
            }

        static unsigned short getMoveCost( unsigned char terrain )
            {
            if( terrain & __plain ) return 100 ;
            if( terrain & __road ) return 75 ;
            if( terrain & (__sand | __snow ) ) return 150 ;
            return 125 ;
            }

        static unsigned short getMoveCostDiag( unsigned char terrain )
            {
            if( terrain & __plain ) return 141 ;
            if( terrain & __road ) return 106 ;
            if( terrain & (__sand | __snow ) ) return 212 ;
            return 176 ;
            }

        unsigned short Min( MoveCost& c )
            {
            unsigned short  changed = 0 ;

            if( m[__plain__].Improve       ( c.m[__plain__]        ) ) changed |= __plain ;
            if( m[__dirt__].Improve        ( c.m[__dirt__]         ) ) changed |= __dirt ;
            if( m[__lava__].Improve        ( c.m[__lava__]         ) ) changed |= __lava ;
            if( m[__sand__].Improve        ( c.m[__sand__]         ) ) changed |= __sand ;
            if( m[__snow__].Improve        ( c.m[__snow__]         ) ) changed |= __snow ;
            if( m[__subterranean__].Improve( c.m[__subterranean__] ) ) changed |= __subterranean ;
            if( m[__taiga__].Improve       ( c.m[__taiga__]        ) ) changed |= __taiga ;
            if( m[__road__].Improve        ( c.m[__road__]         ) ) changed |= __road ;

            return changed ;
            }

        void adjustMinimumDistance()
            {
            for( unsigned short i = 0 ; i < __terrainCnt__ ; i++ ) m[ i ].adjustMinimumDistance() ;
            }

        void getZeroThreatMoveCostEstimate( unsigned char estimate[] ) const
            {
            *estimate++ = zeroThreatPath ? m[__plain__].moveCost64() : 0 ;
            *estimate++ = zeroThreatPath ? m[__dirt__].moveCost64() : 0 ;
            *estimate++ = zeroThreatPath ? m[__lava__].moveCost64() : 0 ;
            *estimate++ = zeroThreatPath ? m[__sand__].moveCost64() : 0 ;
            *estimate++ = zeroThreatPath ? m[__snow__].moveCost64() : 0 ;
            *estimate++ = zeroThreatPath ? m[__subterranean__].moveCost64() : 0 ;
            *estimate++ = zeroThreatPath ? m[__taiga__].moveCost64() : 0 ;
            *estimate++ = zeroThreatPath ? m[__road__].moveCost64() : 0 ;
            }

        void set( unsigned short c )
            {
            m[__plain__]        = c ;
            m[__dirt__]         = c ;
            m[__lava__]         = c ;
            m[__sand__]         = c ;
            m[__snow__]         = c ;
            m[__subterranean__] = c ;
            m[__taiga__]        = c ;
            m[__road__]         = c ;
            }

        void clear()
            {
            m[__plain__]        = 0 ;
            m[__dirt__]         = 0 ;
            m[__lava__]         = 0 ;
            m[__sand__]         = 0 ;
            m[__snow__]         = 0 ;
            m[__subterranean__] = 0 ;
            m[__taiga__]        = 0 ;
            m[__road__]         = 0 ;
            nShortestPath = 0 ;
            }

        void dumpMoveCost() const
            {
            aiLogger() << "plain           : " ; m[__plain__].dump() ;
            aiLogger() << "dirt            : " ; m[__dirt__].dump() ;
            aiLogger() << "lava            : " ; m[__lava__].dump() ;
            aiLogger() << "sand            : " ; m[__sand__].dump() ;
            aiLogger() << "snow            : " ; m[__snow__].dump() ;
            aiLogger() << "subterranean    : " ; m[__subterranean__].dump() ;
            aiLogger() << "snow            : " ; m[__taiga__].dump() ;
            aiLogger() << "unhindered      : " ; m[__road__].dump() ;
            aiLogger() << "nShortestPath   : " << (int)nShortestPath << endl ;
            aiLogger() << "zeroThreatPath  : " << (int)zeroThreatPath << endl ;
            aiLogger() << "noTriggersInPath: " << (int)noTriggersInPath << endl ;
            }

        }; // struct moveCost


/*----------------------------------------------------------------
                    struct MoveCostEstimate
------------------------------------------------------------------*/

    struct MoveCostEstimate
        {
        unsigned char   moveCost[ __terrainCnt__ ];

        /*============== public methods ==============*/

        MoveCostEstimate& operator+= ( const MoveCostEstimate& c )
            {
            for( unsigned short i = 0 ; i < __terrainCnt__ ; i++ ) moveCost[i] += c.moveCost[i] ;
            return *this ;
            }

        MoveCostEstimate operator+ ( const MoveCostEstimate& c )
            {
            MoveCostEstimate tmp = *this ;
            return tmp += c ;
            }

        bool operator<= ( unsigned short c )
            {
            return moveCost[__road__] <= c ;
            }


        unsigned short improve( MoveCost& distance, unsigned short terrain, bool bSetFlagIfEqual = false )
            {
            MoveCostEstimate    tmp ;
            unsigned char*      p ;
            unsigned char*      t ;
            unsigned short      iTerrain ;
            unsigned short      update = 0 ;

            if( distance.zeroThreatPath )
                {
                distance.getZeroThreatMoveCostEstimate( tmp.moveCost );

                p = moveCost + __road__ ;
                t = tmp.moveCost + __road__ ;

                for( iTerrain = __road ; iTerrain ; iTerrain >>= 1, --p, --t )
                    {
                    if( terrain & iTerrain )
                        {
                        if( *t < *p )
                            {
                            *p = *t ;
                            update |= iTerrain ;
                            }
                        else if( bSetFlagIfEqual && *t == *p )
                            update |= iTerrain ;
                        }
                    }
                }

            return update ;
            }

        void set( unsigned char dist )
            {
            for( unsigned short i = 0 ; i < __terrainCnt__ ; i++ ) moveCost[i] = dist ;
            }

        void maximum()
            {
            set( 0xff );
            }

        void zero()
            {
            ZeroMemory( moveCost, __terrainCnt__ );
            }

        }; // struct MoveCostEstimate

Note that the MoveCost is optimized for the accurate calculation of large movement costs, whereas the MoveCostEstimate is more compact and optimized for quick estimates. The difference in practice can easily be a factor 2 to 10 in performance, depending on the size of tile maps, processor cache, and memory bus speed. Imagine that a high duty pathfinder has to process millions of distances a second.

In general good use of constants and a smart C++ class partition is required to implement a complex movement logic (terrain and hero skills affect movement cost) by lightweight, efficient and easy to maintain code.

The tile information itself, FastMoveTile, is stored also in a highly compact format:

   struct FastMoveTile   /* tile information customized for fast movement computation */
        {
        union
            {
            unsigned long       flags ;
            struct {
                unsigned        neighbors           : 8 ;
                unsigned        threatLevel         : 4 ;
                unsigned        bIsUnderground      : 1 ;
                unsigned        bHasAnyWater        : 1 ;
                unsigned        bDeepWater          : 1 ;
                unsigned        bIsBlocked          : 1 ;
                unsigned        bIsHeroHolder       : 1 ;
                unsigned        bIsMonster          : 1 ;  // neutral creatures on the adventure map, or stationary hero
                unsigned        bIsTrigger          : 1 ;  // monster/borderguard trigger or interactive blocked
                unsigned        bHasCollectable     : 1 ;
                unsigned        bInteractiveBlocked : 1 ;
                unsigned        bIsRoad             : 1 ;
                unsigned        ttDirt              : 1 ;
                unsigned        ttGrass             : 1 ;
                unsigned        ttSand              : 1 ;
                unsigned        ttSnow              : 1 ;
                unsigned        ttLava              : 1 ;
                unsigned        ttSubterranean      : 1 ;
                unsigned        ttDwarvenMines      : 1 ;
                unsigned        ttTaiga             : 1 ;
                unsigned        ttWasteland         : 1 ;
                unsigned        bFixedBorder        : 1 ;
                };
            };

        union
            {
            unsigned long       extFlags ;
            struct {
                unsigned        iBorderTile         : 16 ; // TO DO: communicate 64K limit properly
                unsigned        ownerTileID         : 8 ;
                unsigned        bIsBorderGuard      : 1 ;
                unsigned        bIsAdvObjectMajor   : 1 ;
                unsigned        bIsTeleporter       : 1 ;
                unsigned        bIsDock             : 1 ;
				unsigned		bIsLandWaterBorder  : 1 ;
				unsigned		bStationaryHero     : 1 ;
#ifdef STRATEGIC_AI_DBG
                unsigned        bIsBorder           : 1 ;
#endif
                unsigned        bIsGarrison         : 1 ;
                };
            };

        unsigned short          advEventID ;

        unsigned short          x ;
        unsigned short          y ;

        unsigned short          idAreaState ;

        // threat range is a square
        // monsters and heroes threaten adjacent cells
        // garrison cell isn't threatened from neighboring cells
        // garrison doesn't threaten neighboring cells
        // ships don't threaten neighboring cells

        /*============== construction ==============*/

        FastMoveTile() ;

        /*============== operators ==============*/

        //FastMoveTile& operator= ( const STileGameInfo& t );

        /*============== static functions ==============*/

        static FastMoveTile* FindNoEventTile( unsigned char& tileID, FastMoveTile** tiles, unsigned short cnt );
        static unsigned char GetNeighborReverse( unsigned char iNeighbor );
        static unsigned char GetDirection( unsigned char iNeighbor );

        /*============== public methods ==============*/

#ifndef STRATEGIC_AI_PROCESS
        FastMoveTile& init( IWorld* pWorld, const STileGameInfo& t, bool _bIsUnderground );
#endif
        SAdventureCell GetCellPosition() { return SAdventureCell( x, y, bIsUnderground ); }
        bool IsAdvObjectMajor() ;
        bool IsAdvObjectMajorExt() ;
        bool IsAdvObjectMajorNoHolder() ;
        bool IsTown() ;
        bool IsGarrison() ;
        bool IsTeleporter() ;
        bool IsTrigger() ;
        bool IsInteractiveBlockedTile() ;
        bool IsShieldedFromThreat() ;
#ifndef STRATEGIC_AI_PROCESS
        bool UpdateDock( IWorld* pWorld, unsigned short shipyardEventID );
        bool UpdateTown( IWorld* pWorld, IAdvMapTown* town );
        void AddTeleportExits( IWorld* pWorld );
        bool UpdateMobileObject( IWorld* pWorld, IAdvMapObject* obj, bool bRegisterThreatObjects, bool bRegisterNoThreatObjects, bool bFlagEventCache );
        bool ValidateMobileObject( IAdvMapObject* obj );
        bool RemoveMobileObject() ;
        void RemoveBorderGuard() ;
#endif
        unsigned char GetGarrisonNeighborsAtSide( int nSide );
        unsigned char GetGarrisonNeighborsAtSide( FastMoveTile* tn );
        TerrainClassIndex getTerrainClassIndex() ;
        TerrainClass getTerrainClass() ;
        void UpdateThreatLevel( bool bRemoveBorderGuard = false );
        unsigned char UpdateNeighborsMonsterTrigger( bool bSignalChange = false, bool bSignalChangeMajor = false );
        bool IsBorderGuardKeyMaster( FastMoveTile* borderGuard );
        bool IsMobile() ;
        FastMoveTile* IsThreatOfNeighboringMonsterWithPrecedence( FastMoveTile* origin );
        FastMoveTile* GetNeighboringBorderGuard() ;
        FastMoveTile* GetNeighboringMonster( FastMoveTile* ignored = NULL );
        FastMoveTile* GetNeighboringMobile( FastMoveTile* ignored = NULL );
        unsigned char GetNeighborRelationUnblocked( FastMoveTile* tn );
        unsigned char GetNeighborRelation( FastMoveTile* tn );
        bool IsNeighbor( FastMoveTile* tn ) const ;
        void GetNeighbors() ;

        void GetAdjacentTargets( vector<SAdventureCell>* adjacentTargets );
        unsigned short GetCollectables( FastMoveTile** collectables, unsigned short maxCollectables );

        FastMoveTile* GetNeighbor( unsigned char iNeighbor );
        FastMoveTile* getN () { return this - nXSize     ; }
        FastMoveTile* getNE() { return this - nXSize + 1 ; }
        FastMoveTile* getE () { return this          + 1 ; }
        FastMoveTile* getSE() { return this + nXSize + 1 ; }
        FastMoveTile* getS () { return this + nXSize     ; }
        FastMoveTile* getSW() { return this + nXSize - 1 ; }
        FastMoveTile* getW () { return this          - 1 ; }
        FastMoveTile* getNW() { return this - nXSize - 1 ; }

        void dumpTileInfo( bool bEndLine = true );

        /*============== helpers ==============*/
    private:
        unsigned short GetAdjacentCollectables( unsigned short id, FastMoveTile** collectables, unsigned short maxCollectables );

        }; // struct FastMoveTile 

In the next snippet of code this information is put to use to check for threats on neighboring tiles.

aiAreaState::FastMoveTile* aiAreaState::FastMoveTile::GetNeighboringMonster( FastMoveTile* ignored )
    {
    FastMoveTile* monster = NULL ;

    for( unsigned char iNeighbor = N ; iNeighbor ; iNeighbor <<= 1 )
        if( neighbors & iNeighbor )
            {
            FastMoveTile* tn = GetNeighbor( iNeighbor );
            if( tn->bIsMonster && tn->advEventID
                               && ( ! monster || tn->threatLevel > monster->threatLevel ) && ( ! ignored || ignored != tn ) )
                monster = tn ;
            }

    return monster ;
    }


bool aiAreaState::FastMoveTile::IsNeighbor( FastMoveTile* tn )
    {
    if( tn->bIsUnderground != bIsUnderground ) return false ;

    short dx = tn->x - x ;
    short dy = tn->y - y ;

    if( dx > 1 || dx < -1 ) return false ;
    if( dy > 1 || dy < -1 ) return false ;

    return true ;
    }


void aiAreaState::FastMoveTile::GetNeighbors()
    {
    unsigned short neighbors = 0x00ff ;

    if( y == 0 )          neighbors &= ~north ;
    if( y == nYSize - 1 ) neighbors &= ~south ;
    if( x == 0 )          neighbors &= ~west ;
    if( x == nXSize - 1 ) neighbors &= ~east ;

    if( neighbors & N )
        {
        if( (this - nYSize)->bIsBlocked ) neighbors &= ~N ;
        }
    if( neighbors & NE )
        {
        if( (this - nYSize + 1)->bIsBlocked ) neighbors &= ~NE ;
        }
    if( neighbors & E )
        {
        if( (this + 1)->bIsBlocked ) neighbors &= ~E ;
        }
    if( neighbors & SE )
        {
        if( (this + nYSize + 1)->bIsBlocked ) neighbors &= ~SE ;
        }
    if( neighbors & S )
        {
        if( (this + nYSize)->bIsBlocked ) neighbors &= ~S ;
        }
    if( neighbors & SW )
        {
        if( (this + nYSize - 1)->bIsBlocked ) neighbors &= ~SW ;
        }
    if( neighbors & W )
        {
        if( (this - 1)->bIsBlocked ) neighbors &= ~W ;
        }
    if( neighbors & NW )
        {
        if( (this - nYSize - 1)->bIsBlocked ) neighbors &= ~NW ;
        }

    FastMoveTile::neighbors = neighbors ;
    }


aiAreaState::FastMoveTile* aiAreaState::FastMoveTile::GetNeighbor( unsigned char iNeighbor )
    {
    FastMoveTile* tn = this ;

    if( iNeighbor & north )
        tn -= nYSize ;
    else if( iNeighbor & south )
        tn += nYSize ;

    if( iNeighbor & east )
        tn += 1 ;
    else if( iNeighbor & west )
        tn -= 1 ;

    return tn ;
    }


void aiAreaState::FastMoveTile::UpdateThreatLevel( bool bRemoveBorderGuard )
    {
    if( bRemoveBorderGuard )
        bIsBorderGuard = false ;

    if( bIsBorderGuard )
        threatLevel = cMaxThreatLevel ;
    else
        {
        FastMoveTile* nmonster = GetNeighboringMonster() ;
        threatLevel = nmonster ? nmonster->threatLevel : 0 ;
        }

    bIsTrigger = IsTrigger() ;
    }


unsigned char aiAreaState::FastMoveTile::UpdateNeighborsMonsterTrigger( bool bSignalChange )
    {
    unsigned char affectedNeighbors = 0x00 ; 

    for( unsigned char iNeighbor = N ; iNeighbor ; iNeighbor <<= 1 )
        {
        if( neighbors & iNeighbor )
            {
            FastMoveTile* tn = GetNeighbor( iNeighbor );

            if( tn->bIsMonster || tn->bIsBorderGuard ) continue ;

            unsigned char nneighbors     = tn->neighbors ;
            unsigned char newThreatLevel = 0 ; 

            for( unsigned char iiNeighbor = N ; iiNeighbor ; iiNeighbor <<= 1 )
                if( nneighbors & iiNeighbor )
                    {
                    FastMoveTile* tnn = tn->GetNeighbor( iiNeighbor );

                    if( tnn->bIsMonster )
                        {
                        if( tnn->threatLevel > newThreatLevel ) newThreatLevel = tnn->threatLevel ;
                        }
                    }

            if( bSignalChange && tn->threatLevel != newThreatLevel && tn->bFixedBorder )
                affectedNeighbors |= iNeighbor ;

            tn->threatLevel = newThreatLevel ;
            tn->bIsTrigger  = tn->IsTrigger() ;
            } // if( neighbors & iNeighbor )
        } // for( unsigned char iNeighbor = N ; iNeighbor ; iNeighbor <<= 1 )

    return affectedNeighbors ;
    }


void aiAreaState::FastMoveTile::GetAdjacentTargets( vector<SAdventureCell>* adjacentTargets )
    {
    for( unsigned char iNeighbor = N ; iNeighbor ; iNeighbor <<= 1 )
        if( neighbors & iNeighbor )
            {
            FastMoveTile* tn = GetNeighbor( iNeighbor );

            if( tn->bHasCollectable )
                {
                adjacentTargets->push_back( SAdventureCell( tn->x, tn->y, tn->bIsUnderground ) );
                }
            }
    }

It is a functional example how adventure map logic benefits from sound logical partition and how small methods complement each other to perform more complex detection and evaluation, while at the same time resulting in code easy to read and maintain. The adventure map consists of a contiguous two-dimensional array of lightweight FastMoveTile objects. The array can hold two floors (surface and underground) and the map has a size of nXSize * nYSize.

The next code sample gives you an idea how the tile data and the movement cost are connected with evaluating the significance of a path.

/*----------------------------------------------------------------
                    struct ShortPath
------------------------------------------------------------------*/

    struct ShortPath
        {
        /* ... */

        long                value ;
        long                kamikazee ;
        unsigned short      movePoints ;
        unsigned short      mana ;
        unsigned short      manaMax ;

        bool                bCriticalPosition ;
        bool                bMaintainStrategicPresence ;
        bool                bPositionFinal ;
        bool                bProcessed ;
        bool                bSimulated ;

        unsigned char       allies ;
        BIT__KEY            keys ;

        HeroArmyStack       army ;
        HeroSkills          heroSkills ;

        SMoney              available ;
        SMoney              income ;

        /*============== public methods ==============*/

        /* ... */

        bool hasBetterTravelValue( ShortPath& from, long eventValue, unsigned short travelCost, unsigned short minValuePerTravel,
                                   long eventKamikazee ) const
            {
#ifdef STRATEGIC_AI_DETAILEDDBGEVENTTRACE
            if( debug.eventQueue ) aiLogger() << "bProcessed = " << (int)bProcessed << endl ;
#endif
            if( ! bProcessed ) return true ;

            if( eventKamikazee )
                return kamikazee && eventKamikazee > kamikazee ;
            else if( kamikazee )
                return true ;

            short newRemainingMovePoints     = TMoveCost::moveCost64( from.movePoints - travelCost );
            short currentRemainingMovePoints = TMoveCost::moveCost64( movePoints );

            long  gain    = from.value + eventValue - value ;
            long  ceiling = ( minValuePerTravel << 2 ) * (currentRemainingMovePoints - newRemainingMovePoints) ;

#ifdef STRATEGIC_AI_DETAILEDDBGEVENTTRACE
            if( debug.eventQueue )
                {
                aiLogger() << "from.value = " << (int)from.value << ", eventValue = " << (int)eventValue << ", value = "
                           << (int)value << endl ;
                aiLogger() << "currentRemainingMovePoints = " << (int)currentRemainingMovePoints << ", newRemainingMovePoints = "
                           << (int)newRemainingMovePoints << endl ;
                aiLogger() << "gain = " << (int)gain << ", ceiling = " << (int)ceiling << ", improved = " << (int)(gain > ceiling)
                           << endl ;
                }
#endif
            bool bResult = gain > ceiling ;

#ifdef STRATEGIC_AI_DETAILEDDBGEVENTTRACE
            if( debug.eventQueue ) aiLogger() << "bResult = " << (int)bResult << endl ;
#endif
            return bResult ;
            }

        bool hasHigherTravelValue( ShortPath& from, long eventValue, unsigned short travelCost ) const
            {
            if( ! bProcessed ) return true ;

            return from.value + eventValue  > value
                || from.value + eventValue == value && from.movePoints - travelCost > movePoints ;
            }

        bool improve( ShortPath& alternatePath ) const
            {
            if( debug.eventQueue )
                {
                aiLogger() << "ShortPath::improve(), present = " << (bProcessed ? value : cLowestStrategicValue)
                           << ", alternatePath.value = " << (int)alternatePath.value
                           << ", alternatePath.movePoints = " << alternatePath.movePoints ;
                }

            if( alternatePath.kamikazee )
                return ! bProcessed || kamikazee && alternatePath.kamikazee > kamikazee ;

            if( ! bProcessed
            ||  alternatePath.value > value
            ||  alternatePath.value == value && alternatePath.movePoints > movePoints
            ||  kamikazee )
                {
                if( debug.eventQueue ) aiLogger() << "  improved" << endl ;
                
                return true ;
                }

            if( debug.eventQueue ) aiLogger() << "  skipped" << endl ;
            return false ;
            }

        }; // struct ShortPath


/*----------------------------------------------------------------
                    class AdvObjectBase
------------------------------------------------------------------*/

    enum AdvMapEventType 
        {
        AdvMapEvent_None               = 0x00000000,
        AdvMapEvent_Town               = 0x00000001,
        AdvMapEvent_Garrison           = 0x00000002,
        AdvMapEvent_Sanctuary          = 0x00000004,
        AdvMapEvent_Teleporter         = 0x00000008,
        AdvMapEvent_Exit               = 0x00000010,

        AdvMapEvent_Stables            = 0x00000020,
        AdvMapEvent_RallyFlag          = 0x00000040,
        AdvMapEvent_FountainOfYouth    = 0x00000080,
        AdvMapEvent_Oasis              = 0x00000100,
        AdvMapEvent_Shipyard           = 0x00000200,
        AdvMapEvent_Lighthouse         = 0x00000400,
        AdvMapEvent_MagicWell          = 0x00000800,
        AdvMapEvent_MagicSpring        = 0x00001000,
        AdvMapEvent_HillFort           = 0x00002000,
        AdvMapEvent_WarMachineFactory  = 0x00004000,
        AdvMapEvent_TradingPost        = 0x00008000,
        AdvMapEvent_Dock               = 0x00010000,
        AdvMapEvent_BorderGuard        = 0x01000000,

        AdvMapEvent_Monster            = 0x20000000,
        AdvMapEvent_Artifact           = 0x40000000,
        AdvMapEvent_Treasure           = 0x80000000
        };

    static const unsigned long  AdvMapEvent_Removable  = AdvMapEvent_Monster | AdvMapEvent_Artifact | AdvMapEvent_Treasure
                                                       | AdvMapEvent_BorderGuard ;
    static const unsigned long  AdvMapEvent_PreBattle  = AdvMapEvent_RallyFlag | AdvMapEvent_FountainOfYouth | AdvMapEvent_Oasis ;

    static const unsigned long  isAdvObjectMajor       = AdvMapEvent_Town | AdvMapEvent_Garrison | AdvMapEvent_Sanctuary
                                                       | AdvMapEvent_Teleporter | AdvMapEvent_Exit | AdvMapEvent_Dock ;
    static const unsigned long  isAdvObjectHolder      = AdvMapEvent_Exit | AdvMapEvent_Dock ;
    static const unsigned long  isMoveBoost            = AdvMapEvent_Stables | AdvMapEvent_RallyFlag | AdvMapEvent_FountainOfYouth
                                                       | AdvMapEvent_Oasis ;
    static const unsigned long  isMagicBoost           = AdvMapEvent_MagicWell | AdvMapEvent_MagicSpring ;


    class AdvObjectBase : public PooledAdvInfo  /* represents an interactive object on the adventure map */
    {
    public:
        /*============== static const & variables ==============*/

        static const unsigned long      isPreBattleBoost = 0x000001ff ;
        static const unsigned long      builtZoneFlags   = 0x00000600 ;
        static const unsigned long      mobileZoneFlags  = 0x00670000 ;
        static const unsigned long      worldChanged     = 0x0000ffff ;


        /*============== member variables ==============*/
    public:
        union
            {
            unsigned long       flags ;
            struct {
                    /* objects with their own one tile area state */
                unsigned        bIsTown            : 1 ;
                unsigned        bIsGarrison        : 1 ;
                unsigned        bIsSanctuary       : 1 ;
                unsigned        bIsTeleporter      : 1 ;    // teleporter/monolith/subterranean gate
                unsigned        bIsExit            : 1 ;

                    /* buildings */
                unsigned        stables            : 1 ;
                unsigned        rallyFlag          : 1 ;
                unsigned        fountainOfYouth    : 1 ;

                unsigned        oasis              : 1 ;
                unsigned        shipyard           : 1 ;
                unsigned        lighthouse         : 1 ;
                unsigned        magicWell          : 1 ;
                unsigned        magicSpring        : 1 ;
                unsigned        hillFort           : 1 ;
                unsigned        warMachineFactory  : 1 ;
                unsigned        tradingPost        : 1 ;

                unsigned        dock               : 1 ;
                unsigned        tavern             : 1 ;
                unsigned        denOfThieves       : 1 ;
                unsigned        redwoodObservatory : 1 ;
                unsigned        hutOfTheMagi       : 1 ;
                unsigned        seerHut            : 1 ;
                unsigned        cartographer       : 1 ;
                unsigned        keymaster          : 1 ;
                unsigned        borderGuard        : 1 ;

                unsigned        dwelling           : 1 ;  // creature generator
                unsigned        mine               : 1 ;
                unsigned        treasureVault      : 1 ;  // guarded building with treasure stash
                unsigned        otherInteractive   : 1 ;  // other interactive map object

                    /* mobile objects */
                unsigned        monster            : 1 ;
                unsigned        artifact           : 1 ;
                unsigned        treasure           : 1 ;
                };
            };

        /*-------------- extended flags --------------*/

        union
            {
            unsigned long       zoneFlags ;
            struct {
                    /* prebattle boosts */
                unsigned        fountainOfFortune   : 1 ;
                unsigned        mermaids            : 1 ;
                unsigned        faerieRings         : 1 ;
                unsigned        temple              : 1 ;
                unsigned        lakeOfScarletSwan   : 1 ;
                unsigned        idolOfFortune       : 1 ;
                unsigned        buoy                : 1 ;
                unsigned        nomadsShaman        : 1 ;
                unsigned        fortuitousSanctuary : 1 ;     

                unsigned        bMultiTileArea     : 1 ;
                unsigned        bIsAreaState       : 1 ;
                unsigned        library            : 1 ;             
                unsigned        bHeroVisitsOnce    : 1 ;
                unsigned        bWeeklySupplies    : 1 ;
                unsigned        bMerchant          : 1 ;
                unsigned        bGuarded           : 1 ;
                unsigned        bFriendlyHero      : 1 ;
                unsigned        bAlliedHero        : 1 ;
                unsigned        bHostileHero       : 1 ;
                unsigned        bHeroToSupply      : 1 ;
                unsigned        bDockBlocked       : 1 ;
                unsigned        bShip              : 1 ;
                unsigned        bCaravan           : 1 ;
                unsigned        bEvaluated         : 1 ;
                unsigned        bHeroSensitive     : 1 ;    // change of hero
                unsigned        bArmySensitive     : 1 ;    // change of army
                unsigned        bResourceSensitive : 1 ;    // change of available resources
                unsigned        bValidate          : 1 ;
                unsigned        bRemoved           : 1 ;    // takes precedence over bValidate

                unsigned        relay              : 1 ;
                unsigned        queued             : 1 ;
                unsigned        visited            : 1 ;
                };
            };

        union
            {
            unsigned long       worldFlags ;
            struct {
                    /* world state change (modifications need to be reflected in worldChanged) */
                unsigned        affectedNeighbors  : 8 ;
                unsigned        bModified          : 1 ;
                unsigned        bChangeMinor       : 1 ;
                unsigned        bChangeMajor       : 1 ;
                unsigned        bChangeMajorBorder : 1 ;
                unsigned        reservedModified   : 4 ;

                    /* world state */
                unsigned        bValidAdvMapEvent  : 1 ;
                unsigned        bShrine            : 1 ;
                unsigned        bPrison            : 1 ;
                unsigned        bFlagged           : 1 ;
                unsigned        bSpecialDwelling   : 1 ;
                unsigned        bCampFire          : 1 ;
                unsigned        bPlayerVisitsOnce  : 1 ;
                unsigned        bResetAndModify    : 1 ;
                unsigned        bTouched           : 1 ;
                unsigned        bMainRoute         : 1 ;
                unsigned        bStrategicGoal     : 1 ;
                unsigned        reservedWorldState : 5 ;
                };
            };


        unsigned short          advEventID ;
        SAdventureCell          position ;

        /*-------------- object IDs --------------*/

        cacheID                 mID ;               // mobile or object ID
        NDb::EBuildingType      buildingType ;
        unsigned short          visitedOnceID ;     // index into advVisitedOnce

        /*-------------- paired events --------------*/

        SAdventureCell          shipyardPosition ;
        unsigned short          shipyardEvent ;
        unsigned short          shipyardDockEvent ;

        /*-------------- world properties for AI processing --------------*/

        long                    value ;
        long                    baseValue ;
        long                    passValue ;
        SMoney                  income ;
        FastMoney               resourceType ;
        BIT__PLAYER             owner ;
        BIT__PLAYER             visitedBy ;

        /*-------------- guards & creature dwellings --------------*/

        long                    guardPower ;
        long                    guardExpValue ;
        long                    dwellingRecruits ;
        HeroArmyStack           dwellingCreatures ;
        HeroArmyStack           dwellingWeeklyCreatures ;
        NDb::ETownType          dwellingNativeTown ;

        /* ... */

        /*============== construction ==============*/
    public:
        AdvObjectBase( AdvObjectBase* copy = 0 )
            : advEventID(0), flags(0), zoneFlags(0), worldFlags(0), position(INVALID_ADVENTURE_CELL), __tile(NULL),
              buildingType((NDb::EBuildingType)0), visitedOnceID(0), value(0), baseValue(0),
              owner(0), visitedBy(0), guardPower(0), guardExpValue(0) // nilAdvMapEvent
            {
            if( copy )
                *this = *copy ;
            }

        ~AdvObjectBase()
            {
            }

        /*============== public methods ==============*/
    public:
        bool     IsAdvObjectMajor() const { return flags & isAdvObjectMajor ; }
        bool     IsPreBattleBoost() const { return flags & AdvMapEvent_PreBattle || zoneFlags & isPreBattleBoost ; }
        bool     IsBorderGuardKeyMaster( FastMoveTile* tiBorderGuard );
        bool     IsOpenBorderGuard( BIT__KEY keys )
                    {
                    return borderGuard && mID.object < advMapGuards.cnt && advMapGuards[ mID.object ].keyColor & keys ;
                    }

        void     setGuards( long _guardPower, long _guardExpValue )
                    {
                    if( guardPower != _guardPower ) bModified = true ;
                    guardPower    = _guardPower ;
                    guardExpValue = _guardExpValue ;
                    }

        wchar_t* GetName() const ;
        wchar_t* GetInternalName() const ;

        void     dump( unsigned short idAreaState = 0 ) const ;

    }; // class AdvObjectBase


/*----------------------------------------------------------------
                    class AdvMapEvent
------------------------------------------------------------------*/

    class AdvMapEvent : public AdvObjectBase  /* represents an interactive object on the adventure map */
    {
        /* ... */

        /*============== construction ==============*/
    public:

        AdvMapEvent( AdvObjectBase* copy = 0 ); /* usually initialized from cached AdvObjectBase object */

        AdvMapEvent( unsigned short id, FastMoveTile* pTile = NULL )
            : AdvObjectBase(id), tile(pTile),
              idAreaState(0), tileID(0), nLinkedEventCnt(0), shipyardTile(0),
              path(), initialEventSequence(), mobileReinforcements(id) {}

        ~AdvMapEvent()
            {
            }

        /*============== operators ==============*/

        AdvMapEvent& operator= ( const AdvObjectCache& obj )
            {
            AdvObjectBase::operator=( obj );

            if( __tile )
                tile = GetFastMoveTileSafe( position ); 

            if( shipyard )
                shipyardTile = GetFastMoveTileSafe( shipyardPosition );

            return *this ;
            }

        /*============== public methods ==============*/
    public:
        /* ... */

        bool evaluate( AdvMapEvent& from, unsigned short travelCost, unsigned char distance, unsigned short minValuePerTravel,
                                          unsigned char nTurn, unsigned char nPlayer )
            {
            if( &from == this ) return false ;

            bTouched = true ;


            bool bWillEndTurn =
                 bMultiTileArea || bIsSanctuary || ( bShip && ! owner || dock && ! IsDockBlocked( from.path.visited, nTurn ) )
                                                   && ! from.path.heroSkills.bHasSnatchSkill ;

            if( dock || bShip )
                AdvMapEvent::bEvaluated = false ; // TO DO: optimize, reprocess only some cases

            bool bImprove   = false ;
            bool bEvaluated = AdvMapEvent::bEvaluated ;

            if( debug.eventQueue )
                {
                aiLogger() << "     to  : " ; dump() ;
                aiLogger() << "     from: " ; from.dump() ;
                aiLogger() << "     hero: " << from.path.hero->GetName() << endl ;
#ifdef STRATEGIC_AI_DBG_WORLDSTATE
                aiLogger() << "     keys: " << print8Bit( from.path.keys>>1 ) << endl ;
#endif
                }

            passValue = 0 ;
            kamikazee = 0 ;

            if( travelCost <= from.path.movePoints )
                {
                if( bWillEndTurn )
                    {
                    if( bShip || dock || bIsSanctuary ) travelCost = from.path.movePoints, bEvaluated = true ;
                    bImprove = path.hasHigherTravelValue( from.path, 0, travelCost );
                    }
                else if( bEvaluated )
                    bImprove = value >= minValuePerTravel * distance
                            && path.hasBetterTravelValue( from.path, value, travelCost, minValuePerTravel, kamikazee );

                if( bImprove || ! bEvaluated )
                    {
                    long eventValue = getEventValue( &from.path, travelCost, nTurn, nPlayer );

#ifdef STRATEGIC_AI_DETAILEDDBGEVENTTRACE
                    aiLogger() << "eventValue = " << (int)eventValue << ", minValuePerTravel = " << (int)minValuePerTravel
                               << ", distance = " << (int)distance << ", kamikazee = " << (int)kamikazee << endl ;
#endif
                    if( ! bEvaluated )
                        bImprove = (passValue && eventValue > 0 || eventValue >= minValuePerTravel * distance)
                                && path.hasBetterTravelValue( from.path, eventValue, travelCost, minValuePerTravel, kamikazee );

                    if( dock && ! eventValue ) bImprove = false ;

                    if( bImprove )
                        {
                        value = eventValue -= passValue ;

                        path = from.path ;
                        path.value            += eventValue ;
                        path.movePoints       -= travelCost ;
                        path.previousEvent     = from.advEventID ;
                        path.bBattleSite       = bGuarded || monster || bHostileHero ;
                        path.bAcquisition      = mine || treasure || treasureVault ;
                        path.bDwellingRecruits = dwelling ;
                        path.kamikazee         = kamikazee ;

                        path.lastNonHeroEvent = bFriendlyHero ? from.path.lastNonHeroEvent : advEventID ;

                        if( bIsTown )
                            {
                            if( kamikazee );
                            else if( path.strategicAssets.get( mID.strategicAsset ) )
                                {
                                approveEstimatedRecruits() ;
                                }
                            else
                                {
                                SGameTime gameTime( worldParameters.worldTime );
                                getAdjustedGameTime( gameTime, nTurn, nPlayer );

                                unsigned char adjustedTurn = gameTime.nDay - worldParameters.worldTime.nDay ;

                                if( 1 == gameTime.GetDayOfWeek() && (1 << nPlayer) < owner )
                                    path.nTownAcquisition = adjustedTurn ;

                                if( guardPower ) path.bBattleSite = true ;
                                }
                            }
                        else if( hillFort )
                            {
                            value = path.army.UpgradeForces( &path.available );
                            }
                        else if( keymaster )
                            {
                            path.keys |= advMapKeys[ mID.object ].keyColor ;
                            }

                        if( bIsExit && guardPower )
                            {
                            FastMoveTile* monster = tile ;

                            if( monster && ! monster->bIsMonster ) monster = monster->GetNeighboringMonster() ;

                            bool bNeighboringMonster = monster && monster != tile ;

                            if( ! path.visited.get( advEventID )
                            ||  bNeighboringMonster && ! path.visited.get( monster->advEventID ) )
                                {
                                path.bBattleSite = true ;

                                if( bNeighboringMonster )
                                    path.visited.set( monster->advEventID );
                                }
                            }
                        
                        if( shipyard )
                            {
                            SMoney cost( 10, 0, 0, 0, 0, 0, 1000 );
                            path.available -= cost ;
                            path.value     -= 2000 ;
                            path.visited.clear( shipyardDockEvent );
                            if( ! path.bIsCommander && path.available.nWood < 10 ) path.value -= 5000 ;
                            }

                        bool bTownShipyardDock = false ;

                        if( dock && ! IsDockBlocked( from.path.visited, nTurn ) || bShip && ! owner )
                            {
                            if( dock && getAdvMapEvent( shipyardEvent ).bIsTown )
                                {
                                SMoney cost( 10, 0, 0, 0, 0, 0, 1000 ); // reserve some wood
                                path.available   -= cost ;
                                path.bBuildShip   = true ;
                                bTownShipyardDock = true ;
                                }

                            if( ! path.heroSkills.bHasSnatchSkill )
                                path.movePoints = 0 ;
                            }

                        if( fountainOfYouth ) path.movePoints += 400 ;
                        if( oasis           ) path.movePoints += 800 ;
                        if( rallyFlag       ) path.movePoints += 400 ;
                        if( stables && ! path.bVisitedStables ) path.movePoints += 600 ;
                        if( path.bBattleSite && path.heroSkills.bBattleBonusMove ) path.movePoints += 350 ;

                        if( magicWell && path.mana < path.manaMax )
                            path.mana = path.manaMax ;

                        if( magicSpring && path.mana < path.manaMax * 2 )
                            path.mana = path.manaMax * 2 ;
                      
                        if( ! bMultiTileArea && ! IsAdvObjectMajor() || dock && ! bTownShipyardDock || bIsExit )
                            path.visited.set( advEventID );
                        }
                    }

                if( debug.eventQueue )
                    {
                    aiLogger() << distance << (bImprove ? ": (+" : ": (") << (int)value << ")  " ; dump() ; aiLogger() << endl ;
                    }
                }
            else
                {
                if( debug.eventQueue )
                    {
                    aiLogger() << "Travel cost exceeds remaining movement points by " << travelCost – from.path.movePoints
                               << "! " ; dump() ; aiLogger() << endl ;
                    }
                }

            currentTravelCost = travelCost ;

            return bImprove ;
            }

    }; // class AdvMapEvent

It is a more complex example how a logical partition into separate C++ classes yields well developed evaluation services on a higher logical level. ShortPath is used for adventure map path probing, AdvMapEvent encapsulates adventure map object properties. Only shortened samples are provided. The debug code is left in to demonstrate a typical debugging strategy, due to the complexity of the AI variable debug dumping has proved invaluable. It allows also for the incremental validation of functionality and detailed bug tracking during betas. Together the various classes allow for code that appears nearly organic in nature to express high level functionality.

Putting all together

With this short practical guide you have an overview what type of data is typically needed for pathfinding and AI. Please note that the data structures for the game itself are usually much more complex, the data structures presented here are highly optimized for an AI that needs to process millions of variants in a short period of time.

You may also ask why there are data structures for the adventure map events if the aim is to develop a pathfinder. Pathfinder and AI logic is often intertwined, which is highly evident in the design of Heroes III, essentially adventure map objects affect movement. There is a more clear distinction in the advanced AI I developed, but even here time constraints and requirements for optimization make both interdependent.

This concludes today's coding practice. If there is enough interest, the next topic will be how to build a working pathfinder and AI similar to the one Heroes III uses. Let me know if you are interested in this.

You’ve successfully subscribed to Quantomas AI
Welcome back! You’ve successfully signed in.
Great! You’ve successfully signed up.
Success! Your email is updated.
Your link has expired
Success! Check your email for magic link to sign-in.