-
-
Notifications
You must be signed in to change notification settings - Fork 991
/
micro_ai_helper.lua
213 lines (184 loc) · 7.42 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
206
207
208
209
210
211
212
213
local H = wesnoth.require "lua/helper.lua"
local W = H.set_wml_action_metatable {}
local T = H.set_wml_tag_metatable {}
local AH = wesnoth.require("ai/lua/ai_helper.lua")
local MAIUV = wesnoth.require "ai/micro_ais/micro_ai_unit_variables.lua"
local micro_ai_helper = {}
function micro_ai_helper.add_CAs(side, CA_parms, CA_cfg)
-- Add the candidate actions defined in @CA_parms to the AI of @side
-- @CA_parms is an array of tables, one for each CA to be added (CA setup parameters)
-- and also contains one key: ai_id
-- @CA_cfg is a 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
-- We need to make sure that the id/name of each CA are unique.
-- We do this by checking if CAs starting with ai_id exist already
-- If yes, we add numbers to the end of ai_id until we find an id that does not exist yet
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 H.child_range(wesnoth.sides[side].__cfg, 'ai') do
for stage in H.child_range(ai_tag, 'stage') do
for ca in H.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 H.child_range(wesnoth.sides[side].__cfg, 'ai') do
for engine in H.child_range(ai_tag, 'engine') do
for data in H.child_range(engine, 'data') do
for mai in H.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
-- Now add the CAs
for _,parms in ipairs(CA_parms) do
local ca_id = ai_id .. '_' .. 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))
W.modify_ai {
side = side,
action = "add",
path = "stage[main_loop].candidate_action",
T.candidate_action(CA)
}
end
end
function micro_ai_helper.delete_CAs(side, CA_parms)
-- Delete the candidate actions defined in @CA_parms from the AI of @side
-- @CA_parms is an 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 _,parms in ipairs(CA_parms) do
local ca_id = CA_parms.ai_id .. '_' .. parms.ca_id
W.modify_ai {
side = side,
action = "try_delete",
path = "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
W.modify_ai {
side = side,
action = "add",
path = "aspect[" .. parms.aspect .. "].facet",
T.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
W.modify_ai {
side = side,
action = "try_delete",
path = "aspect[attacks].facet[" .. parms.aspect_id .. "]"
}
end
end
function micro_ai_helper.micro_ai_setup(cfg, CA_parms, required_keys, optional_keys)
-- If cfg.ca_id is set, it gets used as the ai_id= key
-- This allows for selective removal of CAs
-- Note: the ca_id key of the [micro_ai] tag should really be renamed to ai_id,
-- but that would mean breaking backward compatibility, so we'll just deal with it internally instead
CA_parms.ai_id = cfg.ca_id or CA_parms.ai_id
-- If action=delete, we do that and are done
if (cfg.action == 'delete') then
micro_ai_helper.delete_CAs(cfg.side, 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 H.get_child(cfg, v) then
H.wml_error("[micro_ai] tag (" .. cfg.ai_type .. ") is missing required parameter: [" .. v .. "]")
end
for child in H.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 H.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, CA_parms, CA_cfg) end
if (cfg.action == 'change') then
micro_ai_helper.delete_CAs(cfg.side, CA_parms, cfg.id)
micro_ai_helper.add_CAs(cfg.side, CA_parms, CA_cfg)
end
end
return micro_ai_helper