Roguelike AI - Doing it the generic way

From RogueBasin
Jump to navigation Jump to search
I'm just starting to code the AI for my own roguelike Portals, and 
I've been planning an AI system that would be generic and easily 
extendable. Here's some ideas I have come up with (this article assumes
some knowledge of C++).

The main idea behind this article is, that by coding a few basic AI 
routines, more complex AI routines can be constructed using combinations 
of these simple routines.


First we'll need to define a simple Creature class.


class Creature {
  friend class Controller;

public:
  void SetAIModule(AI_Algorithm*);

private:
  AI_Algorithm* m_ai;
};

The Controller class handles all interaction between the Creature and
its AI-module.

class Controller {
public:
  Controller(Creature&);

// The MoveBy method is provided as the simplest example. Other methods
// needed would include spellcasting, wielding of items etc.

  void MoveBy(int delta_x, int delta_y);

private:
  Creature& m_creature;
};

The AI_Algorithm class is the abstract base class for all AI routines.
DoAIAction is the only common method between the modules. When it is the
creatures turn to act, it will simply call m_ai-&gtDoAIAction();

class AI_Algorithm {
public:
  AI_Algorithm(Controller&);

  virtual void DoAIAction() = 0; // pure virtual method

protected:
  Controller m_controller;
};

Going to a certain map location is the first AI module that is used by 
most following modules. This module calculates a good path (e.g. the 
shortest or safest path) through the map from the creature's current 
location to the destination. I'm not going to go into details about 
pathfinding in this article, but there are some very good web resources 
available on this subject.

class AI_GoToXY : public AI_Algorithm {
public:
  AI_GotoXY(Controller&);
  AI_GotoXY(Controller&, int x, int y);

  void SetDestination(int x, int y);

// The overriden method DoAIAction() calculates the best path (if not
// already calculated) and then moves the creature one step at a time
// along this path.

  virtual void DoAIAction();

protected:

// The class that generates and stores the best path to the destination.
// I'll cover the details of this class in a separate article.

  Path m_path;
};

Another basic module is random wandering around the map. Instead of the 
usual neutral monster behaviour seen in roguelikes (i.e. move one step 
to a random direction every turn) a more interesting movement can be 
generated by choosing a random (x,y) location from the map and sending 
the creature there, choosing a new destination whenever the current one 
is reached.

struct XYPoint { int x,y; };

class AI_WanderRandomly : public AI_Algorithm {
public:
  AI_WanderRandomly(Controller&);

// If the creature has no destination, the DoAIAction() method randomly
// picks a (x,y) location, and feeds the destination to the GoToXY module
// through m_gotoxy.SetDestination(x,y).

  virtual void DoAIAction();

protected:

  virtual XYPoint GetRandomLocation() const;

private:
// Instead of aggregation, private inheritance could also be used.

  AI_GoToXY m_gotoxy;

  int m_destination_x, m_destination_y;
}:


The next module moves the creature randomly within a certain area (this 
could be for example the creature's "nest"). The area could be defined 
in several different ways, but in this example, it is defined as a 
n-sided polygon. Other good alternatives would be a rectangle or a 
circle.

typedef std::vector<XYPoint> Polygon;

class AI_PatrolArea : public AI_WanderRandomly {
public:
  AI_PatrolArea(Controller&, const Polygon&);

// Alternatively:

// AI_PatrolArea(Controller&, XYPoint top_left, XYPoint bottom_right);
// AI_PatrolArea(Controller&, XYPoint center, int radius);

protected:

// The overriden method from AI_WanderRandomly selects a random (x,y)
// location from within the patrol area.

  virtual XY GetRandomLocation() const;

private:
  Polygon m_area;
};

ChaseCreature is one of the most important AI modules for a roguelike 
and it is used by almost every creature. It uses the GoToXY module to 
find the best path to the target creature's current location. 
Recalculating the path every turn would be very expensive, however, 
so a new destination should be set only on rare intervals. As a good 
rule of thumb, the closer the target is, the more often the path 
should be recalculated. 

class AI_ChaseCreature : public AI_Algorithm {
public:
  AI_ChaseCreature(Controller&);
  AI_ChaseCreature(Controller&, Creature&);

  void SetTarget(Creature&);

// DoAIAction checks whether the path needs to updated (via the
// NeedUpdate() method) and follows the created path.

  virtual void DoAIAction();

protected:

// The NeedUpdate method returns 'true' if the path needs to be
// recalculated. This method takes into account the distance to the
// target creature as well as the distance the target has moved since
// the last update.

  bool NeedUpdate() const;

private:
  Creature* m_target;
  AI_GoToXY m_gotoxy;
};

The EstimateRisk module is just a helper module for the other AI 
modules. It (somehow!) estimates the risk involved in attacking a target 
creature.

class AI_EstimateRisk : public AI_Algorithm {
public:
  AI_EstimateRisk(Controller&);
  AI_EstimateRisk(Controller&, Creature&);

  void SetTarget(Creature&);

  int GetRiskFactor() const;

  virtual void DoAIAction();
};

The GuardArea module works just like the PatrolArea module until an 
intruder is noticed within the patrol area. At this point the module 
estimates the risk (the aggressiveness of the creature can be adjusted 
through the SetMaxRisk() method) and possibly attacks the intruder 
using the ChaseCreature module.

class AI_GuardArea : public AI_Algorithm {
public:
  AI_GuardArea(Controller&, Polygon&);

  void SetMaxRisk(int);

// Move along the path (f_patrol) until and each step check the
// creture's line of sight for intruders.

  virtual void DoAIAction();

private:
  AI_PatrolArea f_patrol;
  AI_ChaseCreature f_attack;
  AI_EstimateRisk f_risk;
};

By combining existing module like done above, any kind of module from 
AI_ScavengeEquipment to AI_KillEverything could be constructed with 
moderate ease. A complete AI module would combine more than a dozen 
sub-modules like WanderRandomly, SearchFood, DefendHome etc. to simulate 
the actions of a realistic, living creature. For my own roguelike, I 
hope to have different creatures use different kind of AI modules, so 
that each creature type has a distinct behaviour (from a cowardly 
stalker to an aggressive hunter). 

If you have any questions, ideas, comments etc. feel free to email me.

Sami Hangaslammi (mailto:shang@st.jyu.fi)
Developer of Portals (http://www.jyu.fi/~shang/portals)