-
-
Notifications
You must be signed in to change notification settings - Fork 990
/
micro_ai_helper.lua
205 lines (176 loc) · 7.89 KB
/
micro_ai_helper.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
local H = wesnoth.require "helper"
local T = wml.tag
local MAIUV = wesnoth.require "ai/micro_ais/micro_ai_unit_variables.lua"
local micro_ai_helper = {}
function micro_ai_helper.add_CAs(side, ca_id_core, CA_parms, CA_cfg)
-- Add the candidate actions defined in @CA_parms to the AI of @side
-- @ca_id_core: ca_id= key from the [micro_ai] tag
-- @CA_parms: array of tables, one for each CA to be added (CA setup parameters)
-- Also contains one key: ai_id
-- @CA_cfg: table with the parameters passed to the eval/exec functions
--
-- Required keys for each table of @CA_parms:
-- - ca_id: is used for CA id/name
-- - location: the path+file name for the external CA file
-- - score: the evaluation score
-- About ai_id, ca_id_core and ca_id:
-- ai_id: If the AI stores information in the [data] variable, we need to
-- ensure that it is uniquely attributed to this AI, and not to a separate
-- AI of the same type. ai_id is used for this and must therefore be unique.
-- We ensure this by checking if CAs or [data][micro_ai] tags using the
-- default ai_id value exist already and if so, by adding numbers to the end
-- until we find an id that is not used yet.
-- ca_id_core: This is used as base for the id= and name= keys of the
-- [candidate_action] tags. If [micro_ai]ca_id= is given, we use it as is
-- without checking if an AI with this id already exists. This is required in
-- order to ensure that removal with action=delete is possible and it is the
-- responsibility of the user to ensure uniqueness. If [micro_ai]ca_id= is not
-- given, use ai_id for ca_id_core, which also makes ids unique for this case.
-- ca_id: This is specific to the individual CAs of an AI and is added to
-- ca_id_core for the names and ids of each CA.
local ai_id, id_found = CA_parms.ai_id, true
local n = 1
while id_found do -- This is really just a precaution
id_found = false
for ai_tag in wml.child_range(wesnoth.sides[side].__cfg, 'ai') do
for stage in wml.child_range(ai_tag, 'stage') do
for ca in wml.child_range(stage, 'candidate_action') do
if string.find(ca.name, ai_id .. '_') then
id_found = true
break
end
end
end
end
-- Ideally, we would also delete previous occurrences of [micro_ai] tags in the
-- AI's data variable. However, the MAI can be changed while it is not
-- the AI's turn, when this is not possible. So instead, we check for the
-- existence of such tags and make sure we are using a different ai_id.
for ai_tag in wml.child_range(wesnoth.sides[side].__cfg, 'ai') do
for engine in wml.child_range(ai_tag, 'engine') do
for data in wml.child_range(engine, 'data') do
for mai in wml.child_range(data, 'micro_ai') do
if (mai.ai_id == ai_id) then
id_found = true
break
end
end
end
end
end
if (id_found) then ai_id = CA_parms.ai_id .. n end
n = n + 1
end
-- For CA ids and names, use value of [micro_ai]ca_id= if given, ai_id otherwise
ca_id_core = ca_id_core or ai_id
-- Now add the CAs
for _,parms in ipairs(CA_parms) do
local ca_id = ca_id_core .. '_' .. parms.ca_id
-- Always pass the ai_id and ca_score to the eval/exec functions
CA_cfg.ai_id = ai_id
CA_cfg.ca_score = parms.score
local CA = {
engine = "lua",
id = ca_id,
name = ca_id,
max_score = parms.score
}
CA.location = parms.location
table.insert(CA, T.args(CA_cfg))
wesnoth.add_ai_component(side, "stage[main_loop].candidate_action", CA)
end
end
function micro_ai_helper.delete_CAs(side, ca_id_core, CA_parms)
-- Delete the candidate actions defined in @CA_parms from the AI of @side
-- @ca_id_core: ca_id= key from the [micro_ai] tag
-- @CA_parms: array of tables, one for each CA to be removed
-- We can simply pass the one used for add_CAs(), although only the
-- CA_parms.ca_id field is needed
-- For CA ids, use value of [micro_ai]ca_id= if given, ai_id otherwise
ca_id_core = ca_id_core or CA_parms.ai_id
for _,parms in ipairs(CA_parms) do
local ca_id = ca_id_core .. '_' .. parms.ca_id
wesnoth.delete_ai_component(side, "stage[main_loop].candidate_action[" .. ca_id .. "]")
-- Also need to delete variable stored in all units of the side, so that later MAIs can use these units
local units = wesnoth.get_units { side = side }
for _,unit in ipairs(units) do
MAIUV.delete_mai_unit_variables(unit, CA_parms.ai_id)
end
end
end
function micro_ai_helper.add_aspects(side, aspect_parms)
-- Add the aspects defined in @aspect_parms to the AI of @side
-- @aspect_parms is an array of tables, one for each aspect to be added
--
-- Required keys for @aspect_parms:
-- - aspect: the aspect name (e.g. 'attacks' or 'aggression')
-- - facet: A table describing the facet to be added
--
-- Examples of facets:
-- 1. Simple aspect, e.g. aggression
-- { value = 0.99 }
--
-- 2. Composite aspect, e.g. attacks
-- { name = "ai_default_rca::aspect_attacks",
-- id = "dont_attack",
-- invalidate_on_gamestate_change = "yes",
-- { "filter_own", {
-- type = "Dark Sorcerer"
-- } }
-- }
for _,parms in ipairs(aspect_parms) do
wesnoth.add_ai_component(side, "aspect[" .. parms.aspect .. "].facet", parms.facet)
end
end
function micro_ai_helper.delete_aspects(side, aspect_parms)
-- Delete the aspects defined in @aspect_parms from the AI of @side
-- @aspect_parms is an array of tables, one for each aspect to be removed
-- We can simply pass the one used for add_aspects(), although only the
-- aspect_parms.aspect_id field is needed
for _,parms in ipairs(aspect_parms) do
wesnoth.delete_ai_component(side, "aspect[attacks].facet[" .. parms.facet.id .. "]")
end
end
function micro_ai_helper.micro_ai_setup(cfg, CA_parms, required_keys, optional_keys)
if (cfg.action == 'delete') then
micro_ai_helper.delete_CAs(cfg.side, cfg.ca_id, CA_parms)
return
end
-- Otherwise, set up the cfg table to be passed to the CA eval/exec functions
local CA_cfg = {}
-- Required keys
for _,v in pairs(required_keys) do
if v:match('%[[a-zA-Z0-9_]+%]') then
v = v:sub(2,-2)
if not wml.get_child(cfg, v) then
H.wml_error("[micro_ai] tag (" .. cfg.ai_type .. ") is missing required parameter: [" .. v .. "]")
end
for child in wml.child_range(cfg, v) do
table.insert(CA_cfg, T[v](child))
end
else
if not cfg[v] then
H.wml_error("[micro_ai] tag (" .. cfg.ai_type .. ") is missing required parameter: " .. v .."=")
end
CA_cfg[v] = cfg[v]
end
end
-- Optional keys
for _,v in pairs(optional_keys) do
if v:match('%[[a-zA-Z0-9_]+%]') then
v = v:sub(2,-2)
for child in wml.child_range(cfg, v) do
table.insert(CA_cfg, T[v](child))
end
else
CA_cfg[v] = cfg[v]
end
end
-- Finally, set up the candidate actions themselves
if (cfg.action == 'add') then micro_ai_helper.add_CAs(cfg.side, cfg.ca_id, CA_parms, CA_cfg) end
if (cfg.action == 'change') then
micro_ai_helper.delete_CAs(cfg.side, cfg.ca_id, CA_parms)
micro_ai_helper.add_CAs(cfg.side, cfg.ca_id, CA_parms, CA_cfg)
end
end
return micro_ai_helper