1local H = wesnoth.require "helper"
2local W = H.set_wml_action_metatable {}
3local LS = wesnoth.require "location_set"
4local F = wesnoth.require "functional"
5local M = wesnoth.map
6
7-- This is a collection of Lua functions used for custom AI development.
8-- Note that this is still work in progress with significant changes occurring
9-- frequently. Backward compatibility cannot be guaranteed at this time in
10-- development releases, but it is of course easily possible to copy a function
11-- from a previous release directly into an add-on if it is needed there.
12--
13-- Invisible units ('viewing_side' parameter):
14-- With their default settings, the ai_helper functions use the vision a player of
15-- the respective side would see, that is, they assume no knowledge of invisible
16-- units. This can be influenced with the 'viewing_side' parameter, which works
17-- in the same way as it does in wesnoth.find_reach() and wesnoth.find_path():
18--   - If set to a valid side number, vision for that side is used
19--   - If set to an invalid side number (e.g. 0), all units on the map are seen
20--   - If omitted and a function takes a a parameter linked to a specific side,
21--     such as a side number or a unit, as input, vision of that side is used. In
22--     this case, viewing_side is passed as part of the optional @cfg configuration
23--     table and can be passed from function to function.
24--   - If omitted and the function takes no such input, viewing_side is made a
25--     required parameter in order to avoid mismatches between the default values
26--     of different functions.
27--
28-- Path finding:
29-- All ai_helper functions disregard shroud for path finding (while still ignoring
30-- hidden units correctly) as of Wesnoth 1.13.7. This is consistent with default
31-- Wesnoth AI behavior and ensures that Lua AIs, including the Micro AIs, can be
32-- used for AI sides with shroud=yes. It is accomplished by using
33-- ai_helper.find_path_with_shroud() instead of wesnoth.find_path().
34
35local ai_helper = {}
36
37----- Debugging helper functions ------
38
39function ai_helper.show_messages()
40    -- Returns true or false (hard-coded). To be used to
41    -- show messages if in debug mode.
42    -- Just edit the following line (easier than trying to set WML variable)
43    local show_messages_flag = false
44    if wesnoth.game_config.debug then return show_messages_flag end
45    return false
46end
47
48function ai_helper.print_exec()
49    -- Returns true or false (hard-coded). To be used to
50    -- show which CA is being executed if in debug mode.
51    -- Just edit the following line (easier than trying to set WML variable)
52    local print_exec_flag = false
53    if wesnoth.game_config.debug then return print_exec_flag end
54    return false
55end
56
57function ai_helper.print_eval()
58    -- Returns true or false (hard-coded). To be used to
59    -- show which CA is being evaluated if in debug mode.
60    -- Just edit the following line (easier than trying to set WML variable)
61    local print_eval_flag = false
62    if wesnoth.game_config.debug then return print_eval_flag end
63    return false
64end
65
66function ai_helper.done_eval_messages(start_time, ca_name)
67    ca_name = ca_name or 'unknown'
68    local dt = wesnoth.get_time_stamp() / 1000. - start_time
69    if ai_helper.print_eval() then
70        ai_helper.print_ts_delta(start_time, '       - Done evaluating ' .. ca_name .. ':')
71    end
72end
73
74function ai_helper.clear_labels()
75    -- Clear all labels on a map
76    local width, height = wesnoth.get_map_size()
77    for x = 1,width do
78        for y = 1,height do
79            W.label { x = x, y = y, text = "" }
80        end
81    end
82end
83
84function ai_helper.put_labels(map, cfg)
85    -- Take @map (location set) and put label containing 'value' onto the map.
86    -- Print 'nan' if element exists but is not a number.
87    -- @cfg: table with optional configuration parameters:
88    --   - show_coords: (boolean) use hex coordinates as labels instead of value
89    --   - factor=1: (number) if value is a number, multiply by this factor
90    --   - keys: (array) if the value to be displayed is a subelement of the LS data,
91    --     use these keys to access it. For example, if we want to display data[3]
92    --     set keys = { 3 }, if it's data.arg[3], set keys = { 'arg', 3 }
93
94    cfg = cfg or {}
95    local factor = cfg.factor or 1
96
97    ai_helper.clear_labels()
98
99    map:iter(function(x, y, data)
100        local out
101        if cfg.show_coords then
102            out = x .. ',' .. y
103        else
104            if cfg.keys then
105                for _,key in ipairs(cfg.keys) do data = data[key] end
106            end
107            if (type(data) == 'string') then
108                out = data
109            else
110                out = tonumber(data) or 'nan'
111            end
112        end
113
114        if (type(out) == 'number') then out = out * factor end
115        W.label { x = x, y = y, text = out }
116    end)
117end
118
119function ai_helper.print_ts(...)
120    -- Print arguments preceded by a time stamp in seconds
121    -- Also return that time stamp
122
123    local ts = wesnoth.get_time_stamp() / 1000.
124
125    local arg = { ... }
126    arg[#arg+1] = string.format('[ t = %.3f ]', ts)
127
128    std_print(table.unpack(arg))
129
130    return ts
131end
132
133function ai_helper.print_ts_delta(start_time, ...)
134    -- @start_time: time stamp in seconds as returned by wesnoth.get_time_stamp / 1000.
135
136    -- Same as ai_helper.print_ts(), but also adds time elapsed since
137    -- the time given in the first argument (in seconds)
138    -- Returns time stamp as well as time elapsed
139
140    local ts = wesnoth.get_time_stamp() / 1000.
141    local delta = ts - start_time
142
143    local arg = { ... }
144    arg[#arg+1] = string.format('[ t = %.3f, dt = %.3f ]', ts, delta)
145
146    std_print(table.unpack(arg))
147
148    return ts, delta
149end
150
151----- AI execution helper functions ------
152
153function ai_helper.is_incomplete_move(check)
154    if (not check.ok) then
155        -- Legitimately interrupted moves have the following error codes:
156        -- E_AMBUSHED = 2005
157        -- E_FAILED_TELEPORT = 2006,
158        -- E_NOT_REACHED_DESTINATION = 2007
159        if (check.status == 2005) or (check.status == 2006) or (check.status == 2007) then
160            return check.status
161        end
162    end
163
164    return false
165end
166
167function ai_helper.is_incomplete_or_empty_move(check)
168    if (not check.ok) then
169        -- Empty moves have the following error code:
170        -- E_EMPTY_MOVE = 2001
171        if ai_helper.is_incomplete_move(check) or (check.status == 2001) then
172            return check.status
173        end
174    end
175
176    return false
177end
178
179function ai_helper.dummy_check_action(gamestate_changed, ok, result, status)
180    return {
181        gamestate_changed = gamestate_changed or false,
182        ok = ok or false,
183        result = result or 'ai_helper::DUMMY_FAILED_ACTION',
184        status = status or 99999
185    }
186end
187
188function ai_helper.checked_action_error(action, error_code)
189    if wesnoth.game_config.debug then
190        error(action .. ' could not be executed. Error code: ' .. error_code, 3)
191    end
192end
193
194function ai_helper.checked_attack(ai, attacker, defender, weapon)
195    local check = ai.check_attack(attacker, defender, weapon)
196
197    if (not check.ok) then
198        ai.stopunit_attacks(attacker)
199        ai_helper.checked_action_error('ai.attack from ' .. attacker.x .. ',' .. attacker.y .. ' to ' .. defender.x .. ',' .. defender.y, check.status .. ' (' .. check.result .. ')')
200        return check
201    end
202
203    return ai.attack(attacker, defender, weapon)
204end
205
206function ai_helper.checked_move_core(ai, unit, x, y, move_type)
207    local check = ai.check_move(unit, x, y)
208
209    if (not check.ok) then
210        if (not ai_helper.is_incomplete_or_empty_move(check)) then
211            ai.stopunit_moves(unit)
212            ai_helper.checked_action_error(move_type .. ' from ' .. unit.x .. ',' .. unit.y .. ' to ' .. x .. ',' .. y, check.status .. ' (' .. check.result .. ')')
213            return check
214        end
215    end
216
217    if (move_type == 'ai.move_full') then
218        return ai.move_full(unit, x, y)
219    else
220        return ai.move(unit, x, y)
221    end
222end
223
224function ai_helper.checked_move_full(ai, unit, x, y)
225    return ai_helper.checked_move_core(ai, unit, x, y, 'ai.move_full')
226end
227
228function ai_helper.checked_move(ai, unit, x, y)
229    return ai_helper.checked_move_core(ai, unit, x, y, 'ai.move')
230end
231
232function ai_helper.checked_recruit(ai, unit_type, x, y)
233    local check = ai.check_recruit(unit_type, x, y)
234
235    if (not check.ok) then
236        ai_helper.checked_action_error('ai.recruit of ' .. unit_type .. ' at ' .. x .. ',' .. y, check.status .. ' (' .. check.result .. ')')
237        return check
238    end
239
240    return ai.recruit(unit_type, x, y)
241end
242
243function ai_helper.checked_stopunit_all(ai, unit)
244    local check = ai.check_stopunit(unit)
245
246    if (not check.ok) then
247        ai_helper.checked_action_error('ai.stopunit_all of ' .. unit.x .. ',' .. unit.y, check.status .. ' (' .. check.result .. ')')
248        return check
249    end
250
251    return ai.stopunit_all(unit)
252end
253
254function ai_helper.checked_stopunit_attacks(ai, unit)
255    local check = ai.check_stopunit(unit)
256
257    if (not check.ok) then
258        ai_helper.checked_action_error('ai.stopunit_attacks of ' .. unit.x .. ',' .. unit.y, check.status .. ' (' .. check.result .. ')')
259        return check
260    end
261
262    return ai.stopunit_attacks(unit)
263end
264
265function ai_helper.checked_stopunit_moves(ai, unit)
266    local check = ai.check_stopunit(unit)
267
268    if (not check.ok) then
269        ai_helper.checked_action_error('ai.stopunit_moves of ' .. unit.x .. ',' .. unit.y, check.status .. ' (' .. check.result .. ')')
270        return check
271    end
272
273    return ai.stopunit_moves(unit)
274end
275
276function ai_helper.robust_move_and_attack(ai, src, dst, target_loc, cfg)
277    -- Perform a move and/or attack with an AI unit in a way that is robust against
278    -- unexpected outcomes such as being ambushed or changes caused by WML events.
279    -- As much as possible, this function also tries to ensure that the gamestate
280    -- is changed in case an action turns out to be impossible due to such an
281    -- unexpected outcome.
282    --
283    -- Required input parameters:
284    -- @ai: the Lua ai table
285    -- @src: current coordinates of the AI unit to be used
286    -- @dst: coordinates to which the unit should move. This does not have to be
287    --   different from @src. In fact, the unit does not even need to have moves
288    --   left, as long as an attack is specified in the latter case. If another
289    --   AI unit is at @dst, it is moved out of the way.
290    --
291    -- Optional parameters:
292    -- @target_loc: coordinates of the enemy unit to be attacked. If not given, no
293    --   attack is attempted.
294    -- @cfg: table with optional configuration parameters:
295    --   partial_move: By default, this function performs full moves. If this
296    --     parameter is true, a partial move is done instead.
297    --   weapon: The number (starting at 1) of the attack weapon to be used.
298    --     If omitted, the best weapon is automatically selected.
299    --   all optional parameters for ai_helper.move_unit_out_of_way()
300
301    -- Notes:
302    -- - @src, @dst and @target_loc can be any table (including proxy units) that contains
303    --   the coordinates of the respective locations using either indices .x/.y or [1]/[2].
304    --   If both are given, .x/.y takes precedence over [1]/[2].
305    -- - This function only safeguards AI moves against outcomes that the AI cannot know
306    --   about, such as hidden units and WML events. It is assumed that the potential
307    --   move was tested for general feasibility (units are on AI side and have moves
308    --   left, terrain is passable, etc.) beforehand. If that is not done, it might
309    --   lead to very undesirable behavior, incl. the CA being blacklisted or even the
310    --   entire AI turn being ended.
311
312    local src_x, src_y = src.x or src[1], src.y or src[2] -- this works with units or locations
313    local dst_x, dst_y = dst.x or dst[1], dst.y or dst[2]
314
315    local unit = wesnoth.get_unit(src_x, src_y)
316    if (not unit) then
317        return ai_helper.dummy_check_action(false, false, 'robust_move_and_attack::NO_UNIT')
318    end
319
320    -- Getting target at beginning also, in case events mess up things along the way
321    local target, target_x, target_y
322    if target_loc then
323        target_x, target_y = target_loc.x or target_loc[1], target_loc.y or target_loc[2]
324        target = wesnoth.get_unit(target_x, target_y)
325
326        if (not target) then
327            return ai_helper.dummy_check_action(false, false, 'robust_move_and_attack::NO_TARGET')
328        end
329    end
330
331    local gamestate_changed = false
332    local move_result = ai_helper.dummy_check_action(false, false, 'robust_move_and_attack::NO_ACTION')
333    if (unit.moves > 0) then
334        if (src_x == dst_x) and (src_y == dst_y) then
335            move_result = ai.stopunit_moves(unit)
336
337            -- The only possible failure modes are non-recoverable (such as E_NOT_OWN_UNIT)
338            if (not move_result.ok) then return move_result end
339
340            if (not unit) or (not unit.valid) then
341                return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::UNIT_DISAPPEARED')
342            end
343
344            gamestate_changed = true
345        else
346            local unit_old_moves = unit.moves
347
348            local unit_in_way = wesnoth.get_unit(dst_x, dst_y)
349            if unit_in_way and (unit_in_way.side == wesnoth.current.side) and (unit_in_way.moves > 0) then
350                local uiw_old_moves = unit_in_way.moves
351                ai_helper.move_unit_out_of_way(ai, unit_in_way, cfg)
352
353                if (not unit_in_way) or (not unit_in_way.valid) then
354                    return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::UNIT_IN_WAY_DISAPPEARED')
355                end
356
357                -- Failed move out of way: abandon remaining actions
358                if (unit_in_way.x == dst_x) and (unit_in_way.y == dst_y) then
359                    if (unit_in_way.moves == uiw_old_moves) then
360                        -- Forcing a gamestate change, if necessary
361                        ai.stopunit_moves(unit_in_way)
362                    end
363                    return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::UNIT_IN_WAY_EMPTY_MOVE')
364                end
365
366                -- Check whether dst hex is free now (an event could have done something funny)
367                local unit_in_way = wesnoth.get_unit(dst_x, dst_y)
368                if unit_in_way then
369                    return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::ANOTHER_UNIT_IN_WAY')
370                end
371
372                gamestate_changed = true
373            end
374
375            if (not unit) or (not unit.valid) or (unit.x ~= src_x) or (unit.y ~= src_y) then
376                return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::UNIT_DISAPPEARED')
377            end
378
379            local check_result = ai.check_move(unit, dst_x, dst_y)
380            if (not check_result.ok) then
381                if (not ai_helper.is_incomplete_or_empty_move(check_result)) then
382                    if (not gamestate_changed) then
383                        ai.stopunit_moves(unit)
384                    end
385                    return check_result
386                end
387            end
388
389            if cfg and cfg.partial_move then
390                move_result = ai.move(unit, dst_x, dst_y)
391            else
392                move_result = ai.move_full(unit, dst_x, dst_y)
393            end
394            if (not move_result.ok) then return move_result end
395
396            if (not unit) or (not unit.valid) then
397                return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::UNIT_DISAPPEARED')
398            end
399
400            -- Failed move: abandon rest of actions
401            if (unit.x == src_x) and (unit.y == src_y) then
402                if (not gamestate_changed) and (unit.moves == unit_old_moves) then
403                    -- Forcing a gamestate change, if necessary
404                    ai.stopunit_moves(unit)
405                end
406                return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::UNPLANNED_EMPTY_MOVE')
407            end
408
409            gamestate_changed = true
410        end
411    end
412
413    -- Tests after the move, before continuing to attack, to ensure WML events
414    -- did not do something funny
415    if (not unit) or (not unit.valid) then
416        return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::UNIT_DISAPPEARED')
417    end
418    if (unit.x ~= dst_x) or (unit.y ~= dst_y) then
419        return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::UNIT_NOT_AT_DESTINATION')
420    end
421
422    -- In case all went well and there's no attack to be done
423    if (not target_x) then return move_result end
424
425    if (not target) or (not target.valid) then
426        return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::TARGET_DISAPPEARED')
427    end
428    if (target.x ~= target_x) or (target.y ~= target_y) then
429        return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::TARGET_MOVED')
430    end
431
432    local weapon = cfg and cfg.weapon
433    local old_attacks_left = unit.attacks_left
434
435    local check_result = ai.check_attack(unit, target, weapon)
436    if (not check_result.ok) then
437        if (not gamestate_changed) then
438            ai.stopunit_all(unit)
439        end
440        return check_result
441    end
442
443    move_result = ai.attack(unit, target, weapon)
444    -- This should not happen, given that we just checked, but just in case
445    if (not move_result.ok) then return move_result end
446
447    if (not unit) or (not unit.valid) then
448        return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::UNIT_DISAPPEARED')
449    end
450
451    if (unit.attacks_left == old_attacks_left) and (not gamestate_changed) then
452        ai.stopunit_all(unit)
453        return ai_helper.dummy_check_action(true, false, 'robust_move_and_attack::NO_ATTACK')
454    end
455
456    return move_result
457end
458
459----- General functionality and maths helper functions ------
460
461function ai_helper.filter(input, condition)
462    return F.filter(input, condition)
463end
464
465function ai_helper.choose(input, value)
466    return F.choose(input, value)
467end
468
469function ai_helper.table_copy(t)
470    -- Make a copy of a table (rather than just another pointer to the same table)
471    local copy = {}
472    for k,v in pairs(t) do copy[k] = v end
473    return copy
474end
475
476function ai_helper.array_merge(a1, a2)
477    -- Merge two arrays without overwriting @a1 or @a2 -> create a new table
478    -- This only works with arrays, not general tables
479    local merger = {}
480    for _,a in pairs(a1) do table.insert(merger, a) end
481    for _,a in pairs(a2) do table.insert(merger, a) end
482    return merger
483end
484
485function ai_helper.serialize(input)
486    -- Convert @input to a string in a format corresponding to the type of @input
487    -- The string is all put into one line
488
489    local str = ''
490    if (type(input) == "number") or (type(input) == "boolean") then
491        str = tostring(input)
492    elseif type(input) == "string" then
493        str = string.format("%q", input)
494    elseif type(input) == "table" then
495        str = str .. "{ "
496        for k,v in pairs(input) do
497            str = str .. "[" .. ai_helper.serialize(k)  .. "] = "
498            str = str .. ai_helper.serialize(v)
499            str = str .. ", "
500        end
501        str = str .. "}"
502    else
503        error("cannot serialize a " .. type(input), 2)
504    end
505
506    return str
507end
508
509function ai_helper.split(str, sep)
510    -- Split string @str into a table using the delimiter @sep (default: ',')
511
512    local sep, fields = sep or ",", {}
513    local pattern = string.format("([^%s]+)", sep)
514    string.gsub(str, pattern, function(c) fields[#fields+1] = c end)
515    return fields
516end
517
518--------- Location set related helper functions ----------
519
520function ai_helper.get_LS_xy(index)
521    -- Get the x,y coordinates for the index of a location set
522    -- For some reason, there doesn't seem to be a LS function for this
523
524    local tmp_set = LS.create()
525    tmp_set.values[index] = 1
526    local xy = tmp_set:to_pairs()[1]
527
528    return xy[1], xy[2]
529end
530
531--------- Location, position or hex related helper functions ----------
532
533function ai_helper.cartesian_coords(x, y)
534    -- Converts coordinates from hex geometry to cartesian coordinates,
535    -- meaning that y coordinates are offset by 0.5 every other hex
536    -- Example: (1,1) stays (1,1) and (3,1) remains (3,1), but (2,1) -> (2,1.5) etc.
537    return x, y + ((x + 1) % 2) / 2.
538end
539
540function ai_helper.get_angle(from_hex, to_hex)
541    -- Returns the angle of the direction from @from_hex to @to_hex
542    -- Angle is in radians and goes from -pi to pi. 0 is toward east.
543    -- Input hex tables can be of form { x, y } or { x = x, y = y }, which
544    -- means that it is also possible to pass a unit table
545    local x1, y1 = from_hex.x or from_hex[1], from_hex.y or from_hex[2]
546    local x2, y2 = to_hex.x or to_hex[1], to_hex.y or to_hex[2]
547
548    local _, y1cart =  ai_helper.cartesian_coords(x1, y1)
549    local _, y2cart =  ai_helper.cartesian_coords(x2, y2)
550
551    return math.atan(y2cart - y1cart, x2 - x1)
552end
553
554function ai_helper.get_direction_index(from_hex, to_hex, n, center_on_east)
555    -- Returns an integer index for the direction from @from_hex to @to_hex
556    -- with the full circle divided into @n slices
557    -- 1 is always to the east, with indices increasing clockwise
558    -- Input hex tables can be of form { x, y } or { x = x, y = y }, which
559    -- means that it is also possible to pass a unit table
560    --
561    -- Optional input:
562    -- @center_on_east (false): boolean. By default, the eastern direction is the
563    -- northern border of the first slice. If this parameter is set, east will
564    -- instead be the center direction of the first slice
565
566    local d_east = 0
567    if center_on_east then d_east = 0.5 end
568
569    local angle = ai_helper.get_angle(from_hex, to_hex)
570    local index = math.floor((angle / math.pi * n/2 + d_east) % n ) + 1
571
572    return index
573end
574
575function ai_helper.get_cardinal_directions(from_hex, to_hex)
576    local dirs = { "E", "S", "W", "N" }
577    return dirs[ai_helper.get_direction_index(from_hex, to_hex, 4, true)]
578end
579
580function ai_helper.get_intercardinal_directions(from_hex, to_hex)
581    local dirs = { "E", "SE", "S", "SW", "W", "NW", "N", "NE" }
582    return dirs[ai_helper.get_direction_index(from_hex, to_hex, 8, true)]
583end
584
585function ai_helper.get_hex_facing(from_hex, to_hex)
586    local dirs = { "se", "s", "sw", "nw", "n", "ne" }
587    return dirs[ai_helper.get_direction_index(from_hex, to_hex, 6)]
588end
589
590function ai_helper.find_opposite_hex_adjacent(hex, center_hex)
591    -- Find the hex that is opposite of @hex with respect to @center_hex
592    -- Both input hexes are of format { x, y }
593    -- Output: {opp_x, opp_y} -- or nil if @hex and @center_hex are not adjacent
594    --   (or no opposite hex is found, e.g. for hexes on border)
595
596    -- If the two input hexes are not adjacent, return nil
597    if (M.distance_between(hex[1], hex[2], center_hex[1], center_hex[2]) ~= 1) then return nil end
598
599    -- Finding the opposite x position is easy
600    local opp_x = center_hex[1] + (center_hex[1] - hex[1])
601
602    -- y is slightly more tricky, because of the hexagonal shape, but there's a trick
603    -- that saves us from having to build in a lot of if statements
604    -- Among the adjacent hexes, it is the one with the correct x, and y _different_ from hex[2]
605    for xa,ya in H.adjacent_tiles(center_hex[1], center_hex[2]) do
606        if (xa == opp_x) and (ya ~= hex[2]) then return { xa, ya } end
607    end
608
609    return nil
610end
611
612function ai_helper.find_opposite_hex(hex, center_hex)
613    -- Find the hex that is opposite of @hex with respect to @center_hex
614    -- Using "square coordinate" method by JaMiT
615    -- Note: this also works for non-adjacent hexes, but might return hexes that are not on the map!
616    -- Both input hexes are of format { x, y }
617    -- Output: { opp_x, opp_y }
618
619    -- Finding the opposite x position is easy
620    local opp_x = center_hex[1] + (center_hex[1] - hex[1])
621
622    -- Going to "square geometry" for y coordinate
623    local y_sq = hex[2] * 2 - (hex[1] % 2)
624    local yc_sq = center_hex[2] * 2 - (center_hex[1] % 2)
625
626    -- Now the same equation as for x can be used for y
627    local opp_y = yc_sq + (yc_sq - y_sq)
628    opp_y = math.floor((opp_y + 1) / 2)
629
630    return {opp_x, opp_y}
631end
632
633function ai_helper.is_opposite_adjacent(hex1, hex2, center_hex)
634    -- Returns true if @hex1 and @hex2 are opposite from each other with respect to @center_hex
635
636    local opp_hex = ai_helper.find_opposite_hex_adjacent(hex1, center_hex)
637
638    if opp_hex and (opp_hex[1] == hex2[1]) and (opp_hex[2] == hex2[2]) then return true end
639    return false
640end
641
642function ai_helper.get_closest_location(hex, location_filter, unit)
643    -- Get the location closest to @hex (in format { x, y })
644    -- that matches @location_filter (in WML table format)
645    -- @unit can be passed as an optional third parameter, in which case the
646    -- terrain needs to be passable for that unit
647    -- Returns nil if no terrain matching the filter was found
648
649    -- Find the maximum distance from 'hex' that's possible on the map
650    local max_distance = 0
651    local width, height = wesnoth.get_map_size()
652    local to_top_left = M.distance_between(hex[1], hex[2], 0, 0)
653    if (to_top_left > max_distance) then max_distance = to_top_left end
654    local to_top_right = M.distance_between(hex[1], hex[2], width+1, 0)
655    if (to_top_right > max_distance) then max_distance = to_top_right end
656    local to_bottom_left = M.distance_between(hex[1], hex[2], 0, height+1)
657    if (to_bottom_left > max_distance) then max_distance = to_bottom_left end
658    local to_bottom_right = M.distance_between(hex[1], hex[2], width+1, height+1)
659    if (to_bottom_right > max_distance) then max_distance = to_bottom_right end
660
661    -- If the hex is supposed to be passable for a unit, it cannot be on the map border
662    local include_borders
663    if unit then include_borders = 'no' end
664
665    local radius = 0
666    while (radius <= max_distance) do
667        local loc_filter = {}
668        if (radius == 0) then
669            loc_filter = {
670                { "and", { x = hex[1], y = hex[2], include_borders = include_borders, radius = radius } },
671                { "and", location_filter }
672            }
673        else
674            loc_filter = {
675                { "and", { x = hex[1], y = hex[2], include_borders = include_borders, radius = radius } },
676                { "not", { x = hex[1], y = hex[2], radius = radius - 1 } },
677                { "and", location_filter }
678            }
679        end
680
681        local locs = wesnoth.get_locations(loc_filter)
682
683        if unit then
684            for _,loc in ipairs(locs) do
685                local movecost = wesnoth.unit_movement_cost(unit, wesnoth.get_terrain(loc[1], loc[2]))
686                if (movecost <= unit.max_moves) then return loc end
687            end
688        else
689            if locs[1] then return locs[1] end
690        end
691
692        radius = radius + 1
693    end
694
695    return nil
696end
697
698function ai_helper.get_passable_locations(location_filter, unit)
699    -- Finds all locations matching @location_filter that are passable for
700    -- @unit. This also excludes hexes on the map border.
701    -- @unit is optional: if omitted, all hexes matching the filter, but
702    -- excluding border hexes are returned
703
704    -- All hexes that are not on the map border
705    local width, height = wesnoth.get_map_size()
706    local all_locs = wesnoth.get_locations{
707        x = '1-' .. width,
708        y = '1-' .. height,
709        { "and", location_filter }
710    }
711
712    -- If @unit is provided, exclude terrain that's impassable for the unit
713    if unit then
714        local locs = {}
715        for _,loc in ipairs(all_locs) do
716            local movecost = wesnoth.unit_movement_cost(unit, wesnoth.get_terrain(loc[1], loc[2]))
717            if (movecost <= unit.max_moves) then table.insert(locs, loc) end
718        end
719        return locs
720    end
721
722    return all_locs
723end
724
725function ai_helper.distance_map(units, map)
726    -- Get the distance map DM for all units in @units (as a location set)
727    -- DM = sum ( distance_from_unit )
728    -- This is done for all elements of @map (a locations set), or for the entire map if @map is not given
729
730    local DM = LS.create()
731
732    if map then
733        map:iter(function(x, y, data)
734            local dist = 0
735            for _,unit in ipairs(units) do
736                dist = dist + M.distance_between(unit.x, unit.y, x, y)
737            end
738            DM:insert(x, y, dist)
739        end)
740    else
741        local width, height = wesnoth.get_map_size()
742        for x = 1,width do
743            for y = 1,height do
744                local dist = 0
745                for _,unit in ipairs(units) do
746                    dist = dist + M.distance_between(unit.x, unit.y, x, y)
747                end
748                DM:insert(x, y, dist)
749            end
750        end
751    end
752
753    return DM
754end
755
756function ai_helper.inverse_distance_map(units, map)
757    -- Get the inverse distance map IDM for all units in @units (as a location set)
758    -- IDM = sum ( 1 / (distance_from_unit+1) )
759    -- This is done for all elements of @map (a locations set), or for the entire map if @map is not given
760
761    local IDM = LS.create()
762    if map then
763        map:iter(function(x, y, data)
764            local dist = 0
765            for _,unit in ipairs(units) do
766                dist = dist + 1. / (M.distance_between(unit.x, unit.y, x, y) + 1)
767            end
768            IDM:insert(x, y, dist)
769        end)
770    else
771        local width, height = wesnoth.get_map_size()
772        for x = 1,width do
773            for y = 1,height do
774                local dist = 0
775                for _,unit in ipairs(units) do
776                    dist = dist + 1. / (M.distance_between(unit.x, unit.y, x, y) + 1)
777                end
778                IDM:insert(x, y, dist)
779            end
780        end
781    end
782
783    return IDM
784end
785
786function ai_helper.generalized_distance(x1, y1, x2, y2)
787    -- Determines distance of (@x1,@y1) from (@x2,@y2) even if
788    -- @x2 and @y2 are not necessarily both given (or not numbers)
789
790    -- Return 0 if neither is given
791    if (not x2) and (not y2) then return 0 end
792
793    -- If only one of the parameters is set
794    if (not x2) then return math.abs(y1 - y2) end
795    if (not y2) then return math.abs(x1 - x2) end
796
797    -- Otherwise, return standard distance
798    return M.distance_between(x1, y1, x2, y2)
799end
800
801function ai_helper.xyoff(x, y, ori, hex)
802    -- Finds hexes at a certain offset from @x,@y
803    -- @ori: direction/orientation: north (0), ne (1), se (2), s (3), sw (4), nw (5)
804    -- @hex: string for the hex to be queried. Possible values:
805    --   's': self, 'u': up, 'lu': left up, 'ld': left down, 'ru': right up, 'rd': right down
806    --   This is all relative "looking" in the direction of 'ori'
807    -- returns x,y for the queried hex
808
809    -- Unlike Lua default, we count 'ori' from 0 (north) to 5 (nw), so that modulo operator can be used
810    ori = ori % 6
811
812    if (hex == 's') then return x, y end
813
814    -- This is all done with ifs, to keep it as fast as possible
815    if (ori == 0)  then -- "north"
816        if (hex == 'u') then return x, y-1 end
817        if (hex == 'd') then return x, y+1 end
818        local dy = 0
819        if (x % 2) == 1 then dy=1 end
820        if (hex == 'lu') then return x-1, y-dy end
821        if (hex == 'ld') then return x-1, y+1-dy end
822        if (hex == 'ru') then return x+1, y-dy end
823        if (hex == 'rd') then return x+1, y+1-dy end
824    end
825
826    if (ori == 1)  then -- "north-east"
827        local dy = 0
828        if (x % 2) == 1 then dy=1 end
829        if (hex == 'u') then return x+1, y-dy end
830        if (hex == 'd') then return x-1, y+1-dy end
831        if (hex == 'lu') then return x, y-1 end
832        if (hex == 'ld') then return x-1, y-dy end
833        if (hex == 'ru') then return x+1, y+1-dy end
834        if (hex == 'rd') then return x, y+1 end
835    end
836
837    if (ori == 2)  then -- "south-east"
838        local dy = 0
839        if (x % 2) == 1 then dy=1 end
840        if (hex == 'u') then return x+1, y+1-dy end
841        if (hex == 'd') then return x-1, y-dy end
842        if (hex == 'lu') then return x+1, y-dy end
843        if (hex == 'ld') then return x, y-1 end
844        if (hex == 'ru') then return x, y+1 end
845        if (hex == 'rd') then return x-1, y+1-dy end
846    end
847
848    if (ori == 3)  then -- "south"
849        if (hex == 'u') then return x, y+1 end
850        if (hex == 'd') then return x, y-1 end
851        local dy = 0
852        if (x % 2) == 1 then dy=1 end
853        if (hex == 'lu') then return x+1, y+1-dy end
854        if (hex == 'ld') then return x+1, y-dy end
855        if (hex == 'ru') then return x-1, y+1-dy end
856        if (hex == 'rd') then return x-1, y-dy end
857    end
858
859    if (ori == 4)  then -- "south-west"
860        local dy = 0
861        if (x % 2) == 1 then dy=1 end
862        if (hex == 'u') then return x-1, y+1-dy end
863        if (hex == 'd') then return x+1, y-dy end
864        if (hex == 'lu') then return x, y+1 end
865        if (hex == 'ld') then return x+1, y+1-dy end
866        if (hex == 'ru') then return x-1, y-dy end
867        if (hex == 'rd') then return x, y-1 end
868    end
869
870    if (ori == 5)  then -- "north-west"
871        local dy = 0
872        if (x % 2) == 1 then dy=1 end
873        if (hex == 'u') then return x-1, y-dy end
874        if (hex == 'd') then return x+1, y+1-dy end
875        if (hex == 'lu') then return x-1, y+1-dy end
876        if (hex == 'ld') then return x, y+1 end
877        if (hex == 'ru') then return x, y-1 end
878        if (hex == 'rd') then return x+1, y-dy end
879    end
880
881    return
882end
883
884function ai_helper.split_location_list_to_strings(list)
885    -- Convert a list of locations @list as returned by wesnoth.get_locations into a pair of strings
886    -- suitable for passing in as x,y coordinate lists to wesnoth.get_locations.
887    -- Could alternatively convert to a WML table and use the find_in argument, but this is simpler.
888    local locsx, locsy = {}, {}
889    for i,loc in ipairs(list) do
890        locsx[i] = loc[1]
891        locsy[i] = loc[2]
892    end
893    locsx = table.concat(locsx, ",")
894    locsy = table.concat(locsy, ",")
895
896    return locsx, locsy
897end
898
899--------- Unit related helper functions ----------
900
901function ai_helper.get_live_units(filter)
902    -- Note: the order of the filters and the [and] tags are important for speed reasons
903    return wesnoth.get_units { { "not", { status = "petrified" } }, { "and", filter } }
904end
905
906function ai_helper.get_units_with_moves(filter, exclude_guardians)
907    -- Optional input: @exclude_guardians: set to 'true' to exclude units with ai_special=guardian
908    -- Note: the order of the filters and the [and] tags are important for speed reasons
909    local exclude_status = 'petrified'
910    if exclude_guardians then
911        exclude_status = exclude_status .. ',guardian'
912    end
913    return wesnoth.get_units {
914        { "and", { formula = "moves > 0" } },
915        { "not", { status = exclude_status } },
916        { "and", filter }
917    }
918end
919
920function ai_helper.get_units_with_attacks(filter)
921    -- Note: the order of the filters and the [and] tags are important for speed reasons
922    return wesnoth.get_units {
923        { "and", { formula = "attacks_left > 0 and size(attacks) > 0" } },
924        { "not", { status = "petrified" } },
925        { "and", filter }
926    }
927end
928
929function ai_helper.get_visible_units(viewing_side, filter)
930    -- Get units that are visible to side @viewing_side
931    --
932    -- Required parameters:
933    -- @viewing_side: see comments at beginning of this file
934    --
935    -- Optional parameters:
936    -- @filter: Standard unit filter WML table for the units
937    --   Example 1: { type = 'Orcish Grunt' }
938    --   Example 2: { { "filter_location", { x = 10, y = 12, radius = 5 } } }
939
940    if (not viewing_side) then
941        error('ai_helper.get_visible_units() is missing required parameter viewing_side.', 2)
942    end
943    if (type(viewing_side) ~= 'number') then
944        error('ai_helper.get_visible_units(): parameter viewing_side must be a number., 2')
945    end
946
947    local filter_plus_vision = {}
948    if filter then filter_plus_vision = ai_helper.table_copy(filter) end
949    if wesnoth.sides[viewing_side] then
950        table.insert(filter_plus_vision, { "filter_vision", { side = viewing_side, visible = 'yes' } })
951    end
952
953    local units = {}
954    local all_units = wesnoth.get_units()
955    for _,unit in ipairs(all_units) do
956        if unit:matches(filter_plus_vision) then
957            table.insert(units, unit)
958        end
959    end
960
961    return units
962end
963
964function ai_helper.is_visible_unit(viewing_side, unit)
965    -- Check whether @unit exists and is visible to side @viewing_side.
966    --
967    -- Required parameters:
968    -- @viewing_side: see comments at beginning of this file.
969    -- @unit: unit proxy table
970
971    if (not viewing_side) then
972        error('ai_helper.is_visible_unit() is missing required parameter viewing_side.', 2)
973    end
974    if (type(viewing_side) ~= 'number') then
975        error('ai_helper.is_visible_unit(): parameter viewing_side must be a number.', 2)
976    end
977
978    if (not unit) then return false end
979
980    if wesnoth.sides[viewing_side]
981        and unit:matches({ { "filter_vision", { side = viewing_side, visible = 'no' } } })
982    then
983        return false
984    end
985
986    return true
987end
988
989function ai_helper.get_attackable_enemies(filter, side, cfg)
990    -- Attackable enemies are defined as being being
991    --   - enemies of the side defined in @side,
992    --   - not petrified
993    --   - and visible to the side defined in @cfg.viewing_side.
994    -- For speed reasons, this is done separately, rather than calling ai_helper.get_visible_units().
995    --
996    -- Optional parameters:
997    -- @filter: Standard unit filter WML table for the enemies
998    --   Example 1: { type = 'Orcish Grunt' }
999    --   Example 2: { { "filter_location", { x = 10, y = 12, radius = 5 } } }
1000    -- @side: side number, if side other than current side is to be considered
1001    -- @cfg: table with optional configuration parameters:
1002    --   viewing_side: see comments at beginning of this file. Defaults to @side.
1003
1004    side = side or wesnoth.current.side
1005    local viewing_side = cfg and cfg.viewing_side or side
1006
1007    local filter_plus_vision = {}
1008    if filter then filter_plus_vision = ai_helper.table_copy(filter) end
1009    if wesnoth.sides[viewing_side] then
1010        table.insert(filter_plus_vision, { "filter_vision", { side = viewing_side, visible = 'yes' } })
1011    end
1012
1013    local enemies = {}
1014    local all_units = wesnoth.get_units()
1015    for _,unit in ipairs(all_units) do
1016        if wesnoth.is_enemy(side, unit.side)
1017           and (not unit.status.petrified)
1018           and unit:matches(filter_plus_vision)
1019        then
1020            table.insert(enemies, unit)
1021        end
1022    end
1023
1024    return enemies
1025end
1026
1027function ai_helper.is_attackable_enemy(unit, side, cfg)
1028    -- Check if @unit exists, is an enemy of @side, is visible to the side defined
1029    -- in @cfg.viewing_side and is not petrified.
1030    --
1031    -- Optional parameters:
1032    -- @side: side number, defaults to current side.
1033    -- @cfg: table with optional configuration parameters:
1034    --   viewing_side: see comments at beginning of this file. Defaults to @side.
1035
1036    side = side or wesnoth.current.side
1037    local viewing_side = cfg and cfg.viewing_side or side
1038
1039    if (not unit)
1040        or (not wesnoth.is_enemy(side, unit.side))
1041        or unit.status.petrified
1042        or (not ai_helper.is_visible_unit(viewing_side, unit))
1043    then
1044        return false
1045    end
1046
1047    return true
1048end
1049
1050function ai_helper.get_closest_enemy(loc, side, cfg)
1051    -- Return distance to and location of the enemy closest to @loc, or to the
1052    -- leader of side with number @side if @loc is not specified
1053    --
1054    -- Optional parameters:
1055    -- @loc: location in format { x , y }
1056    -- @side: number of side for which to find enemy; defaults to current side
1057    -- @cfg: table with optional configuration parameters:
1058    --   viewing_side: see comments at beginning of this file. Defaults to @side.
1059
1060    side = side or wesnoth.current.side
1061
1062    local enemies = ai_helper.get_attackable_enemies({}, side, cfg)
1063
1064    local x, y
1065    if not loc then
1066        local leader = wesnoth.get_units { side = side, canrecruit = 'yes' }[1]
1067        x, y = leader.x, leader.y
1068    else
1069        x, y = loc[1], loc[2]
1070    end
1071
1072    local closest_distance, location = 9e99
1073    for _,enemy in ipairs(enemies) do
1074        enemy_distance = M.distance_between(x, y, enemy.x, enemy.y)
1075        if (enemy_distance < closest_distance) then
1076            closest_distance = enemy_distance
1077            location = { x = enemy.x, y = enemy.y}
1078        end
1079    end
1080
1081    return closest_distance, location
1082end
1083
1084function ai_helper.has_ability(unit, ability)
1085    -- Returns true/false depending on whether unit has the given ability
1086    local has_ability = false
1087    local abilities = wml.get_child(unit.__cfg, "abilities")
1088    if abilities then
1089        if wml.get_child(abilities, ability) then has_ability = true end
1090    end
1091    return has_ability
1092end
1093
1094function ai_helper.has_weapon_special(unit, special)
1095    -- Returns true/false depending on whether @unit has a weapon with special @special
1096    -- Also returns the number of the first weapon with this special
1097    for weapon_number,att in ipairs(unit.attacks) do
1098        for _,sp in ipairs(att.specials) do
1099            if (sp[1] == special) then
1100                return true, weapon_number
1101            end
1102        end
1103    end
1104    return false
1105end
1106
1107function ai_helper.get_cheapest_recruit_cost()
1108    local cheapest_unit_cost = 9e99
1109    for _,recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
1110        if wesnoth.unit_types[recruit_id].cost < cheapest_unit_cost then
1111            cheapest_unit_cost = wesnoth.unit_types[recruit_id].cost
1112        end
1113    end
1114
1115    return cheapest_unit_cost
1116end
1117
1118--------- Move related helper functions ----------
1119
1120ai_helper.no_path = 42424242  -- Value returned by C++ engine for distance when no path is found
1121
1122function ai_helper.get_dst_src_units(units, cfg)
1123    -- Get the dst_src location set for @units
1124    -- @cfg: table with optional configuration parameters:
1125    --   moves: if set to 'max' use max_moves of units, rather than current moves
1126    --   all parameters for wesnoth.find_reach
1127
1128    local max_moves = false
1129    if cfg then
1130        if (cfg['moves'] == 'max') then max_moves = true end
1131    end
1132
1133    local dstsrc = LS.create()
1134    for _,unit in ipairs(units) do
1135        local tmp = unit.moves
1136        if max_moves then
1137            unit.moves = unit.max_moves
1138        end
1139        local reach = wesnoth.find_reach(unit, cfg)
1140        if max_moves then
1141            unit.moves = tmp
1142        end
1143
1144        for _,loc in ipairs(reach) do
1145            local tmp = dstsrc:get(loc[1], loc[2]) or {}
1146            table.insert(tmp, { x = unit.x, y = unit.y })
1147            dstsrc:insert(loc[1], loc[2], tmp)
1148        end
1149    end
1150
1151    return dstsrc
1152end
1153
1154function ai_helper.get_dst_src(units, cfg)
1155    -- If @units table is given use it, otherwise use all units on the current side
1156    -- @cfg: table with optional configuration parameters:
1157    --   moves: if set to 'max' use max_moves of units, rather than current moves
1158    --   all parameters for wesnoth.find_reach
1159
1160    if (not units) then
1161        units = wesnoth.get_units { side = wesnoth.current.side }
1162    end
1163
1164    return ai_helper.get_dst_src_units(units, cfg)
1165end
1166
1167function ai_helper.get_enemy_dst_src(enemies, cfg)
1168    -- If @enemies table is given use it, otherwise use all enemy units
1169    -- @cfg: table with optional configuration parameters:
1170    --   all parameters for wesnoth.find_reach
1171
1172    if (not enemies) then
1173        enemies = wesnoth.get_units {
1174            { "filter_side", { { "enemy_of", { side = wesnoth.current.side} } } }
1175        }
1176    end
1177
1178    local cfg_copy = {}
1179    if cfg then cfg_copy = ai_helper.table_copy(cfg) end
1180    cfg_copy.moves = 'max'
1181
1182    return ai_helper.get_dst_src_units(enemies, cfg_copy)
1183end
1184
1185function ai_helper.my_moves()
1186    -- Produces an array with each field of form:
1187    --   [1] = { dst = { x = 7, y = 16 },
1188    --           src = { x = 6, y = 16 } }
1189
1190    local dstsrc = ai.get_dstsrc()
1191
1192    local my_moves = {}
1193    for key,value in pairs(dstsrc) do
1194        table.insert( my_moves,
1195            {   src = { x = value[1].x , y = value[1].y },
1196                dst = { x = key.x , y = key.y }
1197            }
1198        )
1199    end
1200
1201    return my_moves
1202end
1203
1204function ai_helper.enemy_moves()
1205    -- Produces an array with each field of form:
1206    --   [1] = { dst = { x = 7, y = 16 },
1207    --           src = { x = 6, y = 16 } }
1208
1209    local dstsrc = ai.get_enemy_dstsrc()
1210
1211    local enemy_moves = {}
1212    for key,value in pairs(dstsrc) do
1213        table.insert( enemy_moves,
1214            {   src = { x = value[1].x , y = value[1].y },
1215                dst = { x = key.x , y = key.y }
1216            }
1217        )
1218    end
1219
1220    return enemy_moves
1221end
1222
1223function ai_helper.next_hop(unit, x, y, cfg)
1224    -- Finds the next "hop" of @unit on its way to (@x,@y)
1225    -- Returns coordinates of the endpoint of the hop (or nil if no path to
1226    -- (x,y) is found for the unit), and movement cost to get there.
1227    -- Only unoccupied hexes are considered
1228    -- @cfg: standard extra options for wesnoth.find_path()
1229    --   including:
1230    --     viewing_side: see comments at beginning of this file. Defaults to side of @unit
1231    --   plus:
1232    --     ignore_own_units: if set to true, then own units that can move out of the way are ignored
1233
1234    local path, cost = ai_helper.find_path_with_shroud(unit, x, y, cfg)
1235
1236    if cost >= ai_helper.no_path then return nil, cost end
1237
1238    -- If none of the hexes are unoccupied, use current position as default
1239    local next_hop, nh_cost = { unit.x, unit.y }, 0
1240
1241    -- Go through loop to find reachable, unoccupied hex along the path
1242    -- Start at second index, as first is just the unit position itself
1243    for i = 2,#path do
1244        local sub_path, sub_cost = ai_helper.find_path_with_shroud(unit, path[i][1], path[i][2], cfg)
1245
1246        if sub_cost <= unit.moves then
1247            -- Check for unit in way only if cfg.ignore_units is not set
1248            local unit_in_way
1249            if (not cfg) or (not cfg.ignore_units) then
1250                unit_in_way = wesnoth.get_unit(path[i][1], path[i][2])
1251
1252                -- If ignore_own_units is set, ignore own side units that can move out of the way
1253                if cfg and cfg.ignore_own_units then
1254                    if unit_in_way and (unit_in_way.side == unit.side) then
1255                        local reach = ai_helper.get_reachable_unocc(unit_in_way, cfg)
1256                        if (reach:size() > 1) then unit_in_way = nil end
1257                    end
1258                end
1259            end
1260
1261            if not unit_in_way then
1262                next_hop, nh_cost = path[i], sub_cost
1263            end
1264        else
1265            break
1266        end
1267    end
1268
1269    return next_hop, nh_cost
1270end
1271
1272function ai_helper.can_reach(unit, x, y, cfg)
1273    -- Returns true if hex (@x, @y) is unoccupied (by a visible unit), or
1274    -- at most occupied by unit on same side as @unit that can move away
1275    -- (can be modified with options below)
1276    --
1277    -- @cfg: table with optional configuration parameters:
1278    --   moves = 'max' use max_moves instead of current moves
1279    --   ignore_units: if true, ignore both own and enemy units
1280    --   exclude_occupied: if true, exclude hex if there's a unit there, irrespective of value of 'ignore_units'
1281    --   viewing_side: see comments at beginning of this file. Defaults to side of @unit
1282
1283    cfg = cfg or {}
1284    local viewing_side = cfg.viewing_side or unit.side
1285
1286    -- Is there a unit at the goal hex?
1287    local unit_in_way = wesnoth.get_unit(x, y)
1288    if (cfg.exclude_occupied)
1289      and unit_in_way and ai_helper.is_visible_unit(viewing_side, unit_in_way)
1290    then
1291        return false
1292    end
1293
1294    -- Otherwise, if 'ignore_units' is not set, return false if there's a unit of other side,
1295    -- or a unit of own side that cannot move away (this might be slow, don't know)
1296    if (not cfg.ignore_units) then
1297        -- If there's a unit at the goal that's not on own side (even ally), return false
1298        if unit_in_way
1299            and (unit_in_way.side ~= unit.side)
1300            and ai_helper.is_visible_unit(viewing_side, unit_in_way)
1301        then
1302            return false
1303        end
1304
1305        -- If the unit in the way is on 'unit's' side and cannot move away, also return false
1306        if unit_in_way and (unit_in_way.side == unit.side) then
1307            -- need to pass the cfg here so that it works for enemy units (generally with no moves left) also
1308            local move_away = ai_helper.get_reachable_unocc(unit_in_way, cfg)
1309            if (move_away:size() <= 1) then return false end
1310        end
1311    end
1312
1313    -- After all that, test whether our unit can actually get there
1314    local old_moves = unit.moves
1315    if (cfg.moves == 'max') then unit.moves = unit.max_moves end
1316
1317    local can_reach = false
1318    local path, cost = ai_helper.find_path_with_shroud(unit, x, y, cfg)
1319    if (cost <= unit.moves) then can_reach = true end
1320
1321    unit.moves = old_moves
1322
1323    return can_reach
1324end
1325
1326function ai_helper.get_reachable_unocc(unit, cfg)
1327    -- Get all reachable hexes for @unit that are (or appear) unoccupied, taking
1328    -- vision into account as specified by the @cfg.viewing_side parameter.
1329    -- Returned array is a location set, with value = 1 for each reachable hex
1330    --
1331    -- @cfg: table with optional configuration parameters:
1332    --   moves: if set to 'max', unit MP is set to max_moves before calculation
1333    --   viewing_side: see comments at beginning of this file. Defaults to side of @unit
1334    --   plus all other parameters to wesnoth.find_reach
1335
1336    local viewing_side = cfg and cfg.viewing_side or unit.side
1337
1338    local old_moves = unit.moves
1339    if cfg then
1340        if (cfg.moves == 'max') then unit.moves = unit.max_moves end
1341    end
1342
1343    local reach = LS.create()
1344    local initial_reach = wesnoth.find_reach(unit, cfg)
1345    for _,loc in ipairs(initial_reach) do
1346        local unit_in_way = wesnoth.get_unit(loc[1], loc[2])
1347        if (not unit_in_way) or (not ai_helper.is_visible_unit(viewing_side, unit_in_way)) then
1348            reach:insert(loc[1], loc[2], 1)
1349        end
1350    end
1351
1352    -- Also need to include the hex the unit is on itself
1353    reach:insert(unit.x, unit.y, 1)
1354
1355    unit.moves = old_moves
1356
1357    return reach
1358end
1359
1360function ai_helper.find_path_with_shroud(unit, x, y, cfg)
1361    -- Same as wesnoth.find_path, just that it works under shroud as well while still
1362    -- ignoring invisible units. It does this by using viewing_side=0 and taking
1363    -- invisible units off the map for the path finding process.
1364    --
1365    -- Notes on some of the optional parameters that can be passed in @cfg:
1366    --  - viewing_side: If not given, use side of the unit (not the current side!)
1367    --    for determining which units are hidden and need to be extracted, as that
1368    --    is what the path_finder code uses. If set to an invalid side, we can use
1369    --    default path finding as shroud is ignored then anyway.
1370    --  - ignore_units: if true, hidden units do not need to be extracted because
1371    --    all units are ignored anyway
1372
1373    local viewing_side = (cfg and cfg.viewing_side) or unit.side
1374
1375    local path, cost
1376    if wesnoth.sides[viewing_side] and wesnoth.sides[viewing_side].shroud then
1377        local extracted_units = {}
1378        if (not cfg) or (not cfg.ignore_units) then
1379            local all_units = wesnoth.get_units()
1380            for _,u in ipairs(all_units) do
1381                if (u.side ~= viewing_side)
1382                    and (not ai_helper.is_visible_unit(viewing_side, u))
1383                then
1384                    wesnoth.extract_unit(u)
1385                    table.insert(extracted_units, u)
1386                end
1387            end
1388        end
1389
1390        local cfg_copy = {}
1391        if cfg then cfg_copy = ai_helper.table_copy(cfg) end
1392        cfg_copy.viewing_side = 0
1393        path, cost = wesnoth.find_path(unit, x, y, cfg_copy)
1394
1395        for _,extracted_unit in ipairs(extracted_units) do
1396            wesnoth.put_unit(extracted_unit)
1397        end
1398    else
1399        path, cost = wesnoth.find_path(unit, x, y, cfg)
1400    end
1401
1402    return path, cost
1403end
1404
1405function ai_helper.find_best_move(units, rating_function, cfg)
1406    -- Find the best move and best unit based on @rating_function
1407    -- INPUTS:
1408    --  @units: (required) single unit or table of units
1409    --  @rating_function: (required) function(x, y) with rating function for the hexes the unit can reach
1410    --  @cfg: table with optional configuration parameters:
1411    --    labels: if set, put labels with ratings onto map
1412    --    no_random: if set, do not add random value between 0.0001 and 0.0099 to each hex
1413    --    plus all the possible parameters for ai_helper.get_reachable_unocc()
1414    --
1415    -- OUTPUTS:
1416    --  best_hex: format { x, y }
1417    --  best_unit: unit for which this rating function produced the maximum value
1418    --  max_rating: the rating found for this hex/unit combination
1419    -- If no valid moves were found, best_unit and best_hex are empty arrays
1420
1421    -- TODO: change return value to nil if no unit/hex is found later in 1.13., but keep as is in 1.12
1422
1423    cfg = cfg or {}
1424
1425    -- If this is an individual unit, turn it into an array
1426    if units.hitpoints then units = { units } end
1427
1428    local max_rating, best_hex, best_unit = -9e99, {}, {}
1429    for _,unit in ipairs(units) do
1430        -- Hexes each unit can reach
1431        local reach_map = ai_helper.get_reachable_unocc(unit, cfg)
1432        reach_map:iter( function(x, y, v)
1433            -- Rate based on rating_function argument
1434            local rating = rating_function(x, y)
1435
1436            -- If cfg.random is set, add some randomness (on 0.0001 - 0.0099 level)
1437            if (not cfg.no_random) then rating = rating + math.random(99) / 10000. end
1438            -- If cfg.labels is set: insert values for label map
1439            if cfg.labels then reach_map:insert(x, y, rating) end
1440
1441            if rating > max_rating then
1442                max_rating, best_hex, best_unit = rating, { x, y }, unit
1443            end
1444        end)
1445        if cfg.labels then ai_helper.put_labels(reach_map) end
1446    end
1447
1448    return best_hex, best_unit, max_rating
1449end
1450
1451function ai_helper.move_unit_out_of_way(ai, unit, cfg)
1452    -- Move @unit to the best close location.
1453    -- Main rating is the moves the unit still has left after that
1454    -- Other, configurable, parameters are given to function in @cfg:
1455    --   dx, dy: the direction in which moving out of the way is preferred
1456    --   labels: if set, display labels of the rating for each hex the unit can reach
1457    --   viewing_side: see comments at beginning of this file. Defaults to side of @unit.
1458    --   all other optional parameters to wesnoth.find_reach()
1459
1460    cfg = cfg or {}
1461    local viewing_side = cfg.viewing_side or unit.side
1462
1463    local dx, dy
1464    if cfg.dx and cfg.dy then
1465        local r = math.sqrt(cfg.dx * cfg.dx + cfg.dy * cfg.dy)
1466        if (r ~= 0) then dx, dy = cfg.dx / r, cfg.dy / r end
1467    end
1468
1469    local reach = wesnoth.find_reach(unit, cfg)
1470    local reach_map = LS.create()
1471
1472    local max_rating, best_hex = -9e99
1473    for _,loc in ipairs(reach) do
1474        local unit_in_way = wesnoth.get_unit(loc[1], loc[2])
1475        if (not unit_in_way)       -- also excludes current hex
1476            or (not ai_helper.is_visible_unit(viewing_side, unit_in_way))
1477        then
1478            local rating = loc[3]  -- also disfavors hexes next to visible enemy units for which loc[3] = 0
1479
1480            if dx then
1481                rating = rating + (loc[1] - unit.x) * dx * 0.01
1482                rating = rating + (loc[2] - unit.y) * dy * 0.01
1483            end
1484
1485            if cfg.labels then reach_map:insert(loc[1], loc[2], rating) end
1486
1487            if (rating > max_rating) then
1488                max_rating, best_hex = rating, { loc[1], loc[2] }
1489            end
1490        end
1491    end
1492    if cfg.labels then ai_helper.put_labels(reach_map) end
1493
1494    if best_hex then
1495        ai_helper.checked_move(ai, unit, best_hex[1], best_hex[2])
1496    end
1497end
1498
1499function ai_helper.movefull_stopunit(ai, unit, x, y)
1500    -- Does ai.move_full for @unit if not at (@x,@y), otherwise ai.stopunit_moves
1501    -- Uses ai_helper.next_hop(), so that it works if unit cannot get there in one move
1502    -- Coordinates can be given as x and y components, or as a 2-element table { x, y } or { x = x, y = y }
1503    if (type(x) ~= 'number') then
1504        if x[1] then
1505            x, y = x[1], x[2]
1506        else
1507            x, y = x.x, x.y
1508        end
1509    end
1510
1511    local next_hop = ai_helper.next_hop(unit, x, y)
1512    if next_hop and ((next_hop[1] ~= unit.x) or (next_hop[2] ~= unit.y)) then
1513        return ai_helper.checked_move_full(ai, unit, next_hop[1], next_hop[2])
1514    else
1515        return ai_helper.checked_stopunit_moves(ai, unit)
1516    end
1517end
1518
1519function ai_helper.movefull_outofway_stopunit(ai, unit, x, y, cfg)
1520    -- Same as ai_help.movefull_stopunit(), but also moves a unit out of the way if there is one
1521    --
1522    -- @cfg: table with optional configuration parameters:
1523    --   viewing_side: see comments at beginning of this file. Defaults to side of @unit
1524    --   all other optional parameters to ai_helper.move_unit_out_of_way() and wesnoth.find_path()
1525
1526    local viewing_side = cfg and cfg.viewing_side or unit.side
1527
1528    if (type(x) ~= 'number') then
1529        if x[1] then
1530            x, y = x[1], x[2]
1531        else
1532            x, y = x.x, x.y
1533        end
1534    end
1535
1536    -- Only move unit out of way if the main unit can get there
1537    local path, cost = ai_helper.find_path_with_shroud(unit, x, y, cfg)
1538    if (cost <= unit.moves) then
1539        local unit_in_way = wesnoth.get_unit(x, y)
1540        if unit_in_way and (unit_in_way ~= unit)
1541            and ai_helper.is_visible_unit(viewing_side, unit_in_way)
1542        then
1543            --W.message { speaker = 'narrator', message = 'Moving out of way' }
1544            ai_helper.move_unit_out_of_way(ai, unit_in_way, cfg)
1545        end
1546    end
1547
1548    local next_hop = ai_helper.next_hop(unit, x, y)
1549    if next_hop and ((next_hop[1] ~= unit.x) or (next_hop[2] ~= unit.y)) then
1550        ai_helper.checked_move_full(ai, unit, next_hop[1], next_hop[2])
1551    else
1552        ai_helper.checked_stopunit_moves(ai, unit)
1553    end
1554end
1555
1556---------- Attack related helper functions --------------
1557
1558function ai_helper.get_attacks(units, cfg)
1559    -- Get all attacks the units stored in @units can do. Invisible enemies are
1560    -- excluded unless option @cfg.viewing_side=0 is used.
1561    --
1562    -- This includes a variety of configurable options, passed in the @cfg table
1563    -- @cfg: table with optional configuration parameters:
1564    --   moves: "current" (default for units on current side) or "max" (always used for units on other sides)
1565    --   include_occupied (false): if set, also include hexes occupied by own-side units that can move away
1566    --   simulate_combat (false): if set, also simulate the combat and return result (this is slow; only set if needed)
1567    --   viewing_side: see comments at beginning of this file. Defaults to side of @units
1568    --   all other optional parameters to wesnoth.find_reach()
1569    --
1570    -- Returns {} if no attacks can be done, otherwise table with fields:
1571    --   dst: { x = x, y = y } of attack position
1572    --   src: { x = x, y = y } of attacking unit (don't use id, could be ambiguous)
1573    --   target: { x = x, y = y } of defending unit
1574    --   att_stats, def_stats: as returned by wesnoth.simulate_combat (if cfg.simulate_combat is set)
1575    --   attack_hex_occupied: boolean storing whether an own unit that can move away is on the attack hex
1576
1577    cfg = cfg or {}
1578
1579    local attacks = {}
1580    if (not units[1]) then return attacks end
1581
1582    local side = units[1].side  -- all units need to be on same side
1583    local viewing_side = cfg and cfg.viewing_side or side
1584
1585    -- 'moves' can be either "current" or "max"
1586    -- For unit on current side: use "current" by default, or override by cfg.moves
1587    local moves = cfg.moves or "current"
1588    -- For unit on any other side, only moves="max" makes sense
1589    if (side ~= wesnoth.current.side) then moves = "max" end
1590
1591    local old_moves = {}
1592    if (moves == "max") then
1593        for i,unit in ipairs(units) do
1594            old_moves[i] = unit.moves
1595            unit.moves = unit.max_moves
1596        end
1597    end
1598
1599    -- Note: the remainder is optimized for speed, so we only get_units once,
1600    -- do not use WML filters, etc.
1601    local all_units = wesnoth.get_units()
1602
1603    local enemy_map, my_unit_map, other_unit_map = LS.create(), LS.create(), LS.create()
1604    for i,unit in ipairs(all_units) do
1605        -- The value of all the location sets is the index of the
1606        -- unit in the all_units array
1607        if ai_helper.is_attackable_enemy(unit, side, cfg) then
1608            enemy_map:insert(unit.x, unit.y, i)
1609        end
1610
1611        if (unit.side == side) then
1612            my_unit_map:insert(unit.x, unit.y, i)
1613        else
1614            if ai_helper.is_visible_unit(viewing_side, unit) then
1615                other_unit_map:insert(unit.x, unit.y, i)
1616            end
1617        end
1618    end
1619
1620    local attack_hex_map = LS.create()
1621    enemy_map:iter(function(e_x, e_y, i)
1622        for xa,ya in H.adjacent_tiles(e_x, e_y) do
1623            -- If there's no unit of another side on this hex, include it
1624            -- as possible attack location (this includes hexes occupied
1625            -- by own units at this time)
1626            if (not other_unit_map:get(xa, ya)) then
1627                local target_table = attack_hex_map:get(xa, ya) or {}
1628                table.insert(target_table, { x = e_x, y = e_y, i = i })
1629                attack_hex_map:insert(xa, ya, target_table)
1630            end
1631        end
1632    end)
1633
1634    -- The following is done so that we at most need to do find_reach() once per unit
1635    -- It is needed for all units in @units and for testing whether units can move out of the way
1636    local reaches = LS.create()
1637
1638    for _,unit in ipairs(units) do
1639        local reach
1640        if reaches:get(unit.x, unit.y) then
1641            reach = reaches:get(unit.x, unit.y)
1642        else
1643            reach = wesnoth.find_reach(unit, cfg)
1644            reaches:insert(unit.x, unit.y, reach)
1645        end
1646
1647        for _,loc in ipairs(reach) do
1648            if attack_hex_map:get(loc[1], loc[2]) then
1649                local add_target = true
1650                local attack_hex_occupied = false
1651
1652                -- If another unit of same side is on this hex:
1653                if my_unit_map:get(loc[1], loc[2]) and ((loc[1] ~= unit.x) or (loc[2] ~= unit.y)) then
1654                    attack_hex_occupied = true
1655                    add_target = false
1656
1657                    if cfg.include_occupied then -- Test whether it can move out of the way
1658                        local unit_in_way = all_units[my_unit_map:get(loc[1], loc[2])]
1659                        local uiw_reach
1660                        if reaches:get(unit_in_way.x, unit_in_way.y) then
1661                            uiw_reach = reaches:get(unit_in_way.x, unit_in_way.y)
1662                        else
1663                            uiw_reach = wesnoth.find_reach(unit_in_way, cfg)
1664                            reaches:insert(unit_in_way.x, unit_in_way.y, uiw_reach)
1665                        end
1666
1667                        -- Check whether the unit to move out of the way has an unoccupied hex to move to.
1668                        -- We do not deal with cases where a unit can move out of the way for a
1669                        -- unit that is moving out of the way of the initial unit (etc.).
1670                        for _,uiw_loc in ipairs(uiw_reach) do
1671                            -- Unit in the way of the unit in the way
1672                            local uiw_uiw = wesnoth.get_unit(uiw_loc[1], uiw_loc[2])
1673                            if (not uiw_uiw) or (not ai_helper.is_visible_unit(viewing_side, uiw_uiw)) then
1674                                add_target = true
1675                                break
1676                            end
1677                        end
1678                    end
1679                end
1680
1681                if add_target then
1682                    for _,target in ipairs(attack_hex_map:get(loc[1], loc[2])) do
1683                        local att_stats, def_stats
1684                        if cfg.simulate_combat then
1685                            local unit_dst = wesnoth.copy_unit(unit)
1686                            unit_dst.x, unit_dst.y = loc[1], loc[2]
1687
1688                            local enemy = all_units[target.i]
1689                            att_stats, def_stats = wesnoth.simulate_combat(unit_dst, enemy)
1690                        end
1691
1692                        table.insert(attacks, {
1693                            src = { x = unit.x, y = unit.y },
1694                            dst = { x = loc[1], y = loc[2] },
1695                            target = { x = target.x, y = target.y },
1696                            att_stats = att_stats,
1697                            def_stats = def_stats,
1698                            attack_hex_occupied = attack_hex_occupied
1699                        })
1700                    end
1701                end
1702            end
1703        end
1704    end
1705
1706    if (moves == "max") then
1707        for i,unit in ipairs(units) do
1708            unit.moves = old_moves[i]
1709        end
1710    end
1711
1712    return attacks
1713end
1714
1715function ai_helper.add_next_attack_combo_level(combos, attacks)
1716    -- This is called from ai_helper.get_attack_combos_full() and
1717    -- builds up the combos for the next recursion level.
1718    -- It also calls the next recursion level, if possible
1719    -- Important: function needs to make a copy of the input array, otherwise original is changed
1720
1721    -- Set up the array, if this is the first recursion level
1722    if (not combos) then combos = {} end
1723
1724    -- Array to hold combinations for this recursion level only
1725    local combos_this_level = {}
1726
1727    for _,attack in ipairs(attacks) do
1728        local dst = attack.dst.y + attack.dst.x * 1000.  -- attack hex (src)
1729        local src = attack.src.y + attack.src.x * 1000.  -- attacker hex (dst)
1730        if (not combos[1]) then  -- if this is the first recursion level, set up new combos for this level
1731            local move = {}
1732            move[dst] = src
1733            table.insert(combos_this_level, move)
1734        else
1735            -- Otherwise, we need to go through the already existing elements in 'combos'
1736            -- to see if either hex, or attacker is already used; and then add new attack to each
1737            for _,combo in ipairs(combos) do
1738                local this_combo = {}  -- needed because tables are pointers, need to create a separate one
1739                local add_combo = true
1740                for d,s in pairs(combo) do
1741                    if (d == dst) or (s == src) then
1742                        add_combo = false
1743                        break
1744                    end
1745                    this_combo[d] = s  -- insert individual moves to a combo
1746                end
1747                if add_combo then  -- and add it into the array, if it contains only unique moves
1748                    this_combo[dst] = src
1749                    table.insert(combos_this_level, this_combo)
1750                end
1751            end
1752        end
1753    end
1754
1755    local combos_next_level = {}
1756    if combos_this_level[1] then  -- If moves were found for this level, also find those for the next level
1757        combos_next_level = ai_helper.add_next_attack_combo_level(combos_this_level, attacks)
1758    end
1759
1760    -- Finally, combine this level and next level combos
1761    combos_this_level = ai_helper.array_merge(combos_this_level, combos_next_level)
1762    return combos_this_level
1763end
1764
1765function ai_helper.get_attack_combos_full(units, enemy, cfg)
1766    -- Calculate attack combination result by @units on @enemy
1767    -- All combinations of all units are taken into account, as well as their order
1768    -- This can result in a _very_ large number of possible combinations
1769    -- Use ai_helper.get_attack_combos() instead if order does not matter
1770    --
1771    -- Optional inputs:
1772    --   @cfg: Configuration table to be passed on to ai_helper.get_attacks()
1773    --
1774    -- Return value:
1775    --   1. Attack combinations in form { dst = src }
1776
1777    local all_attacks = ai_helper.get_attacks(units, cfg)
1778
1779    -- Eliminate those that are not on @enemy
1780    local attacks = {}
1781    for _,attack in ipairs(all_attacks) do
1782        if (attack.target.x == enemy.x) and (attack.target.y == enemy.y) then
1783            table.insert(attacks, attack)
1784        end
1785    end
1786    if (not attacks[1]) then return {} end
1787
1788    -- This recursive function does all the work:
1789    local combos = ai_helper.add_next_attack_combo_level(combos, attacks)
1790
1791    return combos
1792end
1793
1794function ai_helper.get_attack_combos(units, enemy, cfg)
1795    -- Calculate attack combination result by @units on @enemy
1796    -- All the unit/hex combinations are considered, but without specifying the order of the
1797    -- attacks. Use ai_helper.get_attack_combos_full() if order matters.
1798    --
1799    -- Optional inputs:
1800    -- @cfg: Configuration table to be passed on to wesnoth.find_path()
1801    --
1802    -- Return values:
1803    --   1. Attack combinations in form { dst = src }
1804    --   2. All the attacks indexed by [dst][src]
1805
1806    -- We don't need the full attacks here, just the coordinates,
1807    -- so for speed reasons, we do not use ai_helper.get_attacks()
1808
1809    -- For units on the current side, we need to make sure that
1810    -- there isn't a unit in the way that cannot move any more
1811    -- TODO: generalize it so that it works not only for units with moves=0, but blocked units etc.
1812    local blocked_hexes = LS.create()
1813    if units[1] and (units[1].side == wesnoth.current.side) then
1814        local all_units = wesnoth.get_units { side = wesnoth.current.side }
1815        for _,unit in ipairs(all_units) do
1816            if (unit.moves == 0) then
1817                blocked_hexes:insert(unit.x, unit.y)
1818            end
1819        end
1820    end
1821
1822    -- For sides other than the current, we always use max_moves,
1823    -- for the current side we always use current moves
1824    local old_moves = {}
1825    for i,unit in ipairs(units) do
1826        if (unit.side ~= wesnoth.current.side) then
1827            old_moves[i] = unit.moves
1828            unit.moves = unit.max_moves
1829        end
1830    end
1831
1832    -- Find which units in @units can get to hexes next to the enemy
1833    local attacks_dst_src = {}
1834    local found_attacks = false
1835    for xa,ya in H.adjacent_tiles(enemy.x, enemy.y) do
1836        -- Make sure the hex is not occupied by unit that cannot move out of the way
1837
1838        local dst = xa * 1000 + ya
1839
1840        for _,unit in ipairs(units) do
1841            if ((unit.x == xa) and (unit.y == ya)) or (not blocked_hexes:get(xa, ya)) then
1842
1843                -- wesnoth.map.distance_between() is much faster than wesnoth.find_path()
1844                --> pre-filter using the former
1845                local cost = M.distance_between(unit.x, unit.y, xa, ya)
1846
1847                -- If the distance is <= the unit's MP, then see if it can actually get there
1848                -- This also means that only short paths have to be evaluated (in most situations)
1849                if (cost <= unit.moves) then
1850                    local path  -- since cost is already defined outside this block
1851                    path, cost = ai_helper.find_path_with_shroud(unit, xa, ya, cfg)
1852                end
1853
1854                if (cost <= unit.moves) then
1855                    -- for attack by no unit on this hex
1856                    if (not attacks_dst_src[dst]) then
1857                        attacks_dst_src[dst] = { 0, unit.x * 1000 + unit.y }
1858                        found_attacks = true  -- since attacks_dst_src is not a simple array, this is easier
1859                    else
1860                        table.insert(attacks_dst_src[dst], unit.x * 1000 + unit.y )
1861                    end
1862                end
1863            end
1864        end
1865    end
1866
1867    for i,unit in ipairs(units) do
1868        if (unit.side ~= wesnoth.current.side) then
1869            unit.moves = old_moves[i]
1870        end
1871    end
1872
1873    if (not found_attacks) then return {}, {} end
1874
1875    -- Now we set up an array of all attack combinations
1876    -- at this time, this includes all the 'no unit attacks this hex' elements
1877    -- which have a value of 0 for 'src'
1878    -- They need to be kept in this part, so that we get the combos that do not
1879    -- use the maximum amount of units possible. They will be eliminated below.
1880    local attack_array = {}
1881    -- For all values of 'dst'
1882    for dst,ads in pairs(attacks_dst_src) do
1883        local org_array = ai_helper.table_copy(attack_array)
1884        attack_array = {}
1885
1886        -- Go through all the values of 'src'
1887        for _,src in ipairs(ads) do
1888            -- If the array does not exist, set it up
1889            if (not org_array[1]) then
1890                local tmp = {}
1891                tmp[dst] = src
1892                table.insert(attack_array, tmp)
1893            else  -- otherwise, add the new dst-src pair to each element of the existing array
1894                for _,org in ipairs(org_array) do
1895                    -- but only do so if that 'src' value does not exist already
1896                    -- except for 0's those all need to be kept
1897                    local add_attack = true
1898                    for _,s in pairs(org) do
1899                        if (s == src) and (src ~=0) then
1900                            add_attack = false
1901                            break
1902                        end
1903                    end
1904                    -- Finally, add it to the array
1905                    if add_attack then
1906                        local tmp = ai_helper.table_copy(org)
1907                        tmp[dst] = src
1908                        table.insert(attack_array, tmp)
1909                    end
1910                end
1911            end
1912        end
1913    end
1914
1915    -- Now eliminate all the 0s
1916    -- Also eliminate the combo that has no attacks on any hex (all zeros)
1917    local i_empty = 0
1918    for i,att in ipairs(attack_array) do
1919        local count = 0
1920        for dst,src in pairs(att) do
1921            if (src == 0) then
1922                att[dst] = nil
1923            else
1924                count = count + 1
1925            end
1926        end
1927        if (count == 0) then i_empty = i end
1928    end
1929
1930    -- This last step eliminates the "empty attack combo" (the one with all zeros)
1931    -- Since this is only one, it's okay to use table.remove (even though it's slow)
1932    table.remove(attack_array, i_empty)
1933
1934    return attack_array
1935end
1936
1937function ai_helper.get_unit_time_of_day_bonus(alignment, lawful_bonus)
1938    local multiplier = 1
1939    if (lawful_bonus ~= 0) then
1940        if (alignment == 'lawful') then
1941            multiplier = (1 + lawful_bonus / 100.)
1942        elseif (alignment == 'chaotic') then
1943            multiplier = (1 - lawful_bonus / 100.)
1944        elseif (alignment == 'liminal') then
1945            multipler = (1 - math.abs(lawful_bonus) / 100.)
1946        end
1947    end
1948    return multiplier
1949end
1950
1951return ai_helper
1952