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