LuaAI

From The Battle for Wesnoth Wiki
Revision as of 21:07, 31 January 2010 by Crab (talk | contribs) (The role and responsibilities of ai support engine)

Good lua support for Wesnoth's AI would be cool.

The role and responsibilities of ai support engine

AI is constructed from components. AI support engines (src/ai/composite/engine_*.?pp) are stateful components in the context of the ai for the specific side, responsible with taking config snippets and translating them into c++ components. So, each AI has its own copy of engines.

[stage]
 # this stage will be parsed by default cpp engine
 {ANYTHING_HERE_CAN_BE_CPP_ENGINE_SPECIFIC}
[/stage]
[stage]
 engine=cpp
 # this stage will be parsed by default cpp engine
 {ANYTHING_HERE_CAN_BE_CPP_ENGINE_SPECIFIC}
[/stage]
[stage]
 engine=fai
 # this stage will be parsed by formula ai engine
 {ANYTHING_HERE_CAN_BE_FAI_ENGINE_SPECIFIC}
[/stage]
[stage]
 engine=lua
 # this stage will be parsed by lua engine
 {ANYTHING_HERE_CAN_BE_LUA_ENGINE_SPECIFIC}
[/stage]
[candidate_action]
 # this candidate_action will be parsed by default cpp engine
 {ANYTHING_HERE_CAN_BE_CPP_ENGINE_SPECIFIC}
[/candidate_action]
[candidate_action]
 engine=cpp
 # this candidate_action will be parsed by default cpp engine
 {ANYTHING_HERE_CAN_BE_CPP_ENGINE_SPECIFIC}
[/candidate_action]
[candidate_action]
 engine=fai
 # this candidate_action will be parsed by formula ai engine
 {ANYTHING_HERE_CAN_BE_FAI_ENGINE_SPECIFIC}
[/candidate_action
[candidate_action]
 engine=lua
 # this candidate_action will be parsed by lua engine
 {ANYTHING_HERE_CAN_BE_LUA_ENGINE_SPECIFIC}
[/candidate_action]

So, I've committed (r40744) a stub lua ai support engine , src/ai/composite/engine_lua.cpp

There's 1 such engine for each side, and each can hold distinct state associated with this side.

We need this engine to parse [stage] and [candidate_action] wml tags, and create ai components from them.

This engine is constructed when the ai is not yet fully initialized, so, the constructor is passed two arguments: the config of the engine (if present, i.e. when loading a savegame or a scenario where the engine config is already present) and the readonly_context (which provides functions to access the game state but not to modify it)

At this point, ai aspects and ai goals are created. Supporting writing aspects and goals in lua is not first-priority (since they tend to have a simple config like 'target units standing on locations matching this SLF' or 'aggression=0.5' and can be written in plain WML), but, it would be interesting to have in the future (since some aspects handle calculation of interesting things like attack ratings)

When the ai is fully initialized, full ai_context is passed to the engine. Then, ai stages and their parts are parsed. At this point we're interested in parsing stages and candidate actions, so lua engine has to override those methods from engine which deal with parsing stages and candidate actions from config.

stage - stateful object in the context of the ai for the specific side, it's just a 'part of ai turn' with do_play_stage() method. candidate_action - stateful object in the context of the candidate action evaluation loop stage, it's 'evaluator+executor' pair (if THIS, than doing THAT has score SCORE)

Exposing game state

ai_context includes functions which provide access to

  1. current side number
  2. unit map
  3. game map
  4. information about teams, villages
  5. information about turn and time of day
  6. scenario info such as income per turn
  7. current values of ai aspects (aggression, attack_depth, avoid, passive_leader, passive_leader_shares_keep, leader_aggression,caution, grouping, leader_goal, leader_value, number_of_possible_recruits_to_force_recruit, recruitment_ignore_bad_combat, recruitment_ignore_bad_movement, recruitment_pattern, scout_village_targeting, simple_targeting, support_villages, village_value, villages_per_scout,...)
  8. on-demand calculated and auto-invalidated caches like movement maps.
  9. action handlers (more on them below)

Invalidation system

During AI turn, the only time when the gamestate can change is after ai action (move/attack/recruit/recall/etc). So, there's a mechanism which allows to listen for 'gamestate changed' events. This allows to set up on-demand calculated and auto-invalidated caches. to allow to listen to gamestate_changed events:

ai::manager::add_gamestate_observer is the method to call in the constructor.
ai::manager::remove_gamestate_observer is the method to call in the destructor.

This might be useful for lua (e.g. on first access, convert a movement_map to lua table, and only invalidate it if gamestate has changed)

Exposing action handlers

action handlers - 'move,attack,recruit,recall,stop' actions from src/ai/actions.cpp must be made available to lua code. there are helper functions for that in readwrite_context

check_attack_action(
     const map_location& attacker_loc,
     const map_location& defender_loc,
     int attacker_weapon)
check_move_action(
     const map_location& from,
     const map_location& to, bool remove_movement=true)
check_recall_action(
     const std::string& id,
     const map_location &where = map_location::null_location)
check_recruit_action(
     const std::string& unit_name,
     const map_location &where = map_location::null_location)
check_stopunit_action(
     const map_location& unit_location,
     bool remove_movement = true,
     bool remove_attacks = false)
execute_attack_action(
     const map_location& attacker_loc,
     const map_location& defender_loc,
     int attacker_weapon)
execute_move_action(
     const map_location& from,
     const map_location& to,
     bool remove_movement=true)
execute_recall_action(
     const std::string& id,
     const map_location &where = map_location::null_location)
execute_recruit_action(
     const std::string& unit_name,
     const map_location &where = map_location::null_location)
execute_stopunit_action(
     const map_location& unit_location,
     bool remove_movement = true,
     bool remove_attacks = false)

Note that it's possible to check if the action is allowed before actually executing it, using the ->execute() function on the smart pointer object returned by check. the c++ code, when used in candidate_actions, is usually similar to this:

---evaluation---

move_ = check_move_action(leader->first,keep,false);
if (move_->is_ok()){
    return get_score();
}

---later, if selected for execution---

move_->execute();

Note: (1) action (or the list of actions) is usually cached between evaluation and execution (2) it is possible to check if the move is 'ok' without executing it.

Notes

P.S. it is, probably, also needed to somehow prohibit modification of game state via lua handlers (i.e. prohibit calling stuff such as wesnoth.fire() from AIs). also it's important to prevent changes of state of ai for side A by ai of side B. For start,it's probably enough to prevent only accidential changes, but, if it's possible to achieve a proper separation between lua code for different AIs, it'll be a good thing.