This is very similar to how one might handle Achievements, or Challenges, or any other situation needing you to keep track of a series of events and counters.
Simply coding the rules into a scripting language does work. You would need to be sure that all events related to conditions you care about are exposed to the language. You might also need to allow scripts to register conditions and current values with an external UI system, of course.
A more structured version of this would be to just keep a list of conditions, current state, and so on. In many ways, you'd just be building a limited and very domain-specific scripting system; the advantages of this approach are that any other interested system (the UI, your map editor, debug console, etc.) can trivially integrate with this system as there is a well-defined structure to how it works.
If you're trying to throw something together that works with the minimal pain and effort, I'd probably go with the first approach (scripting). If you plan to put in the time to make a high-quality map editor for user-created content or want to develop very high-quality debugging tools (which map designers love, even if they're your in-house designers), I would go with the second option.
The second option can be coded up using either C-like arrays of structs/unions with enumerations that define requirements for conditions, e.g.:
enum condition_t { controlled_houses, killed_enemies };
struct condition_defition_t {
condition_t type;
int data; // number of houses required, or number of killed enemies required, etc.
};
condition_definition_t* conditions; // per-map; load this data from your map file
int evaluate_for_player(condition_definition_t* condition, size_t num_conditions, player_t* player) {
for (size_t i = 0; i != num_conditions; ++condition) {
switch (condition->type) {
case controlled_houses:
if (player->houses < condition->data)
return 0; // does not have enough houses to win
break;
case killed_enemies:
if (player->kills < condition->data)
return 0; // did not kill enough enemies
break;
}
return 1; // all conditions have passed
}
With a little extra work, you can also support OR
and AND
conditions, ranges, or whatever other complexity you need for your game design. It's often better to avoid state in the condition itself; e.g., if the condition is "controlled castle for 5 turns" then just keep track of how long a player has controlled the castle in the player, and the condition simply checks that value. As your conditions grow in complexity, you may need to add some kind of per-player condition_state_t
, though.
A polymorphic approach to the above would just replace the enum and the big switch statement with an abstract base class and derived classes that implement specific rules, e.g.
struct ICondition {
virtual bool IsSatisfied(Player const& player) = 0;
};
class KilledEnemiesCondition : public ICondition {
int _requiredKills;
public;
KilledEnemiesCondition(int num_required) : _requiredKills(num_required) {}
bool IsSatisfied(Player const& player) override {
return player.GetNumKilledEnemies() >= _requiredKills;
}
};
class OrCondition : public ICondition {
std::vector<std::unique_ptr<ICondition>> _conditions;
public:
bool IsSatisfied(Player const& player) override {
return std::find_if(_conditions.begin(), _conditions.end(), std::mem_fun(&ICondition::IsSatisfied)) != _conditions.end();
}
};
And the loop would use that interface. As shown, this approach does make it a bit easier to add more complex conditions like an OR
condition. Loading the conditions from file would likely need extra work to deserialize the definitions into the proper classes with the proper data, of course, and you'll probably find the polymorphic version to just be bigger and more complex; that complexity may or may not be worth it to you, depending on various factors (e.g., required complexity of conditions, the API you want for exposing these to other systems, whether you already have a good serialization system in place, and your comfort level with that programming paradigm).