├── .gitignore ├── AI-demos ├── CHANGELOG ├── _main.cfg ├── ais │ ├── ai_fred.cfg │ └── ai_manual.cfg ├── lua │ ├── ai_helper_local.lua │ ├── ca_fred.lua │ ├── ca_manual_move.lua │ ├── ca_manual_stats.lua │ ├── ca_move_unittype.lua │ ├── ca_stats.lua │ ├── compatibility.lua │ ├── debug.lua │ ├── eval_exec_CA.lua │ ├── fred_advance.lua │ ├── fred_attack.lua │ ├── fred_attack_utils.lua │ ├── fred_benefits_utilities.lua │ ├── fred_config.lua │ ├── fred_data_incremental.lua │ ├── fred_data_move.lua │ ├── fred_data_turn.lua │ ├── fred_gamestate_map.lua │ ├── fred_hold.lua │ ├── fred_map_config.lua │ ├── fred_map_utils.lua │ ├── fred_move_leader_utils.lua │ ├── fred_ops_analysis.lua │ ├── fred_retreat.lua │ ├── fred_scenario_events.lua │ ├── fred_scenario_setup.lua │ ├── fred_status.lua │ ├── fred_utils.lua │ ├── fred_village_utils.lua │ ├── fred_virtual_state.lua │ ├── generic_recruit_engine.lua │ ├── manual_input.lua │ └── test_lua_template.lua ├── maps │ ├── Dark_Forecast.map │ └── fai_demo.map ├── multiplayer │ ├── _initial.cfg │ ├── era.cfg │ └── modification.cfg ├── scenarios │ ├── Fred.cfg │ ├── fai_demo.cfg │ ├── luaai_demo.cfg │ ├── manual_ai.cfg │ ├── mp_ais.cfg │ ├── switchboard.cfg │ └── test.cfg ├── units │ └── Invisible_Unit.cfg ├── version.lua └── version.txt ├── COPYING.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test_lua.lua 3 | _server.pbl 4 | .idea 5 | *.pyc 6 | *~ 7 | ml_utils/stats__release_*.log 8 | */.DS_Store 9 | -------------------------------------------------------------------------------- /AI-demos/_main.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | [textdomain] 3 | name="wesnoth-AI-demos" 4 | path="data/add-ons/AI-demos/translations" 5 | [/textdomain] 6 | 7 | # wmllint: general spellings RCA AIs Lua self.data Multiplayer_AI commandline dbms config 8 | # wmllint: general spellings Ron Fred Jabb Urudin Quank Freelands Google 9 | 10 | [campaign] 11 | id=AI-demos 12 | name=_"AI Modification Demos" 13 | abbrev=_"AI-demos" 14 | 15 | define=CAMPAIGN_AIDEMOS 16 | first_scenario=aid_switchboard 17 | 18 | icon=units/goblins/spearman.png~RC(magenta>green)~BLIT(halo/misc/leadership-flare-6.png~CROP(21,21,30,30)~CS(0,-50,0),30,8) 19 | image=portraits/goblins/spearman.png~CROP(1,65,500,335) 20 | rank=9999 21 | description=_"AI Development and Demonstration Campaign 22 | 23 | Home of a number of AI development projects, most notably two MP AIs dubbed 'Ron' and 'Fred' as well as several test and demo scenarios. This is also where the Micro AIs were developed. As the latter have now been moved into mainline, they are no longer part of this add-on. 24 | 25 | Version {./version.txt}" # wmllint: no spellcheck 26 | 27 | [about] 28 | title=_"Initial Campaign Design" 29 | [entry] 30 | name=_"mattsc" 31 | [/entry] 32 | [/about] 33 | [about] 34 | title=_"Coding" 35 | [entry] 36 | name=_"Alarantalara, AI0867, mattsc" 37 | [/entry] 38 | [/about] 39 | [about] 40 | title=_"2012 Google Code-in Students' Micro AI development" 41 | [entry] 42 | name=_"Samuel Kim, Robert Spencer, Martin Bede, gh0st, vitiv" 43 | [/entry] 44 | [/about] 45 | [about] 46 | title=_"Code and strategy contributions" 47 | [entry] 48 | name=_"nelson, Rigor and various others (see forum thread)" 49 | [/entry] 50 | [/about] 51 | [about] 52 | title=_"Home guard code" 53 | [entry] 54 | name=_"JaMiT" 55 | [/entry] 56 | [/about] 57 | [about] 58 | title=_"Stationed guardian and coward ideas" 59 | [entry] 60 | name=_"Simons Mith" 61 | [/entry] 62 | [/about] 63 | [about] 64 | title=_"Patrol scenario map, Konrad move idea (from 'The Earth Gut'), and dbms debug function" 65 | [entry] 66 | name=_"Anonymissimus" 67 | [/entry] 68 | [/about] 69 | [about] 70 | title=_"Goblin Jabb patrol code (from 'A Rough Life')" 71 | [entry] 72 | name=_"Elvish_Hunter" 73 | [/entry] 74 | [/about] 75 | [about] 76 | title=_"Urudin retreat code (from 'Legend of Wesmere')" 77 | [entry] 78 | name=_"nephro and Elvish_Hunter" 79 | [/entry] 80 | [/about] 81 | [about] 82 | title=_"Animal unit graphics and config files" 83 | [entry] 84 | name=_"Deer & Stag: Anna D. (dirtywhitellama); Rabbit: cedric; Other Animals: Different sources -- TBD: need to list those specifically!!!" 85 | [/entry] 86 | [/about] 87 | #[about] 88 | # title=_"Formula AI scenario and code" 89 | # [entry] 90 | # name=_"Wehrli Johan and Rinaldini Julien" 91 | # [/entry] 92 | #[/about] 93 | [/campaign] 94 | 95 | # This is needed for the CA debugging mechanism to work 96 | [lua] 97 | code = << 98 | dummy_self = { data = {} } 99 | >> 100 | [/lua] 101 | 102 | #ifdef CAMPAIGN_AIDEMOS 103 | [binary_path] 104 | path=data/add-ons/AI-demos 105 | [/binary_path] 106 | 107 | [+units] 108 | {./units} 109 | [/units] 110 | {./scenarios} 111 | #endif 112 | 113 | #ifdef MULTIPLAYER 114 | [binary_path] 115 | path="data/add-ons/AI-demos" 116 | [/binary_path] 117 | {./multiplayer} 118 | #endif 119 | 120 | # Also enable use of test scenarios in scenarios/ 121 | #ifdef TEST 122 | [binary_path] 123 | path=data/add-ons/AI-demos 124 | [/binary_path] 125 | 126 | [+units] 127 | {./units} 128 | [/units] 129 | {./scenarios} 130 | #endif 131 | -------------------------------------------------------------------------------- /AI-demos/ais/ai_fred.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | 3 | [ai] 4 | id=fred 5 | description=_"Multiplayer_AI^AI-demos: Fred — Freelands Custom AI" 6 | mp_rank=2000 7 | [stage] 8 | id=main_loop 9 | name=ai_default_rca::candidate_action_evaluation_loop 10 | 11 | [candidate_action] 12 | engine=lua 13 | name=stats 14 | max_score=999990 15 | location="~add-ons/AI-demos/lua/ca_stats.lua" 16 | [/candidate_action] 17 | [candidate_action] 18 | engine=lua 19 | name=fred 20 | max_score=350000 21 | location="~add-ons/AI-demos/lua/ca_fred.lua" 22 | [/candidate_action] 23 | [/stage] 24 | [/ai] 25 | -------------------------------------------------------------------------------- /AI-demos/ais/ai_manual.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | 3 | #ifndef AI_CA_GOTO 4 | {core/macros/ai_candidate_actions.cfg} 5 | #endif 6 | 7 | [ai] 8 | id=manual_ai 9 | description=_"Multiplayer_AI^AI-demos: Manual AI" 10 | mp_rank=2 11 | [stage] 12 | id=main_loop 13 | name=ai_default_rca::candidate_action_evaluation_loop 14 | 15 | [candidate_action] 16 | engine=lua 17 | name=stats 18 | max_score=999990 19 | location="~add-ons/AI-demos/lua/ca_manual_stats.lua" 20 | [/candidate_action] 21 | [candidate_action] 22 | engine=lua 23 | name=manual_move 24 | max_score=999980 25 | location="~add-ons/AI-demos/lua/ca_manual_move.lua" 26 | [/candidate_action] 27 | [/stage] 28 | [/ai] 29 | -------------------------------------------------------------------------------- /AI-demos/lua/ai_helper_local.lua: -------------------------------------------------------------------------------- 1 | local AH = wesnoth.require "ai/lua/ai_helper.lua" 2 | local COMP = wesnoth.require "~/add-ons/AI-demos/lua/compatibility.lua" 3 | 4 | local ai_helper_local = {} 5 | 6 | function ai_helper_local.movepartial_outofway_stopunit(ai, unit, x, y, cfg) 7 | local viewing_side = cfg and cfg.viewing_side or unit.side 8 | 9 | if (type(x) ~= 'number') then 10 | if x[1] then 11 | x, y = x[1], x[2] 12 | else 13 | x, y = x.x, x.y 14 | end 15 | end 16 | 17 | -- Only move unit out of way if the main unit can get there 18 | local path, cost = AH.find_path_with_shroud(unit, x, y, cfg) 19 | if (cost <= unit.moves) then 20 | local unit_in_way_proxy = COMP.get_unit(x, y) 21 | if unit_in_way_proxy and (unit_in_way_proxy ~= unit) 22 | and AH.is_visible_unit(viewing_side, unit_in_way_proxy) 23 | then 24 | AH.move_unit_out_of_way(ai, unit_in_way_proxy, cfg) 25 | end 26 | end 27 | 28 | local next_hop = AH.next_hop(unit, x, y) 29 | if next_hop and ((next_hop[1] ~= unit.x) or (next_hop[2] ~= unit.y)) then 30 | AH.checked_move(ai, unit, next_hop[1], next_hop[2]) 31 | else 32 | AH.checked_stopunit_moves(ai, unit) 33 | end 34 | end 35 | 36 | return ai_helper_local 37 | -------------------------------------------------------------------------------- /AI-demos/lua/ca_manual_move.lua: -------------------------------------------------------------------------------- 1 | ----- CA: Manual move (max_score: 999980) ----- 2 | 3 | local ca_manual_move = {} 4 | 5 | function ca_manual_move:evaluation() 6 | local start_time = wesnoth.get_time_stamp() 7 | local timeout_ms = 10000 -- milli-seconds 8 | --std_print('Start time:', start_time) 9 | 10 | local old_id = wml.variables.manual_ai_id 11 | local old_x = wml.variables.manual_ai_x 12 | local old_y = wml.variables.manual_ai_y 13 | local id, x, y = wesnoth.dofile("~/add-ons/AI-demos/lua/manual_input.lua") 14 | 15 | while (id == old_id) and (x == old_x) and (y == old_y) and (wesnoth.get_time_stamp() < start_time + timeout_ms) do 16 | id, x, y = wesnoth.dofile("~/add-ons/AI-demos/lua/manual_input.lua") 17 | end 18 | 19 | if (id == old_id) and (x == old_x) and (y == old_y) then 20 | std_print('manual move CA has timed out') 21 | return 0 22 | else 23 | return 999980 24 | end 25 | end 26 | 27 | function ca_manual_move:execution(cfg, data) 28 | local id, x, y = wesnoth.dofile("~/add-ons/AI-demos/lua/manual_input.lua") 29 | std_print('move ' .. id .. ' --> ' .. x .. ',' .. y) 30 | 31 | local unit = wesnoth.get_unit(id) 32 | ai.move(unit, x, y) 33 | 34 | wml.variables.manual_ai_id = id 35 | wml.variables.manual_ai_x = x 36 | wml.variables.manual_ai_y = y 37 | end 38 | 39 | return ca_manual_move 40 | -------------------------------------------------------------------------------- /AI-demos/lua/ca_manual_stats.lua: -------------------------------------------------------------------------------- 1 | ----- CA: Stats at beginning of turn (max_score: 999990) ----- 2 | -- This will be blacklisted after first execution each turn 3 | 4 | local ca_manual_stats = {} 5 | 6 | function ca_manual_stats:evaluation() 7 | return 999990 8 | end 9 | 10 | function ca_manual_stats:execution(cfg, data) 11 | local tod = wesnoth.get_time_of_day() 12 | std_print('\n**** Manual AI *********************************************************') 13 | std_print('Beginning of Turn ' .. wesnoth.current.turn .. ' (' .. tod.name ..') stats') 14 | 15 | local units = wesnoth.get_units() 16 | for _,unit in ipairs(units) do 17 | std_print(string.format('%-25s %2d,%2d %1d', unit.id, unit.x, unit.y, unit.side)) 18 | end 19 | 20 | std_print('************************************************************************') 21 | 22 | local id, x, y = wesnoth.dofile("~/add-ons/AI-demos/lua/manual_input.lua") 23 | wml.variables.manual_ai_id = id 24 | wml.variables.manual_ai_x = x 25 | wml.variables.manual_ai_y = y 26 | end 27 | 28 | return ca_manual_stats 29 | -------------------------------------------------------------------------------- /AI-demos/lua/ca_move_unittype.lua: -------------------------------------------------------------------------------- 1 | local AH = wesnoth.require "ai/lua/ai_helper.lua" 2 | 3 | local ca_move_unittype = {} 4 | 5 | function ca_move_unittype:evaluation(cfg) 6 | local units = wesnoth.get_units { 7 | side = wesnoth.current.side, 8 | type = cfg.type, 9 | formula = '$this_unit.moves > 0' 10 | } 11 | 12 | if units[1] then return cfg.score end 13 | return 0 14 | end 15 | 16 | function ca_move_unittype:execution(cfg) 17 | local unit = wesnoth.get_units { 18 | side = wesnoth.current.side, 19 | type = cfg.type, 20 | formula = '$this_unit.moves > 0' 21 | }[1] 22 | 23 | -- Find path toward the goal 24 | local path, cost = wesnoth.find_path(unit, cfg.goal_x, cfg.goal_y) 25 | 26 | -- If there's no path to goal, or all path hexes are occupied, 27 | -- use current position as default 28 | local next_hop = { unit.x, unit.y } 29 | 30 | -- Go through path to find farthest reachable, unoccupied hex 31 | -- Start at second index, as the first is just the unit position itself 32 | for i = 2,#path do 33 | local sub_path, sub_cost = wesnoth.find_path( unit, path[i][1], path[i][2]) 34 | 35 | -- If we can get to that hex, check whether it is occupied 36 | -- (can only be own or allied units as enemy unit hexes are not reachable) 37 | if sub_cost <= unit.moves then 38 | local unit_in_way = wesnoth.get_unit(path[i][1], path[i][2]) 39 | if not unit_in_way then 40 | next_hop = path[i] 41 | end 42 | else -- otherwise stop here; rest of path is outside movement range 43 | break 44 | end 45 | end 46 | 47 | --std_print('Moving:', unit.id, '-->', next_hop[1], next_hop[2]) 48 | AH.checked_move_full(ai, unit, next_hop[1], next_hop[2]) 49 | end 50 | 51 | return ca_move_unittype 52 | -------------------------------------------------------------------------------- /AI-demos/lua/ca_stats.lua: -------------------------------------------------------------------------------- 1 | ----- CA: Stats at beginning of turn (max_score: 999990) ----- 2 | -- This will be blacklisted after first execution each turn 3 | 4 | local DBG = wesnoth.dofile "~/add-ons/AI-demos/lua/debug.lua" 5 | local COMP = wesnoth.require "~/add-ons/AI-demos/lua/compatibility.lua" 6 | 7 | local ca_stats = {} 8 | 9 | function ca_stats:evaluation() 10 | return 999990 11 | end 12 | 13 | function ca_stats:execution() 14 | local tod = wesnoth.get_time_of_day() 15 | std_print('\n**** Fred Side ' .. wesnoth.current.side .. ' (version ' .. wesnoth.dofile('~/add-ons/AI-demos/version.lua') .. ') *******************************************************') 16 | DBG.print_ts('Beginning of Turn ' .. wesnoth.current.turn .. ' (' .. tod.name ..') stats') 17 | 18 | local sides = {} 19 | local leveled_units 20 | for _,unit_proxy in ipairs(COMP.get_units()) do 21 | local unit_side = unit_proxy.side 22 | if (not sides[unit_side]) then sides[unit_side] = {} end 23 | 24 | sides[unit_side].num_units = (sides[unit_side].num_units or 0) + 1 25 | sides[unit_side].hitpoints = (sides[unit_side].hitpoints or 0) + unit_proxy.hitpoints 26 | 27 | if unit_proxy.canrecruit then 28 | sides[unit_side].leader_type = unit_proxy.type 29 | sides[unit_side].leader_hp = unit_proxy.hitpoints 30 | sides[unit_side].leader_max_hp = unit_proxy.max_hitpoints 31 | else 32 | if (unit_side == wesnoth.current.side) and (unit_proxy.level > 1) then 33 | if (not leveled_units) then leveled_units = '' end 34 | leveled_units = leveled_units 35 | .. unit_proxy.type .. ' (' 36 | .. unit_proxy.hitpoints .. '/' .. unit_proxy.max_hitpoints .. ') ' 37 | end 38 | end 39 | end 40 | 41 | local total_villages = 0 42 | for _,village in ipairs(wesnoth.get_villages()) do 43 | local owner = wesnoth.get_village_owner(village[1], village[2]) or 0 44 | if (owner > 0) then 45 | sides[owner].num_villages = (sides[owner].num_villages or 0) + 1 46 | end 47 | 48 | total_villages = total_villages + 1 49 | end 50 | 51 | for _,side_info in ipairs(wesnoth.sides) do 52 | local side = side_info.side 53 | local num_villages = sides[side].num_villages or 0 54 | std_print(' Side ' .. side .. ': ' 55 | .. sides[side].num_units .. ' Units (' .. sides[side].hitpoints .. ' HP), ' 56 | .. num_villages .. '/' .. total_villages .. ' villages (' 57 | .. sides[side].leader_type .. ', ' .. sides[side].leader_hp .. '/' .. sides[side].leader_max_hp .. ' HP, ' .. side_info.gold .. ' gold)' 58 | ) 59 | end 60 | 61 | if leveled_units then std_print(' Leveled units: ' .. leveled_units) end 62 | 63 | std_print('************************************************************************************') 64 | end 65 | 66 | return ca_stats 67 | -------------------------------------------------------------------------------- /AI-demos/lua/compatibility.lua: -------------------------------------------------------------------------------- 1 | -- Functions for simplifying complying with both 1.14 and 1.15 standards 2 | -- Many of these could, in principle, be combined into few functions with 3 | -- function names passed as arguments. I am doing it separately intentionally, 4 | -- so that it is clear which functions are affected. 5 | 6 | local AH = wesnoth.dofile "ai/lua/ai_helper.lua" 7 | local H = wesnoth.require "helper" 8 | local I = wesnoth.require "lua/wml/items.lua" 9 | 10 | local compatibility = {} 11 | 12 | function compatibility.change_max_moves(unit_proxy, max_moves) 13 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 14 | unit_proxy.max_moves = max_moves 15 | else 16 | -- Note: this is very slow, calling it should be avoided as much as possible 17 | H.modify_unit( 18 | { id = unit_proxy.id} , 19 | { max_moves = max_moves } 20 | ) 21 | end 22 | end 23 | 24 | function compatibility.copy_unit(...) 25 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 26 | return wesnoth.units.clone(...) 27 | else 28 | return wesnoth.copy_unit(...) 29 | end 30 | end 31 | 32 | function compatibility.create_unit(...) 33 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 34 | return wesnoth.units.create(...) 35 | else 36 | return wesnoth.create_unit(...) 37 | end 38 | end 39 | 40 | function compatibility.debug_ai() 41 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 42 | return wesnoth.sides.debug_ai(wesnoth.current.side).ai 43 | else 44 | return wesnoth.debug_ai(wesnoth.current.side).ai 45 | end 46 | end 47 | 48 | function compatibility.erase_unit(...) 49 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 50 | return wesnoth.units.erase(...) 51 | else 52 | return wesnoth.erase_unit(...) 53 | end 54 | end 55 | 56 | function compatibility.extract_unit(...) 57 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 58 | return wesnoth.units.extract(...) 59 | else 60 | return wesnoth.extract_unit(...) 61 | end 62 | end 63 | 64 | function compatibility.find_path_custom_cost(unit, x, y, cost_function) 65 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 66 | return wesnoth.find_path(unit, x, y, { calculate = cost_function }) 67 | else 68 | return wesnoth.find_path(unit, x, y, cost_function) 69 | end 70 | end 71 | 72 | function compatibility.get_closest_enemy(...) 73 | local distance, enemy = AH.get_closest_enemy(...) 74 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 75 | local tmp = distance 76 | distance = enemy 77 | enemy = tmp 78 | end 79 | return distance, enemy 80 | end 81 | 82 | function compatibility.get_sides(side_filter) 83 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 84 | return wesnoth.sides.find(side_filter) 85 | else 86 | return wesnoth.get_sides(side_filter) 87 | end 88 | end 89 | 90 | function compatibility.get_starting_location(side) 91 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 92 | return wesnoth.sides[side].starting_location 93 | else 94 | return wesnoth.get_starting_location(side) 95 | end 96 | end 97 | 98 | function compatibility.get_unit(...) 99 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 100 | return wesnoth.units.get(...) 101 | else 102 | return wesnoth.get_unit(...) 103 | end 104 | end 105 | 106 | function compatibility.get_units(...) 107 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 108 | return wesnoth.units.find_on_map(...) 109 | else 110 | return wesnoth.get_units(...) 111 | end 112 | end 113 | 114 | function compatibility.is_enemy(...) 115 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 116 | return wesnoth.sides.is_enemy(...) 117 | else 118 | return wesnoth.is_enemy(...) 119 | end 120 | end 121 | 122 | function compatibility.match_unit(unit, filter) 123 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 124 | return unit:matches(filter) 125 | else 126 | return wesnoth.match_unit(unit, filter) 127 | end 128 | end 129 | 130 | function compatibility.place_halo(...) 131 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 132 | return wesnoth.interface.add_item_halo(...) 133 | else 134 | return I.place_halo(...) 135 | end 136 | end 137 | 138 | function compatibility.put_unit(...) 139 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 140 | return wesnoth.units.to_map(...) 141 | else 142 | return wesnoth.put_unit(...) 143 | end 144 | end 145 | 146 | function compatibility.remove(...) 147 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 148 | return wesnoth.interface.remove_item(...) 149 | else 150 | return I.remove(...) 151 | end 152 | end 153 | 154 | function compatibility.unit_ability(unit, ability) 155 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 156 | return unit:ability(ability) 157 | else 158 | return wesnoth.unit_ability(unit, ability) 159 | end 160 | end 161 | 162 | function compatibility.unit_defense(unit, terrain_type) 163 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 164 | return 100 - unit:defense_on(terrain_type) 165 | else 166 | return wesnoth.unit_defense(unit, terrain_type) 167 | end 168 | end 169 | 170 | function compatibility.unit_movement_cost(unit, terrain_type) 171 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 172 | return unit:movement_on(terrain_type) 173 | else 174 | return wesnoth.unit_movement_cost(unit, terrain_type) 175 | end 176 | end 177 | 178 | function compatibility.unit_resistance(unit, attack_type) 179 | if wesnoth.compare_versions(wesnoth.game_config.version, '>=', '1.15.0') then 180 | return 100 - unit:resistance_against(attack_type) 181 | else 182 | return wesnoth.unit_resistance(unit, attack_type) 183 | end 184 | end 185 | 186 | return compatibility 187 | -------------------------------------------------------------------------------- /AI-demos/lua/eval_exec_CA.lua: -------------------------------------------------------------------------------- 1 | -- This is a collection of functions that can be used to evaluate and/or execute 2 | -- Fred's main CA from the right-click menu 3 | -- 4 | -- See the github wiki page for a detailed description of how to use them: 5 | -- https://github.com/mattsc/Wesnoth-AI-Demos/wiki/CA-debugging 6 | 7 | local COMP = wesnoth.require "~/add-ons/AI-demos/lua/compatibility.lua" 8 | local DBG = wesnoth.dofile "~/add-ons/AI-demos/lua/debug.lua" 9 | 10 | local function is_CA_debugging_mode() 11 | -- Edit manually to indicate whether you want CA debugging mode enabled 12 | return false 13 | end 14 | 15 | local function is_wrong_side() 16 | local side_info = wesnoth.sides[wesnoth.current.side] 17 | local stage = wml.get_child(wml.get_child(side_info.__cfg, 'ai'), 'stage') 18 | for CA in wml.child_range(stage, 'candidate_action') do 19 | --std_print(CA.name) 20 | if (CA.name == 'fred') then 21 | return false 22 | end 23 | end 24 | 25 | wesnoth.message("!!!!! Error !!!!! You need to be in control of a side using the Fred AI CAs.") 26 | return true 27 | end 28 | 29 | local function CA_name() 30 | -- This function returns the name of the CA to be executed or evaluated 31 | -- It takes the value from WML variable 'debug_CA_name' if that variable exists, 32 | -- otherwise it defaults to 'recruit_orcs' 33 | 34 | local name = wml.variables.debug_CA_name 35 | return name or 'fred' 36 | end 37 | 38 | local function set_menus() 39 | -- Set the two menu items that have the selected CA name in them 40 | -- They need to be reset when that is changed, that's why this is done here 41 | -- Note: changing the CA name is currently not used, but we keep this for now 42 | 43 | wesnoth.wml_actions.set_menu_item { 44 | id = 'm01_eval', 45 | description = 'Evaluate Single Candidate Action: ' .. CA_name(), 46 | image = 'items/ring-red.png~CROP(26,26,20,20)', 47 | { 'command', { 48 | { 'lua', { 49 | code = 'wesnoth.dofile "~add-ons/AI-demos/lua/eval_exec_CA.lua".eval_exec_CA(false, "v")' 50 | } }, 51 | } }, 52 | { 'default_hotkey', { key = 'v' } } 53 | } 54 | 55 | wesnoth.wml_actions.set_menu_item { 56 | id = 'm02_exec', 57 | description = 'Evaluate and Execute Single Candidate Action: ' .. CA_name(), 58 | image = 'items/ring-gold.png~CROP(26,26,20,20)', 59 | { 'command', { 60 | { 'lua', { 61 | code = 'wesnoth.dofile "~add-ons/AI-demos/lua/eval_exec_CA.lua".eval_exec_CA(true, "x")' 62 | } }, 63 | } }, 64 | { 'default_hotkey', { key = 'x' } } 65 | } 66 | 67 | wesnoth.wml_actions.set_menu_item { 68 | id = 'm03_play_turn', 69 | description = "Play an entire AI turn", 70 | image = 'items/ring-white.png~CROP(26,26,20,20)', 71 | { 'command', { 72 | { 'lua', { 73 | code = 'wesnoth.dofile "~add-ons/AI-demos/lua/eval_exec_CA.lua".play_turn()' 74 | } }, 75 | } }, 76 | { 'default_hotkey', { key = 'a', shift = 'yes' } } 77 | } 78 | 79 | wesnoth.wml_actions.set_menu_item { 80 | id = 'm04_units_info', 81 | description = "Show/toggle Units Info", 82 | image = 'items/ring-silver.png~CROP(26,26,20,20)', 83 | { 'command', { 84 | { 'lua', { 85 | code = 'wesnoth.dofile "~add-ons/AI-demos/lua/eval_exec_CA.lua".units_info()' 86 | } }, 87 | } }, 88 | { 'default_hotkey', { key = 'i' } } 89 | } 90 | end 91 | 92 | local function init_CA(self) 93 | wesnoth.clear_messages() 94 | if is_wrong_side() then return end 95 | 96 | -- Get the AI table and the CA functions 97 | local ai = COMP.debug_ai() 98 | local ca = wesnoth.dofile("~add-ons/AI-demos/lua/ca_fred.lua") 99 | 100 | return ai, ca 101 | end 102 | 103 | return { 104 | activate_CA_debugging_mode = function() 105 | -- CA debugging mode is enabled only if 'is_CA_debugging_mode()' above 106 | -- returns true and if we're in debug mode 107 | if is_CA_debugging_mode() and wesnoth.game_config.debug then 108 | wesnoth.wml_actions.message { 109 | speaker = 'narrator', 110 | image = 'wesnoth-icon.png', 111 | caption = "Candidate Action Debugging Mode", 112 | message = "You are entering CA debugging mode. Check out the AI Demos github wiki about information on how to use this mode, or how to deactivate it." 113 | } 114 | 115 | wesnoth.sides[1].controller = 'human' 116 | wesnoth.sides[2].controller = 'human' 117 | 118 | set_menus() 119 | 120 | -- Also remove the menu items at the end of the scenario. 121 | -- This is only important when playing Fred from the switchboard 122 | -- scenario in AI-demos, and going back to switchboard afterward. 123 | wesnoth.add_event_handler { 124 | name = 'victory,defeat,time_over,enemies_defeated', 125 | 126 | { 'clear_menu_item', { id = 'm01_eval' } }, 127 | { 'clear_menu_item', { id = 'm02_exec' } }, 128 | { 'clear_menu_item', { id = 'm03_play_turn' } }, 129 | { 'clear_menu_item', { id = 'm04_units_info' } } 130 | } 131 | end 132 | end, 133 | 134 | eval_exec_CA = function(exec_also, hotkey) 135 | if is_wrong_side() then return end 136 | 137 | std_print('\n********** Manual command : ' .. hotkey .. ' **********\n') 138 | 139 | local self = dummy_self 140 | local ai, ca = init_CA(self) 141 | 142 | local score, action = ca:evaluation(nil, self, ai) 143 | local action_str = '' 144 | if action then 145 | action_str = action.zone_id .. ' ' .. action.action_str .. ': ' 146 | end 147 | 148 | wesnoth.message("Eval score for " .. CA_name() .. ' ' .. action_str .. score) 149 | 150 | if exec_also and (score > 0) then 151 | ca:execution(nil, self, ai) 152 | end 153 | dummy_self = self 154 | end, 155 | 156 | units_info = function(stdout_only) 157 | -- Shows some information for all units. Specifically, this links the 158 | -- unit id to its position, name etc. for easier identification 159 | local tmp_unit_proxies = COMP.get_units() 160 | local str = '' 161 | for _,u in ipairs(tmp_unit_proxies) do 162 | str = str .. string.format('%2d,%2d HP: %3d/%3d XP: %3d/%3d %s (%s)\n', 163 | u.x, u.y, 164 | u.hitpoints, u.max_hitpoints, u.experience, u.max_experience, 165 | u.id, tostring(u.name)) 166 | 167 | if wml.variables.debug_unit_labels then 168 | wesnoth.label { x = u.x, y = u.y, text = '' } 169 | else 170 | wesnoth.label { x = u.x, y = u.y, text = u.id } 171 | end 172 | end 173 | 174 | if wml.variables.debug_unit_labels then 175 | wml.variables.debug_unit_labels = nil 176 | wesnoth.clear_messages() 177 | else 178 | wml.variables.debug_unit_labels = true 179 | std_print(str) 180 | if (not stdout_only) then wesnoth.message(str) end 181 | end 182 | 183 | end, 184 | 185 | play_turn = function(ai) 186 | -- Play through an entire AI turn (fred CA only) 187 | if is_wrong_side() then return end 188 | 189 | local self = dummy_self 190 | 191 | while 1 do 192 | local ai, ca = init_CA(self) 193 | 194 | local ca_name, score, action = 'fred', ca:evaluation(nil, self, ai) 195 | 196 | if (score > 0) then 197 | local action_str = action.zone_id .. ' ' .. action.action_str .. ': ' 198 | 199 | if (not DBG.show_debug('timing')) then 200 | wesnoth.wml_actions.message { 201 | speaker = 'narrator', 202 | caption = "Executing " .. ca_name .. " CA", 203 | image = 'wesnoth-icon.png', 204 | message = "Score for " .. ca_name .. ' ' .. action_str .. score 205 | } 206 | end 207 | 208 | -- Need to evaluate the CA again first, so that 'self.data' gets set up 209 | wml.variables.debug_CA_name = ca_name 210 | ca:execution(nil, self, ai) 211 | else 212 | wesnoth.wml_actions.message { 213 | speaker = 'narrator', 214 | caption = "No more CAs with positive scores to execute", 215 | image = 'wesnoth-icon.png', message = "That's all." 216 | } 217 | break 218 | end 219 | end 220 | 221 | dummy_self = self 222 | end 223 | } 224 | -------------------------------------------------------------------------------- /AI-demos/lua/fred_config.lua: -------------------------------------------------------------------------------- 1 | -- AI configuration parameters 2 | 3 | -- Note: Assigning this table is slow compared to accessing values in it. It is 4 | -- thus done outside the functions below, so that it is not done over and over 5 | -- for each function call. 6 | local cfg = { 7 | ----- Parameters in this section are meant to be adjustable for faction, scenario, ... ----- 8 | 9 | aggression = 2, -- To first order, base ratio of acceptable damage: own / enemy 10 | min_aggression = 1.25, 11 | next_turn_influence_weight = 1, -- Fraction of ToD influence change of next turn to take into account 12 | pushfactor_min_midpoint = 1.4, 13 | milk_xp_die_chance_limit = 0, -- Enemy_leader die chance has to be greater than this to check for XP milking attack. 14 | -- Set to >=1 to disable. Engine uses 0 if this is set to negative values. 15 | 16 | action_base_ratings = { -- setting a rating to <= 0 disables the action 17 | protect_leader_eval = 32000, -- eval only 18 | attack_leader_threat = 31000, 19 | protect_leader_exec = 30000, 20 | 21 | fav_attack = 27000, 22 | advance_toward_leader = 26000, 23 | attack = 25000, 24 | 25 | protect = 22000, 26 | grab_villages = 21000, 27 | hold = 20000, 28 | 29 | recruit = 12000, 30 | retreat = 11000, 31 | advance = 10000, 32 | advance_all_map = 1000 33 | }, 34 | 35 | leader_joins_action = false, -- set to true to make leader participate in zone actions 36 | 37 | 38 | ----- Do not change values below unless you know exactly what you are doing ----- 39 | 40 | -- Unit value parameters 41 | xp_weight = 1.0, 42 | leader_weight = 1.5, 43 | leader_derating = 0.5, 44 | 45 | influence_falloff_floor = 0.5, 46 | influence_falloff_exp = 2, 47 | 48 | winning_ratio = 1.25, 49 | losing_ratio = 0.8, 50 | 51 | protect_others_ratio = 1.1, 52 | protect_min_value = 2, 53 | 54 | -- Leader rating parameters 55 | leader_unthreatened_hex_bonus = 500, 56 | leader_village_bonus = 10, 57 | leader_village_grab_bonus = 3, 58 | leader_moves_left_factor = 0.1, 59 | leader_unit_in_way_penalty = - 0.01, 60 | leader_unit_in_way_no_moves_penalty = - 1000, 61 | leader_eld_factor = - 0.01, 62 | 63 | -- Village grabbing parameters 64 | villages_per_unit = 2, 65 | 66 | -- Attack rating parameters 67 | terrain_defense_weight = 0.1, 68 | distance_leader_weight = 0.002, 69 | occupied_hex_penalty = 0.001, 70 | leader_max_die_chance = 0.02, 71 | leader_protect_weight = 1.0, 72 | unit_protect_weight = 0.5, 73 | village_protect_weight = 0.5, 74 | castle_protect_weight = 0.5, 75 | 76 | -- Hold rating parameters 77 | vuln_weight = 0.25, 78 | forward_weight = 0.02, 79 | hold_counter_weight = 0.1, 80 | protect_forward_weight = 1, 81 | 82 | -- Retreat rating parameters 83 | hp_inflection_base = 20, 84 | 85 | -- Delayed action scores 86 | score_grab_village = 20, 87 | score_leader_to_keep = 10, 88 | score_recruit = 9, 89 | score_leader_to_village = 8 90 | } 91 | 92 | local interaction_matrix = { 93 | abbrevs = { 94 | ALT = 'attack_leader_threat', 95 | PL = 'protect_leader', 96 | FA = 'favorable_attack', 97 | att = 'attack', 98 | pro = 'protect', 99 | GV = 'grab_village', 100 | adv = 'advance', 101 | rec = 'recruit', 102 | MLV = 'move_leader_to_village', 103 | MLK = 'move_leader_to_keep', 104 | ret = 'retreat' 105 | }, 106 | -- A penalty multiplier of 0 is equivalent to not setting it in the first place. 107 | -- If one is set to 0 here, that is done to emphasize that it should be ignored. 108 | penalties = { 109 | -- 'ALT' and 'PL': use 'att' 110 | att = { 111 | units = { GV = 1, MLV = 1, MLK = 0, ret = 1 }, 112 | hexes = { GV = 1, rec = 1, MLV = 1, MLK = 1, } 113 | }, 114 | hold = { 115 | units = { GV = 1, MLV = 0, MLK = 0, ret = 1 }, 116 | hexes = { GV = 1, rec = 1, MLV = 1, MLK = 1, } 117 | }, 118 | GV = { 119 | units = { MLV = 1000, Ret = 0 }, -- retreaters may be used 120 | hexes = { MLV = 1000 } 121 | } 122 | } 123 | } 124 | 125 | 126 | local fred_config = {} 127 | 128 | function fred_config.get_cfg_parm(parm) 129 | return cfg[parm] 130 | end 131 | 132 | function fred_config.interaction_matrix() 133 | return interaction_matrix 134 | end 135 | 136 | return fred_config 137 | -------------------------------------------------------------------------------- /AI-demos/lua/fred_data_incremental.lua: -------------------------------------------------------------------------------- 1 | -- Collection of functions to get information about units and the gamestate and 2 | -- collect them in tables for easy access. They are expensive, so this should 3 | -- be done as infrequently as possible, but it needs to be redone after each move. 4 | -- 5 | -- Unlike fred_data_move.lua, these functions only acquire the information 6 | -- needed at the time and add it to a cache table as only a subset of all possible 7 | -- information is needed for any move evaluation. 8 | 9 | local FGM = wesnoth.require "~/add-ons/AI-demos/lua/fred_gamestate_map.lua" 10 | local COMP = wesnoth.require "~/add-ons/AI-demos/lua/compatibility.lua" 11 | 12 | local fred_data_incremental = {} 13 | 14 | function fred_data_incremental.get_unit_defense(unit_copy, x, y, defense_maps_cache) 15 | -- Get the terrain defense of a unit as a factor (that is, e.g. 0.40 rather than 40) and cache it 16 | -- 17 | -- Inputs: 18 | -- @unit_copy: private copy of the unit (proxy table works too, but is slower) 19 | -- @x, @y: the location for which to calculate the unit's terrain defense 20 | -- @defense_maps_cache: table in which to cache the results. Note: this is NOT an optional input 21 | 22 | local unit_id = unit_copy.id 23 | local defense_map = defense_maps_cache[unit_id] 24 | if (not defense_map) then 25 | defense_maps_cache[unit_id] = {} 26 | defense_map = defense_maps_cache[unit_id] 27 | end 28 | local defense = FGM.get_value(defense_map, x, y, 'defense') 29 | if (not defense) then 30 | defense = (100. - COMP.unit_defense(unit_copy, wesnoth.get_terrain(x, y))) / 100. 31 | FGM.set_value(defense_maps_cache[unit_id], x, y, 'defense', defense) 32 | end 33 | 34 | return defense 35 | end 36 | 37 | function fred_data_incremental.get_unit_movecost(unit_copy, x, y, movecost_maps_cache) 38 | -- Get the movement cost of a unit and cache it 39 | -- 40 | -- Inputs: 41 | -- @unit_copy: private copy of the unit (proxy table works too, but is slower) 42 | -- @x, @y: the location for which to calculate the unit's movement cost 43 | -- @movecost_maps_cache: table in which to cache the results. Note: this is NOT an optional input 44 | 45 | local unit_id = unit_copy.id 46 | local movecost_map = movecost_maps_cache[unit_id] 47 | if (not movecost_map) then 48 | movecost_maps_cache[unit_id] = {} 49 | movecost_map = movecost_maps_cache[unit_id] 50 | end 51 | local movecost = FGM.get_value(movecost_map, x, y, 'movecost') 52 | if (not movecost) then 53 | movecost = COMP.unit_movement_cost(unit_copy, wesnoth.get_terrain(x, y)) 54 | FGM.set_value(movecost_maps_cache[unit_id], x, y, 'movecost', movecost) 55 | end 56 | 57 | return movecost 58 | end 59 | 60 | function fred_data_incremental.get_unit_type_attribute(unit_type, attribute_name, unit_types_cache) 61 | -- Access an attribute in the wesnoth.unit_types table and cache it 62 | -- 63 | -- Inputs: 64 | -- @unit_type: string 65 | -- @attribute_name: string containing the key for the attribute to access 66 | -- @unit_types_cache: table in which to cache the results. Note: this is NOT an optional input 67 | 68 | local unit_type_table = unit_types_cache[unit_type] 69 | if (not unit_type_table) then 70 | unit_types_cache[unit_type] = {} 71 | unit_type_table = unit_types_cache[unit_type] 72 | end 73 | local attribute_value = unit_type_table[attribute_name] 74 | if (not attribute_value) then 75 | attribute_value = wesnoth.unit_types[unit_type][attribute_name] 76 | unit_type_table[attribute_name] = attribute_value 77 | end 78 | 79 | return attribute_value 80 | end 81 | 82 | return fred_data_incremental 83 | -------------------------------------------------------------------------------- /AI-demos/lua/fred_data_move.lua: -------------------------------------------------------------------------------- 1 | local AH = wesnoth.dofile "ai/lua/ai_helper.lua" 2 | local H = wesnoth.require "helper" 3 | local FGM = wesnoth.require "~/add-ons/AI-demos/lua/fred_gamestate_map.lua" 4 | local FU = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_utils.lua" 5 | local FMU = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_map_utils.lua" 6 | local DBG = wesnoth.dofile "~/add-ons/AI-demos/lua/debug.lua" 7 | local COMP = wesnoth.require "~/add-ons/AI-demos/lua/compatibility.lua" 8 | 9 | -- Collect information about units and the gamestate and collect them in tables 10 | -- for easy access. This is expensive, so this should be done as infrequently as 11 | -- possible, but it needs to be redone after each move. 12 | -- 13 | -- TODO: this does not account for allied sides at this time 14 | -- 15 | -- Unit tables are of form { id = loc }, with loc = { x, y } 16 | -- except for leader tables, which also include the leader id 17 | 18 | local function show_timing_info(fred_data, text) 19 | -- Set to true or false manually, to enable timing info specifically for this 20 | -- function. The debug_utils timing flag still needs to be set also. 21 | if false then 22 | DBG.print_timing(fred_data, 1, text) 23 | end 24 | end 25 | 26 | local function find_connected_castles(keeps_map) 27 | local castle_map, new_hexes = {}, {} 28 | for x,y in FGM.iter(keeps_map) do 29 | FGM.set_value(castle_map, x, y, 'castle', true) 30 | if wesnoth.get_terrain_info(wesnoth.get_terrain(x, y)).keep then 31 | castle_map[x][y].keep = true 32 | end 33 | table.insert(new_hexes, { x, y }) 34 | end 35 | 36 | while (#new_hexes > 0) do 37 | local old_hexes = AH.table_copy(new_hexes) 38 | new_hexes = {} 39 | 40 | for _,hex in ipairs(old_hexes) do 41 | for xa,ya in H.adjacent_tiles(hex[1], hex[2]) do 42 | local terrain_info = wesnoth.get_terrain_info(wesnoth.get_terrain(xa, ya)) 43 | if (not FGM.get_value(castle_map, xa, ya, 'castle')) 44 | and terrain_info.castle 45 | then 46 | table.insert(new_hexes, { xa, ya }) 47 | FGM.set_value(castle_map, xa, ya, 'castle', true) 48 | if terrain_info.keep then 49 | castle_map[xa][ya].keep = true 50 | end 51 | end 52 | end 53 | end 54 | end 55 | 56 | return castle_map 57 | end 58 | 59 | 60 | local fred_data_move = {} 61 | 62 | function fred_data_move.get_move_data(fred_data) 63 | -- Returns: 64 | -- - State of villages and units on the map (all in one variable: gamestate) 65 | -- - Reach maps for all the AI's units (in separate variable: reach_maps) 66 | -- - Private unit copies (in separate variable: unit_copies) 67 | -- These are returned separately in case only part of it is needed. 68 | -- They can also be retrieved all in one table using get_move_data() 69 | -- 70 | -- See above for the information returned 71 | 72 | show_timing_info(fred_data, 'start fred_data_move.get_move_data()') 73 | 74 | local village_map = {} 75 | for _,village in ipairs(wesnoth.get_villages()) do 76 | FGM.set_value(village_map, village[1], village[2], 'owner', 77 | wesnoth.get_village_owner(village[1], village[2]) or 0 78 | ) 79 | end 80 | 81 | local units, my_units, my_units_MP, my_units_noMP, enemies = {}, {}, {}, {}, {} 82 | local unit_infos, unit_copies = {}, {} 83 | local unit_map, my_unit_map, my_unit_map_MP, my_unit_map_noMP, enemy_map = {}, {}, {}, {}, {} 84 | local my_attack_map, my_move_map = {}, {} 85 | local attack_maps = { {}, {} } 86 | local reach_maps = {} 87 | 88 | local additional_turns = 1 89 | for i = 1,additional_turns+1 do 90 | my_attack_map[i] = {} 91 | my_move_map[i] = {} 92 | end 93 | 94 | local unit_proxies = COMP.get_units() 95 | 96 | local leaders = {} -- this is not added to move_data 97 | for _,unit_proxy in ipairs(unit_proxies) do 98 | local unit_info = FU.single_unit_info(unit_proxy, fred_data.caches.unit_types) 99 | local unit_copy = COMP.copy_unit(unit_proxy) 100 | local id, unit_x, unit_y = unit_info.id, unit_copy.x, unit_copy.y 101 | local current_power = unit_info.current_power 102 | local max_moves = unit_info.max_moves 103 | 104 | unit_infos[id] = unit_info 105 | unit_copies[id] = unit_copy 106 | 107 | FGM.set_value(unit_map, unit_x, unit_y, 'id', id) 108 | 109 | attack_maps[1][id] = {} 110 | attack_maps[2][id] = {} 111 | 112 | units[id] = { unit_x, unit_y } 113 | 114 | if unit_info.canrecruit then 115 | leaders[unit_info.side] = { unit_x, unit_y, id = id } 116 | end 117 | 118 | if (unit_info.side == wesnoth.current.side) then 119 | FGM.set_value(my_unit_map, unit_x, unit_y, 'id', id) 120 | my_units[id] = { unit_x, unit_y } 121 | 122 | -- Hexes the unit can reach in additional_turns+1 turns 123 | local reach = wesnoth.find_reach(unit_copy, { additional_turns = additional_turns }) 124 | 125 | local n_reach_this_turn = 0 126 | reach_maps[id] = {} 127 | local attack_range, moves_left = {}, {} 128 | 129 | for _,loc in ipairs(reach) do 130 | -- reach_map: 131 | -- Only first-turn moves counts toward reach_maps 132 | if (loc[3] >= (max_moves * additional_turns)) then 133 | FGM.set_value(reach_maps[id], loc[1], loc[2], 'moves_left', loc[3] - (max_moves * additional_turns)) 134 | n_reach_this_turn = n_reach_this_turn + 1 135 | end 136 | 137 | local turns = (additional_turns + 1) - loc[3] / max_moves 138 | local int_turns = math.ceil(turns) 139 | if (int_turns == 0) then int_turns = 1 end 140 | 141 | if (not my_move_map[int_turns][loc[1]]) then my_move_map[int_turns][loc[1]] = {} end 142 | if (not my_move_map[int_turns][loc[1]][loc[2]]) then my_move_map[int_turns][loc[1]][loc[2]] = {} end 143 | if (not my_move_map[int_turns][loc[1]][loc[2]].ids) then my_move_map[int_turns][loc[1]][loc[2]].ids = {} end 144 | table.insert(my_move_map[int_turns][loc[1]][loc[2]].ids, id) 145 | 146 | local moves_left_this_turn = loc[3] % max_moves 147 | if (loc[1] == unit_x) and (loc[2] == unit_y) then 148 | moves_left_this_turn = max_moves 149 | end 150 | 151 | -- attack_range: for attack_map 152 | if (not attack_range[loc[1]]) then 153 | attack_range[loc[1]] = {} 154 | moves_left[loc[1]] = {} 155 | end 156 | if (not attack_range[loc[1]][loc[2]]) or (attack_range[loc[1]][loc[2]] > int_turns) then 157 | attack_range[loc[1]][loc[2]] = int_turns 158 | moves_left[loc[1]][loc[2]] = moves_left_this_turn 159 | elseif (attack_range[loc[1]][loc[2]] == int_turns) and (moves_left[loc[1]][loc[2]] < moves_left_this_turn) then 160 | moves_left[loc[1]][loc[2]] = moves_left_this_turn 161 | end 162 | 163 | for xa,ya in H.adjacent_tiles(loc[1], loc[2]) do 164 | if (not attack_range[xa]) then 165 | attack_range[xa] = {} 166 | moves_left[xa] = {} 167 | end 168 | 169 | if (not attack_range[xa][ya]) or (attack_range[xa][ya] > int_turns) then 170 | attack_range[xa][ya] = int_turns 171 | moves_left[xa][ya] = moves_left_this_turn 172 | elseif (attack_range[xa][ya] == int_turns) and (moves_left[xa][ya] < moves_left_this_turn) then 173 | moves_left[xa][ya] = moves_left_this_turn 174 | end 175 | end 176 | end 177 | 178 | -- This is not a standard fgsmap 179 | for x,arr in pairs(attack_range) do 180 | for y,int_turns in pairs(arr) do 181 | if (not my_attack_map[int_turns][x]) then my_attack_map[int_turns][x] = {} end 182 | if (not my_attack_map[int_turns][x][y]) then my_attack_map[int_turns][x][y] = {} end 183 | if (not my_attack_map[int_turns][x][y].ids) then my_attack_map[int_turns][x][y].ids = {} end 184 | table.insert(my_attack_map[int_turns][x][y].ids, id) 185 | 186 | if (int_turns <= 2) then 187 | FGM.set_value(attack_maps[int_turns][id], x, y, 'current_power', current_power) 188 | attack_maps[int_turns][id][x][y].moves_left_this_turn = moves_left[x][y] 189 | end 190 | end 191 | end 192 | 193 | if (n_reach_this_turn > 1) then 194 | FGM.set_value(my_unit_map_MP, unit_x, unit_y, 'id', id) 195 | my_units_MP[id] = { unit_x, unit_y } 196 | else 197 | FGM.set_value(my_unit_map_noMP, unit_x, unit_y, 'id', id) 198 | my_units_noMP[id] = { unit_x, unit_y } 199 | end 200 | else 201 | if COMP.is_enemy(unit_info.side, wesnoth.current.side) then 202 | FGM.set_value(enemy_map, unit_x, unit_y, 'id', id) 203 | enemies[id] = { unit_x, unit_y } 204 | end 205 | end 206 | end 207 | 208 | show_timing_info(fred_data, ' reachmaps') 209 | 210 | -- reach_maps: eliminate hexes with other own units that cannot move out of the way 211 | -- Also, there might be hidden enemies on the hex 212 | for id,reach_map in pairs(reach_maps) do 213 | for id_noMP,loc in pairs(my_units_noMP) do 214 | if (id ~= id_noMP) then 215 | if reach_map[loc[1]] then reach_map[loc[1]][loc[2]] = nil end 216 | end 217 | end 218 | end 219 | 220 | -- Also mark units that can only get to hexes occupied by own units 221 | local my_units_can_move_away = {} 222 | for x,y,data in FGM.iter(my_unit_map_MP) do 223 | for x2,y2,_ in FGM.iter(reach_maps[data.id]) do 224 | if (not FGM.get_value(my_unit_map, x2, y2, 'id')) then 225 | my_units_can_move_away[data.id] = true 226 | break 227 | end 228 | end 229 | end 230 | 231 | show_timing_info(fred_data, ' keeps and castles') 232 | 233 | -- Reachable keeps: those the leader can actually move onto (AI side only) 234 | -- Reachable castles: connected to reachable keeps, independent of whether the leader 235 | -- can get there or not, but must be available for recruiting (no enemy or noMP units on it) 236 | -- Close keeps: those within one full move, not taking other units into account (all sides) 237 | -- Close castles: connected to close keeps, not taking other units into account 238 | local reachable_keeps_map, reachable_castles_map = {}, {} 239 | local close_keeps_map, close_castles_map = {}, {} 240 | local my_leader, enemy_leader 241 | for side,leader in ipairs(leaders) do 242 | if (side == wesnoth.current.side) then 243 | my_leader = leader 244 | else 245 | enemy_leader = leader 246 | end 247 | 248 | local leader_copy = unit_copies[leader.id] 249 | local old_moves = leader_copy.moves 250 | leader_copy.moves = leader_copy.max_moves 251 | local reach = wesnoth.find_reach(leader_copy, { ignore_units = true }) 252 | leader_copy.moves = old_moves 253 | 254 | close_keeps_map[side], close_castles_map[side] = {}, {} 255 | 256 | for _,loc in ipairs(reach) do 257 | if wesnoth.get_terrain_info(wesnoth.get_terrain(loc[1], loc[2])).keep then 258 | FGM.set_value(close_keeps_map[side], loc[1], loc[2], 'moves_left', loc[3]) 259 | if (side == wesnoth.current.side) then 260 | local ml = FGM.get_value(reach_maps[leader.id], loc[1], loc[2], 'moves_left') 261 | if ml then -- this excludes hexes with units without MP 262 | FGM.set_value(reachable_keeps_map, loc[1], loc[2], 'moves_left', ml) 263 | end 264 | end 265 | end 266 | end 267 | 268 | close_castles_map[side] = find_connected_castles(close_keeps_map[side]) 269 | if (side == wesnoth.current.side) then 270 | reachable_castles_map = find_connected_castles(reachable_keeps_map) 271 | for x,y in FGM.iter(reachable_castles_map) do 272 | if FGM.get_value(enemy_map, x, y, 'id') or FGM.get_value(my_unit_map_noMP, x, y, 'id') then 273 | reachable_castles_map[x][y] = nil 274 | end 275 | end 276 | end 277 | 278 | if DBG.show_debug('ops_keep_maps') then 279 | DBG.show_fgm_with_message(close_keeps_map[side], 'moves_left', 'close_keeps_map: Side ' .. side, { x = leader[1], y = leader[2] }) 280 | DBG.show_fgm_with_message(close_castles_map[side], 'castle', 'close_castles_map: Side ' .. side, { x = leader[1], y = leader[2] }) 281 | if (side == wesnoth.current.side) then 282 | DBG.show_fgm_with_message(reachable_keeps_map, 'moves_left', 'reachable_keeps_map: Side ' .. side, { x = leader[1], y = leader[2] }) 283 | DBG.show_fgm_with_message(reachable_castles_map, 'castle', 'reachable_castles_map: Side ' .. side, { x = leader[1], y = leader[2] }) 284 | end 285 | end 286 | end 287 | 288 | show_timing_info(fred_data, ' enemy maps') 289 | 290 | -- Get enemy attack and reach maps 291 | -- These are for max MP of enemy units, and after taking all AI units with MP off the map 292 | -- This is almost the same as for my_units, but there are enough differences that this should be its own code block 293 | local enemy_attack_map, trapped_enemies = {}, {} 294 | 295 | for i = 1,additional_turns+1 do 296 | enemy_attack_map[i] = {} 297 | end 298 | 299 | -- Take all own units with MP left off the map (for enemy pathfinding) 300 | local extracted_units = {} 301 | for id,loc in pairs(my_units_MP) do 302 | local unit_proxy = COMP.get_unit(loc[1], loc[2]) 303 | COMP.extract_unit(unit_proxy) 304 | table.insert(extracted_units, unit_proxy) 305 | end 306 | 307 | for enemy_id,_ in pairs(enemies) do 308 | local old_moves = unit_infos[enemy_id].moves 309 | unit_copies[enemy_id].moves = unit_copies[enemy_id].max_moves 310 | local unit_x, unit_y = unit_copies[enemy_id].x, unit_copies[enemy_id].y 311 | 312 | attack_maps[1][enemy_id] = {} 313 | attack_maps[2][enemy_id] = {} 314 | 315 | -- Hexes the enemy can reach in additional_turns+1 turns 316 | local reach = wesnoth.find_reach(unit_copies[enemy_id], { additional_turns = additional_turns }) 317 | 318 | unit_copies[enemy_id].moves = old_moves 319 | 320 | reach_maps[enemy_id] = {} 321 | local is_trapped = true 322 | local attack_range, moves_left = {}, {} 323 | local max_moves = unit_infos[enemy_id].max_moves 324 | for _,loc in ipairs(reach) do 325 | -- reach_map: 326 | -- Only first-turn moves counts toward reach_maps 327 | if (loc[3] >= (max_moves * additional_turns)) then 328 | FGM.set_value(reach_maps[enemy_id], loc[1], loc[2], 'moves_left', loc[3] - (max_moves * additional_turns)) 329 | end 330 | 331 | local turns = (additional_turns + 1) - loc[3] / max_moves 332 | local int_turns = math.ceil(turns) 333 | if (int_turns == 0) then int_turns = 1 end 334 | 335 | -- We count all enemies that cannot move more than 1 hex from their 336 | -- current location (for whatever reason) as trapped 337 | if is_trapped and (int_turns == 1) then 338 | if (wesnoth.map.distance_between(loc[1], loc[2], unit_copies[enemy_id].x, unit_copies[enemy_id].y) > 1) then 339 | is_trapped = nil 340 | end 341 | end 342 | 343 | local moves_left_this_turn = loc[3] % max_moves 344 | if (loc[1] == unit_x) and (loc[2] == unit_y) then 345 | moves_left_this_turn = max_moves 346 | end 347 | 348 | -- attack_range: for attack_map 349 | if (not attack_range[loc[1]]) then 350 | attack_range[loc[1]] = {} 351 | moves_left[loc[1]] = {} 352 | end 353 | if (not attack_range[loc[1]][loc[2]]) or (attack_range[loc[1]][loc[2]] > int_turns) then 354 | attack_range[loc[1]][loc[2]] = int_turns 355 | moves_left[loc[1]][loc[2]] = moves_left_this_turn 356 | elseif (attack_range[loc[1]][loc[2]] == int_turns) and (moves_left[loc[1]][loc[2]] < moves_left_this_turn) then 357 | moves_left[loc[1]][loc[2]] = moves_left_this_turn 358 | end 359 | 360 | for xa,ya in H.adjacent_tiles(loc[1], loc[2]) do 361 | if (not attack_range[xa]) then 362 | attack_range[xa] = {} 363 | moves_left[xa] = {} 364 | end 365 | 366 | if (not attack_range[xa][ya]) or (attack_range[xa][ya] > int_turns) then 367 | attack_range[xa][ya] = int_turns 368 | moves_left[xa][ya] = moves_left_this_turn 369 | elseif (attack_range[xa][ya] == int_turns) and (moves_left[xa][ya] < moves_left_this_turn) then 370 | moves_left[xa][ya] = moves_left_this_turn 371 | end 372 | end 373 | end 374 | 375 | if is_trapped then 376 | trapped_enemies[enemy_id] = true 377 | end 378 | 379 | -- This is not a standard fgsmap 380 | for x,arr in pairs(attack_range) do 381 | for y,int_turns in pairs(arr) do 382 | if (not enemy_attack_map[int_turns][x]) then enemy_attack_map[int_turns][x] = {} end 383 | if (not enemy_attack_map[int_turns][x][y]) then enemy_attack_map[int_turns][x][y] = {} end 384 | if (not enemy_attack_map[int_turns][x][y].ids) then enemy_attack_map[int_turns][x][y].ids = {} end 385 | table.insert(enemy_attack_map[int_turns][x][y].ids, enemy_id) 386 | 387 | if (int_turns <= 2) then 388 | FGM.set_value(attack_maps[int_turns][enemy_id], x, y, 'current_power', unit_infos[enemy_id].current_power) 389 | attack_maps[int_turns][enemy_id][x][y].moves_left_this_turn = moves_left[x][y] 390 | end 391 | end 392 | end 393 | end 394 | 395 | -- Put the own units with MP back out there 396 | for _,extracted_unit in ipairs(extracted_units) do COMP.put_unit(extracted_unit) end 397 | 398 | 399 | -- Keeping all of this in one place, as a "table of contents" 400 | fred_data.caches.attacks = {} -- unlike the other caches, we clear this every move, otherwise it gets too big 401 | fred_data.move_data = { 402 | units = units, 403 | my_units = my_units, 404 | my_units_MP = my_units_MP, 405 | my_units_can_move_away = my_units_can_move_away, 406 | my_units_noMP = my_units_noMP, 407 | enemies = enemies, 408 | my_leader = my_leader, 409 | enemy_leader = enemy_leader, 410 | 411 | reachable_keeps_map = reachable_keeps_map, 412 | reachable_castles_map = reachable_castles_map, 413 | close_keeps_map = close_keeps_map, 414 | close_castles_map = close_castles_map, 415 | village_map = village_map, 416 | 417 | -- can any of these be combined? 418 | unit_map = unit_map, 419 | attack_maps = attack_maps, 420 | 421 | my_unit_map = my_unit_map, 422 | my_unit_map_MP = my_unit_map_MP, 423 | my_unit_map_noMP = my_unit_map_noMP, 424 | my_move_map = my_move_map, 425 | my_attack_map = my_attack_map, 426 | 427 | enemy_map = enemy_map, 428 | enemy_attack_map = enemy_attack_map, 429 | trapped_enemies = trapped_enemies, 430 | 431 | reach_maps = reach_maps, 432 | unit_copies = unit_copies, 433 | unit_infos = unit_infos 434 | } 435 | 436 | show_timing_info(fred_data, ' influence maps') 437 | 438 | local influence_map, influence_maps = FMU.get_influence_maps(fred_data) 439 | 440 | fred_data.move_data.influence_map = influence_map 441 | fred_data.move_data.influence_maps = influence_maps 442 | 443 | show_timing_info(fred_data, 'end fred_data_move.get_move_data()') 444 | end 445 | 446 | return fred_data_move 447 | -------------------------------------------------------------------------------- /AI-demos/lua/fred_data_turn.lua: -------------------------------------------------------------------------------- 1 | local FGM = wesnoth.require "~/add-ons/AI-demos/lua/fred_gamestate_map.lua" 2 | local DBG = wesnoth.dofile "~/add-ons/AI-demos/lua/debug.lua" 3 | 4 | local fred_data_turn = {} 5 | 6 | function fred_data_turn.set_turn_data(fred_data) 7 | -- The if statement below is so that debugging works when starting the evaluation in the 8 | -- middle of the turn. In normal gameplay, we can just use the existing enemy reach maps, 9 | -- so that we do not have to double-calculate them. 10 | local move_data = fred_data.move_data 11 | local enemy_initial_reach_maps = {} 12 | if (not next(move_data.my_units_noMP)) then 13 | --std_print('Using existing enemy move map') 14 | for enemy_id,_ in pairs(move_data.enemies) do 15 | enemy_initial_reach_maps[enemy_id] = {} 16 | for x,y,data in FGM.iter(move_data.reach_maps[enemy_id]) do 17 | FGM.set_value(enemy_initial_reach_maps[enemy_id], x, y, 'moves_left', data.moves_left) 18 | end 19 | end 20 | else 21 | --std_print('Need to create new enemy move map') 22 | for enemy_id,_ in pairs(move_data.enemies) do 23 | enemy_initial_reach_maps[enemy_id] = {} 24 | 25 | local old_moves = move_data.unit_copies[enemy_id].moves 26 | move_data.unit_copies[enemy_id].moves = move_data.unit_copies[enemy_id].max_moves 27 | local reach = wesnoth.find_reach(move_data.unit_copies[enemy_id], { ignore_units = true }) 28 | move_data.unit_copies[enemy_id].moves = old_moves 29 | 30 | for _,loc in ipairs(reach) do 31 | FGM.set_value(enemy_initial_reach_maps[enemy_id], loc[1], loc[2], 'moves_left', loc[3]) 32 | end 33 | end 34 | end 35 | 36 | if DBG.show_debug('ops_enemy_initial_reach_maps') then 37 | for enemy_id,_ in pairs(move_data.enemies) do 38 | DBG.show_fgm_with_message(enemy_initial_reach_maps[enemy_id], 'moves_left', 'enemy_initial_reach_maps', move_data.unit_copies[enemy_id]) 39 | end 40 | end 41 | 42 | fred_data.turn_data = { 43 | turn_number = wesnoth.current.turn, 44 | side_number = wesnoth.current.side, 45 | enemy_initial_reach_maps = enemy_initial_reach_maps 46 | } 47 | end 48 | 49 | function fred_data_turn.update_turn_data(fred_data) 50 | -- In case an enemy has died 51 | for enemy_id,eirm in pairs(fred_data.turn_data.enemy_initial_reach_maps) do 52 | if (not fred_data.move_data.enemies[enemy_id]) then 53 | fred_data.turn_data.enemy_initial_reach_maps[enemy_id] = nil 54 | end 55 | end 56 | end 57 | 58 | return fred_data_turn 59 | -------------------------------------------------------------------------------- /AI-demos/lua/fred_gamestate_map.lua: -------------------------------------------------------------------------------- 1 | local H = wesnoth.require "helper" 2 | 3 | local fred_gamestate_map = {} 4 | 5 | function fred_gamestate_map.get_value(map, x, y, key) 6 | if (not key) then error("Required parameter 'key' is missing in call to fred_gamestate_map.get_value()") end 7 | return (map[x] and map[x][y] and map[x][y][key]) 8 | end 9 | 10 | function fred_gamestate_map.set_value(map, x, y, key, value) 11 | if (not map[x]) then map[x] = {} end 12 | if (not map[x][y]) then map[x][y] = {} end 13 | map[x][y][key] = value 14 | end 15 | 16 | function fred_gamestate_map.add(map, x, y, key, value) 17 | local old_value = fred_gamestate_map.get_value(map, x, y, key) or 0 18 | fred_gamestate_map.set_value(map, x, y, key, old_value + value) 19 | end 20 | 21 | function fred_gamestate_map.iter(map) 22 | function each_hex(state) 23 | while state.x ~= nil do 24 | local child = map[state.x] 25 | state.y = next(child, state.y) 26 | if state.y == nil then 27 | state.x = next(map, state.x) 28 | else 29 | return state.x, state.y, child[state.y] 30 | end 31 | end 32 | end 33 | 34 | return each_hex, { x = next(map) } 35 | end 36 | 37 | function fred_gamestate_map.normalize(map, key) 38 | local mx 39 | for _,_,data in fred_gamestate_map.iter(map) do 40 | if (not mx) or (data[key] > mx) then 41 | mx = data[key] 42 | end 43 | end 44 | for _,_,data in fred_gamestate_map.iter(map) do 45 | data[key] = data[key] / mx 46 | end 47 | end 48 | 49 | function fred_gamestate_map.blur(map, key) 50 | for x,y,data in fred_gamestate_map.iter(map) do 51 | local blurred_data = data[key] 52 | if blurred_data then 53 | local count = 1 54 | local adj_weight = 0.5 55 | for xa,ya in H.adjacent_tiles(x, y) do 56 | local value = fred_gamestate_map.get_value(map, xa, ya, key) 57 | if value then 58 | blurred_data = blurred_data + value * adj_weight 59 | count = count + adj_weight 60 | end 61 | end 62 | fred_gamestate_map.set_value(map, x, y, 'blurred_' .. key, blurred_data / count) 63 | end 64 | end 65 | end 66 | 67 | return fred_gamestate_map 68 | -------------------------------------------------------------------------------- /AI-demos/lua/fred_map_config.lua: -------------------------------------------------------------------------------- 1 | -- These set all the scenario/map specific information 2 | 3 | local AH = wesnoth.require "ai/lua/ai_helper.lua" 4 | 5 | local fred_map_config = {} 6 | 7 | function fred_map_config.get_side_cfgs() 8 | -- These are one off from wesnoth.special_locations[2] on Freelands 9 | -- TODO: test whether that makes a difference 10 | local cfgs = { 11 | { start_hex = { 18, 4 } }, 12 | { start_hex = { 20, 20 } } 13 | } 14 | 15 | return cfgs 16 | end 17 | 18 | function fred_map_config.get_attack_test_locs() 19 | -- TODO: this is really just a placeholder for now until I know whether this works 20 | -- It's easy to have this found automatically 21 | local locs = { 22 | attacker_loc = { 28, 13 }, 23 | defender_loc = { 28, 14 } 24 | } 25 | 26 | return locs 27 | end 28 | 29 | function fred_map_config.get_raw_cfgs(zone_id) 30 | local cfg_all_map = { 31 | zone_id = 'all_map', 32 | ops_slf = { include_borders = 'no' }, 33 | center_hexes = { { 20, 20 } } 34 | } 35 | 36 | local cfg_leader_threat = { 37 | zone_id = 'leader', 38 | ops_slf = { include_borders = 'no' }, 39 | } 40 | 41 | local ops_slf_west = { x = '4-16,4-15,4-14,4-16,4-17', y = '1-6,7-12,13-15,16,17-23' } 42 | local center_hexes_west = { { 10, 13 } } 43 | 44 | local ops_slf_center = { x = '16-20,16-22,16-23,15-22,15-22,17-22,18-22', y = '7-8,9,10-12,13-14,15,16,17' } 45 | local center_hexes_center = { { 18, 12 }, { 20, 12 } } 46 | 47 | local ops_slf_east = { x = '21-34,23-34,24-34,23-34,22-34', y = '1-8,9,10-12,13-17,18-23' } 48 | local center_hexes_east = { { 28, 13 } } 49 | 50 | local cfg_right = { 51 | zone_id = 'right' 52 | } 53 | 54 | local cfg_center = { 55 | zone_id = 'center' 56 | } 57 | 58 | local cfg_left = { 59 | zone_id = 'left' 60 | } 61 | 62 | if (wesnoth.current.side == 1) then 63 | local enemy_leader_slf = { x = '17-22', y = '18-24' } 64 | 65 | cfg_right.ops_slf = ops_slf_west 66 | table.insert(cfg_right.ops_slf, { "or", enemy_leader_slf }) 67 | cfg_right.center_hexes = center_hexes_west 68 | 69 | cfg_center.ops_slf = ops_slf_center 70 | table.insert(cfg_center.ops_slf, { "or", enemy_leader_slf }) 71 | cfg_center.center_hexes = center_hexes_center 72 | 73 | cfg_left.ops_slf = ops_slf_east 74 | table.insert(cfg_left.ops_slf, { "or", enemy_leader_slf }) 75 | cfg_left.center_hexes = center_hexes_east 76 | else 77 | local enemy_leader_slf = { x = '16-21', y = '1-7' } 78 | 79 | cfg_right.ops_slf = ops_slf_east 80 | table.insert(cfg_right.ops_slf, { "or", enemy_leader_slf }) 81 | cfg_right.center_hexes = center_hexes_east 82 | 83 | cfg_center.ops_slf = ops_slf_center 84 | table.insert(cfg_center.ops_slf, { "or", enemy_leader_slf }) 85 | cfg_center.center_hexes = center_hexes_center 86 | 87 | cfg_left.ops_slf = ops_slf_west 88 | table.insert(cfg_left.ops_slf, { "or", enemy_leader_slf }) 89 | cfg_left.center_hexes = center_hexes_west 90 | end 91 | 92 | 93 | -- Replacement zones: 94 | local cfg_top = { 95 | zone_id = 'top', 96 | center_hexes = {} 97 | } 98 | for _,hex in ipairs(cfg_center.center_hexes) do table.insert(cfg_top.center_hexes, hex) end 99 | for _,hex in ipairs(cfg_left.center_hexes) do table.insert(cfg_top.center_hexes, hex) end 100 | cfg_top.ops_slf = AH.table_copy(cfg_center.ops_slf) 101 | table.insert(cfg_top.ops_slf, { "or", cfg_left.ops_slf }) 102 | 103 | if (wesnoth.current.side == 1) then 104 | cfg_top.enemy_slf = { x = '17-34,21-34', y = '1-9,10' } 105 | else 106 | -- This one is much more complicated than for the other side, because of the uneven hex symmetry 107 | cfg_top.enemy_slf = { x = '4,6,8,10,12,14,16,4-18,20,4-21', y = '14,14,14,14,14,14,14,15,15,16-23' } 108 | end 109 | 110 | if (zone_id == 'leader_threat') then 111 | return cfg_leader_threat 112 | end 113 | 114 | if (not zone_id) then 115 | local zone_cfgs = { 116 | right = cfg_right, 117 | center = cfg_center, 118 | left = cfg_left 119 | } 120 | return zone_cfgs 121 | elseif (zone_id == 'all') then 122 | local all_cfgs = { 123 | leader_threat = cfg_leader_threat, 124 | right = cfg_right, 125 | center = cfg_center, 126 | left = cfg_left, 127 | top = cfg_top, 128 | all_map = cfg_all_map 129 | } 130 | return all_cfgs 131 | else 132 | local cfgs = { 133 | leader_threat = cfg_leader_threat, 134 | right = cfg_right, 135 | center = cfg_center, 136 | left = cfg_left, 137 | top = cfg_top, 138 | all_map = cfg_all_map 139 | } 140 | 141 | for _,cfg in pairs(cfgs) do 142 | if (cfg.zone_id == zone_id) then 143 | return cfg 144 | end 145 | end 146 | end 147 | end 148 | 149 | function fred_map_config.replace_zone_ids() 150 | local zone_ids = { { 151 | old = { 'center', 'left' }, 152 | new = 'top' 153 | } } 154 | 155 | return zone_ids 156 | end 157 | 158 | return fred_map_config 159 | -------------------------------------------------------------------------------- /AI-demos/lua/fred_scenario_events.lua: -------------------------------------------------------------------------------- 1 | -- Fred events (start/end messages, Easter eggs, etc.) are set up using Lua 2 | -- so that they can be inserted conditionally only into Fred games, and not 3 | -- into every game when AI-demos is active. 4 | 5 | -- TODO: the start and end messages will not show up any more, because the id 6 | -- of the side leader has been change to FredN, where N is the side number. 7 | -- That's okay, they were getting old anyway. Keep the events for now, but 8 | -- I'll probably remove them at some point altogether. 9 | 10 | ---------- Start message ---------- 11 | wesnoth.add_event_handler { 12 | name = 'side 1 turn 1', 13 | 14 | { 'message', { 15 | id = 'Fred', 16 | caption = "Fred (Freelands AI v$AI_Demos_version)", 17 | message = "Good luck, have fun!\n" 18 | } } 19 | } 20 | 21 | ---------- End message ---------- 22 | wesnoth.add_event_handler { 23 | name = 'last_breath', 24 | { 'filter', { canrecruit = 'yes' } }, 25 | 26 | { 'message', { 27 | id = 'Fred', 28 | message = "Good game, thanks!", 29 | scroll = 'no' 30 | } } 31 | } 32 | 33 | ---------- Assassin exclaim ---------- 34 | -- Assassin exclaims in frustration if he misses all 3 strikes (2 events) 35 | -- Assassin Event 1: The actual message. This is a separate event and without 36 | -- first_time_only=yes, so that it only fires once per scenario 37 | wesnoth.add_event_handler { 38 | name = 'assassin_message', 39 | 40 | { 'set_variable', { 41 | name = 'random', 42 | rand = "Oh come on!,I don't believe this!,What the …" 43 | } }, 44 | { 'message', { 45 | id = '$unit.id', 46 | message = "$random" 47 | } }, 48 | { 'clear_variable', { name = 'random' } } 49 | } 50 | 51 | -- Assassin Event 2: The trigger 52 | wesnoth.add_event_handler { 53 | name = 'attack end', 54 | first_time_only = 'no', 55 | 56 | -- Only happens for assassins on Fred's side 57 | { 'filter', { 58 | type = 'Orcish Assassin', 59 | { 'filter_side', { 60 | { 'has_unit', { id = 'Fred1,Fred2' } } 61 | } } 62 | } }, 63 | -- Only if the defender is not poisoned after the attack. This also 64 | -- excludes attacks in which then enemy is already poisoned beforehand, 65 | -- but that does not matter, as it is not supposed to show every time anyway. 66 | -- Exclude unpoisonable units, as they may produce a false positive. 67 | { 'filter_second', { 68 | { 'filter_wml', { 69 | { 'not', { 70 | { 'status', { poisoned = 'yes' } } 71 | } }, 72 | { 'not', { 73 | { 'status', { unpoisonable = 'yes' } } 74 | } } 75 | } } 76 | } }, 77 | -- Only after the poison attack 78 | { 'filter_attack', { range = 'ranged' } }, 79 | 80 | -- And even if all this is true, the message is only displayed with a 33% chance 81 | -- Also need to check that defender did not die, otherwise he might not count as 82 | -- poisoned (if this was the first hit) 83 | { 'set_variable', { 84 | name = 'random', 85 | rand = "1..3" 86 | } }, 87 | 88 | { 'if', { 89 | { 'variable', { 90 | name = 'random', 91 | equals = 1 92 | } }, 93 | { 'variable', { 94 | name = 'second_unit.hitpoints', 95 | greater_than = 0 96 | } }, 97 | 98 | { 'then', { 99 | { 'fire_event', { 100 | name = 'assassin_message', 101 | { 'primary_unit' , { id = '$unit.id' } } 102 | } } 103 | } } 104 | } }, 105 | 106 | { 'clear_variable', { name = 'random' } } 107 | } 108 | 109 | ---------- Goblin boasts ---------- 110 | -- Goblin with 1HP boasts if he survives an attack (2 events) 111 | -- Goblin Event 1: The actual message. This is a separate event and without 112 | -- first_time_only=yes, so that it only fires once per scenario 113 | wesnoth.add_event_handler { 114 | name = 'goblin_boasts', 115 | 116 | { 'message', { 117 | id = '$unit.id', 118 | message = "Haw Haw!" 119 | } }, 120 | 121 | { 'set_variable', { 122 | name = 'boast_goblin_id', 123 | value = "$unit.id" 124 | } }, 125 | 126 | { 'event', { 127 | name = 'last breath', 128 | { 'filter', { id = '$boast_goblin_id' } }, 129 | 130 | { 'message', { 131 | id = '$unit.id', 132 | message = "I guess that's what I get for being cocky." 133 | } }, 134 | 135 | { 'clear_variable', { name = 'boast_goblin_id' } } 136 | } } 137 | } 138 | 139 | -- Goblin Event 2: The trigger 140 | wesnoth.add_event_handler { 141 | name = 'attack', 142 | first_time_only = 'no', 143 | 144 | -- Only happens for goblins on Fred's side that have 1 HP before the attack 145 | { 'filter_second', { 146 | race = 'goblin', 147 | { 'filter_side', { 148 | { 'has_unit', { id = 'Fred1,Fred2' } } 149 | } }, 150 | { 'filter_wml', { hitpoints = 1 } } 151 | } }, 152 | 153 | -- Attack end without filter, so it has to be the same attack 154 | { 'event', { 155 | name = 'attack end', 156 | 157 | -- 50% chance of the goblin boasting, if he still has 1 HP 158 | { 'set_variable', { 159 | name = 'random', 160 | rand = "1..2" 161 | } }, 162 | 163 | { 'if', { 164 | { 'variable', { 165 | name = 'random', 166 | equals = 1 167 | } }, 168 | { 'variable', { 169 | name = 'second_unit.hitpoints', 170 | greater_than = 0 171 | } }, 172 | 173 | { 'then', { 174 | { 'fire_event', { 175 | name = 'goblin_boasts', 176 | { 'primary_unit' , { id = '$second_unit.id' } } 177 | } } 178 | } } 179 | } }, 180 | 181 | { 'clear_variable', { name = 'random' } } 182 | } } 183 | } 184 | 185 | ---------- Lucky level-up message ---------- 186 | -- Unit with fewer than 5 HP surviving and leveling up exclaims (2 events) 187 | -- Lucky Level-up Event 1: The actual message. This is a separate event and without 188 | -- first_time_only=yes, so that it only fires once per scenario 189 | wesnoth.add_event_handler { 190 | name = 'lucky_levelup', 191 | 192 | { 'message', { 193 | id = '$unit.id', 194 | message = "Ha! What does not kill you, makes you stronger." 195 | } } 196 | } 197 | 198 | -- Lucky Level-up Event 2: The trigger 199 | wesnoth.add_event_handler { 200 | name = 'attack', 201 | first_time_only = 'no', 202 | 203 | -- For any unit on Fred's side being attacked 204 | { 'filter_second', { 205 | { 'filter_side', { 206 | { 'has_unit', { id = 'Fred1,Fred2' } } 207 | } }, 208 | } }, 209 | 210 | -- If HP < 5, XP = max_XP-1 and attacker is at least level 1 211 | { 'if', { 212 | { 'variable', { 213 | name = 'unit.level', 214 | greater_than = 0 215 | } }, 216 | { 'variable', { 217 | name = 'second_unit.hitpoints', 218 | less_than = 5 219 | } }, 220 | { 'variable', { 221 | name = 'second_unit.experience', 222 | equals = '$($second_unit.max_experience-1)' 223 | } }, 224 | 225 | { 'then', { 226 | { 'event', { 227 | name = 'attack end', 228 | 229 | -- 33% chance of the message being triggered, if the unit survived 230 | { 'set_variable', { 231 | name = 'random', 232 | rand = "1..3" 233 | } }, 234 | 235 | { 'if', { 236 | { 'variable', { 237 | name = 'random', 238 | equals = 1 239 | } }, 240 | { 'variable', { 241 | name = 'second_unit.hitpoints', 242 | greater_than = 0 243 | } }, 244 | 245 | { 'then', { 246 | { 'fire_event', { 247 | name = 'lucky_levelup', 248 | { 'primary_unit' , { id = '$second_unit.id' } } 249 | } } 250 | } } 251 | } }, 252 | 253 | { 'clear_variable', { name = 'random' } } 254 | } } 255 | } } 256 | } } 257 | } 258 | 259 | ---------- Fred angry message ---------- 260 | -- Leveled-up unit with decent HP being killed causes Fred to exclaim (2 events) 261 | -- Fred Angry Event 1: The actual message. This is a separate event and without 262 | -- first_time_only=yes, so that it only fires once per scenario 263 | wesnoth.add_event_handler { 264 | name = 'Fred_angry', 265 | 266 | { 'message', { 267 | id = 'Fred1,Fred2', 268 | side = '$unit.side', 269 | message = "You'll pay for that!" 270 | } } 271 | } 272 | 273 | -- Fred Angry Event 2: The trigger 274 | wesnoth.add_event_handler { 275 | name = 'attack', 276 | first_time_only = 'no', 277 | -- For any unit on Fred's side (other than Fred himself) being attacked 278 | { 'filter_second', { 279 | canrecruit = 'no', 280 | { 'filter_side', { 281 | { 'has_unit', { id = 'Fred1,Fred2' } } 282 | } }, 283 | } }, 284 | 285 | -- If HP > 19 and level > 1 286 | { 'if', { 287 | { 'variable', { 288 | name = 'second_unit.hitpoints', 289 | greater_than = 19 290 | } }, 291 | { 'variable', { 292 | name = 'second_unit.level', 293 | greater_than = 1 294 | } }, 295 | 296 | 297 | { 'then', { 298 | { 'event', { 299 | name = 'attack end', 300 | 301 | -- 50% chance of the message being triggered, if the unit survived 302 | { 'set_variable', { 303 | name = 'random', 304 | rand = "1..2" 305 | } }, 306 | 307 | { 'if', { 308 | { 'variable', { 309 | name = 'random', 310 | equals = 1 311 | } }, 312 | { 'variable', { 313 | name = 'second_unit.hitpoints', 314 | less_than_equal_to = 0 315 | } }, 316 | 317 | { 'then', { 318 | { 'fire_event', { 319 | name = 'Fred_angry', 320 | { 'primary_unit' , { id = '$second_unit.id' } } 321 | } } 322 | } } 323 | } }, 324 | 325 | { 'clear_variable', { name = 'random' } } 326 | } } 327 | } } 328 | } } 329 | } 330 | -------------------------------------------------------------------------------- /AI-demos/lua/fred_scenario_setup.lua: -------------------------------------------------------------------------------- 1 | local COMP = wesnoth.require "~/add-ons/AI-demos/lua/compatibility.lua" 2 | 3 | local function fred_sides() 4 | -- Check which sides are played by Fred 5 | -- We do this by testing whether the 'fred' CA exists 6 | -- Returns an array of the side numbers played by Fred 7 | 8 | local fred_sides = {} 9 | for _,side_info in ipairs(wesnoth.sides) do 10 | local stage = wml.get_child(wml.get_child(side_info.__cfg, 'ai'), 'stage') 11 | for CA in wml.child_range(stage, 'candidate_action') do 12 | --std_print(CA.name) 13 | if (CA.name == 'fred') then 14 | table.insert(fred_sides, side_info.side) 15 | break 16 | end 17 | end 18 | end 19 | 20 | return fred_sides 21 | end 22 | 23 | local fred_scenario_setup = {} 24 | 25 | function fred_scenario_setup.fred_scenario_setup() 26 | local fred_sides_table = fred_sides() 27 | 28 | if fred_sides_table[1] then 29 | -- First we set the names and ids of the leaders of all sides played by Fred 30 | -- This is done mostly for the messages 31 | for _,fred_side in pairs(fred_sides_table) do 32 | wesnoth.wml_actions.modify_unit { 33 | { 'filter', { side = fred_side, canrecruit = 'yes' } }, 34 | id = 'Fred' .. fred_side, 35 | name = 'Fred Side ' .. fred_side 36 | } 37 | end 38 | 39 | -- We put this into a WML variable, so that it can easily be retrieved from replays 40 | wml.variables['AI_Demos_version'] = wesnoth.dofile('~/add-ons/AI-demos/version.lua') 41 | 42 | local version = wml.variables.AI_Demos_version 43 | version = version or '?.?.?' 44 | 45 | -- Freelands map is checked through the map size and the starting location of side 1 46 | local width, height = wesnoth.get_map_size() 47 | local start_loc = COMP.get_starting_location(1) 48 | 49 | if (width ~= 37) or (height ~= 24) or (start_loc[1] ~= 19) or ((start_loc[2] ~= 4) and (start_loc[2] ~= 20)) then 50 | wesnoth.wml_actions.message { 51 | id = 'Fred' .. fred_sides_table[1], 52 | caption = "Fred (Freelands AI v" .. version .. ")", 53 | message = "I currently only know how to play the Freelands map. Sorry!" 54 | } 55 | wesnoth.wml_actions.endlevel { result = 'victory' } 56 | return 57 | end 58 | 59 | -- Turn off fog and shroud if set 60 | local fog_set = false 61 | for _,side_info in ipairs(wesnoth.sides) do 62 | if side_info.fog or side_info.shroud then 63 | fog_set = true 64 | end 65 | end 66 | if fog_set then 67 | wesnoth.wml_actions.message { 68 | id = 'Fred' .. fred_sides_table[1], 69 | message = "I'm noticing that you have fog/shroud turned on. I'm turning it off in order to help with testing." 70 | } 71 | wesnoth.wml_actions.modify_side { 72 | side = '1,2', 73 | fog = false, 74 | shroud = false 75 | } 76 | end 77 | 78 | -- Set the message and easter egg events 79 | wesnoth.require "~/add-ons/AI-demos/lua/fred_scenario_events.lua" 80 | 81 | -- Set the behavior display menu options 82 | wesnoth.wml_actions.set_menu_item { 83 | id = 'm08_show_behavior', 84 | description = "Toggle Fred's behavior analysis display", 85 | image = 'items/ring-white.png~CROP(26,26,20,20)', 86 | { 'show_if', { 87 | { 'lua', { 88 | code = "return wesnoth.game_config.debug" 89 | } }, 90 | } }, 91 | { 'command', { 92 | { 'lua', { 93 | code = "local options = { 'off', 'start turn instructions only', 'move instructions (text)', 'move instructions (map)', 'move instructions (text & map)' }" 94 | .. " local fred_show_behavior = wml.variables.fred_show_behavior or 1" 95 | .. " fred_show_behavior = (fred_show_behavior % #options) + 1" 96 | .. " wml.variables.fred_show_behavior = fred_show_behavior" 97 | .. " local str = 'Show behavior now set to ' .. fred_show_behavior .. ': ' .. options[fred_show_behavior] .. ' (press shift-b to change)'" 98 | .. " wesnoth.message('Fred', str)" 99 | .. " std_print(str)" 100 | } }, 101 | } }, 102 | { 'default_hotkey', { key = 'b', shift = 'yes' } } 103 | } 104 | 105 | wesnoth.wml_actions.set_menu_item { 106 | id = 'm09_last_behavior', 107 | description = "Fred's most recent behavior instructions", 108 | image = 'items/ring-white.png~CROP(26,26,20,20)', 109 | { 'show_if', { 110 | { 'lua', { 111 | code = "return wesnoth.game_config.debug" 112 | } }, 113 | } }, 114 | { 'command', { 115 | { 'lua', { 116 | code = "local fred_behavior_str = wml.variables.fred_behavior_str or 'No behavior instructions yet'" 117 | .. " wesnoth.message('Fred', fred_behavior_str)" 118 | .. " std_print(fred_behavior_str)" 119 | } }, 120 | } }, 121 | { 'default_hotkey', { key = 'b' } } 122 | } 123 | 124 | -- Also remove the variables and menu items at the end of the scenario. 125 | -- This is only important when playing Fred from the switchboard 126 | -- scenario in AI-demos, and going back to switchboard afterward. 127 | -- The behavior variables also need to be deleted when loading. 128 | wesnoth.add_event_handler { 129 | name = 'victory,defeat,time_over,enemies_defeated,preload', 130 | first_time_only = 'no', 131 | { 'clear_variable', { name = 'fred_behavior_str' } }, 132 | { 'clear_variable', { name = 'fred_show_behavior' } } 133 | } 134 | wesnoth.add_event_handler { 135 | name = 'victory,defeat,time_over,enemies_defeated', 136 | first_time_only = 'no', 137 | { 'clear_variable', { name = 'AI_Demos_version' } }, 138 | { 'clear_menu_item', { id = 'm08_show_behavior' } }, 139 | { 'clear_menu_item', { id = 'm09_last_behavior' } } 140 | } 141 | 142 | ---------- CA debugging mode ---------- 143 | -- This needs to be activated here once, but this preload event is set 144 | -- to be removed after one use (because of all the other stuff in it), 145 | -- so we then also set up another preload event with first_time_only=no. 146 | -- That one will fire when saves/replays get loaded. 147 | wesnoth.dofile "~add-ons/AI-demos/lua/eval_exec_CA.lua".activate_CA_debugging_mode() 148 | 149 | wesnoth.add_event_handler { 150 | name = 'preload', 151 | first_time_only = 'no', 152 | 153 | { 'lua', { 154 | code = 'wesnoth.dofile "~add-ons/AI-demos/lua/eval_exec_CA.lua".activate_CA_debugging_mode()' 155 | } } 156 | } 157 | end 158 | end 159 | 160 | return fred_scenario_setup 161 | -------------------------------------------------------------------------------- /AI-demos/lua/fred_status.lua: -------------------------------------------------------------------------------- 1 | -- Set up (and reset) hypothetical situations on the map that can be used for 2 | -- analyses of ZoC, counter attacks etc. 3 | 4 | local H = wesnoth.require "helper" 5 | local FGM = wesnoth.require "~/add-ons/AI-demos/lua/fred_gamestate_map.lua" 6 | local FU = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_utils.lua" 7 | local FAU = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_attack_utils.lua" 8 | local FVS = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_virtual_state.lua" 9 | local DBG = wesnoth.dofile "~/add-ons/AI-demos/lua/debug.lua" 10 | local COMP = wesnoth.require "~/add-ons/AI-demos/lua/compatibility.lua" 11 | 12 | local fred_status = {} 13 | 14 | function fred_status.is_significant_threat(unit_info, av_total_loss, max_total_loss) 15 | -- Currently this is only set up for the leader. 16 | -- TODO: generalize? 17 | -- 18 | -- We only consider these leader threats, if they either 19 | -- - maximum damage reduces current HP by more than 75% 20 | -- - average damage is more than 50% of max HP 21 | -- Otherwise we assume the leader can deal with them alone 22 | 23 | local significant_threat = false 24 | if (max_total_loss >= unit_info.hitpoints * 0.75) 25 | or (av_total_loss >= unit_info.max_hitpoints * 0.5) 26 | then 27 | significant_threat = true 28 | end 29 | 30 | return significant_threat 31 | end 32 | 33 | function fred_status.unit_exposure(pos_counter_rating, neg_counter_rating) 34 | -- @pos_counter_rating needs to be a positive number (that is, as returned by 35 | -- and from the point of view of the counter attack) 36 | -- Equivalently, @neg_counter_rating needs to be negative (also as returned by the counter attack eval) 37 | -- Just done to standardize this 38 | local exposure = pos_counter_rating + neg_counter_rating / 10 39 | if (exposure < 0) then exposure = 0 end 40 | return exposure 41 | end 42 | 43 | function fred_status.check_exposures(objectives, combo, virtual_reach_maps, cfg, fred_data) 44 | -- The virtual state needs to be set up for this, but virtual_reach_maps are 45 | -- calculated here unless passed as a parameter. 46 | -- Potential TODO: skip evaluation of units/hexes that have already been shown 47 | -- to be protected. However, no expensive analysis is done for these anyway, 48 | -- they are sorted out both for reach_maps and counter attacks, so the overhead 49 | -- is likely small. Eventually can do some timing tests on this. 50 | -- 51 | -- @cfg: optional parameters 52 | -- zone_id: if set, use only protect units/villages in that zone 53 | -- exclude_leader: if true, leader exposure is not checked and set to zero; this is 54 | -- to be used when the leader is part of the action to be checked 55 | 56 | local move_data = fred_data.move_data 57 | local zone_id = cfg and cfg.zone_id 58 | local exclude_leader = cfg and cfg.exclude_leader 59 | 60 | local units = {} 61 | for zid,protect_objectives in pairs(objectives.protect.zones) do 62 | if (not zone_id) or (zone_id == zid) then 63 | for _,unit in ipairs(protect_objectives.units) do 64 | units[unit.id] = { unit.x, unit.y } 65 | end 66 | end 67 | end 68 | --DBG.dbms(units, false, 'units') 69 | 70 | local villages = {} 71 | for zid,protect_objectives in pairs(objectives.protect.zones) do 72 | if (not zone_id) or (zone_id == zid) then 73 | for _,village in ipairs(protect_objectives.villages or {}) do 74 | villages[village.x * 1000 + village.y] = { 75 | loc = { village.x, village.y }, 76 | exposure = village.raw_benefit 77 | } 78 | end 79 | end 80 | end 81 | --DBG.dbms(villages, false, 'villages') 82 | 83 | if (not virtual_reach_maps) then 84 | local to_unit_locs, to_locs = {}, {} 85 | if (not exclude_leader) then 86 | table.insert(to_unit_locs, objectives.leader.final) 87 | end 88 | for x,y,_ in FGM.iter(move_data.close_castles_map[wesnoth.current.side]) do 89 | table.insert(to_locs, { x, y }) 90 | end 91 | for id,loc in pairs(units) do 92 | table.insert(to_unit_locs, loc) 93 | end 94 | for xy,village in pairs(villages) do 95 | table.insert(to_locs, village.loc) 96 | end 97 | --DBG.dbms(to_locs, false, 'to_locs') 98 | --DBG.dbms(to_unit_locs, false, 'to_unit_locs') 99 | 100 | virtual_reach_maps = FVS.virtual_reach_maps(move_data.enemies, to_unit_locs, to_locs, move_data) 101 | --DBG.dbms(virtual_reach_maps, false, 'virtual_reach_maps') 102 | end 103 | 104 | local cfg_attack = { value_ratio = fred_data.ops_data.behavior.orders.value_ratio } 105 | 106 | local leader_exposure, enemy_power, is_protected, is_significant_threat = 0, 0, true, false 107 | if (not exclude_leader) then 108 | local leader = move_data.my_leader 109 | local leader_target = {} 110 | leader_target[leader.id] = objectives.leader.final 111 | local counter_outcomes, _, all_attackers = FAU.calc_counter_attack( 112 | leader_target, nil, nil, nil, virtual_reach_maps, cfg_attack, fred_data 113 | ) 114 | --DBG.dbms(counter_outcomes) 115 | --DBG.dbms(all_attackers) 116 | 117 | local pos_rating, neg_rating = 0, 0 118 | if counter_outcomes then 119 | is_protected = false 120 | pos_rating = counter_outcomes.rating_table.pos_rating 121 | neg_rating = counter_outcomes.rating_table.neg_rating 122 | --std_print('pos_rating, neg_rating', pos_rating, neg_rating) 123 | local leader_info = move_data.unit_infos[leader.id] 124 | is_significant_threat = fred_status.is_significant_threat( 125 | leader_info, 126 | leader_info.hitpoints - counter_outcomes.def_outcome.average_hp, 127 | leader_info.hitpoints - counter_outcomes.def_outcome.min_hp 128 | ) 129 | leader_exposure = fred_status.unit_exposure(pos_rating, neg_rating) 130 | --std_print('leader_exposure', leader_exposure) 131 | for enemy_id,_ in pairs(all_attackers) do 132 | -- TODO: not 100% sure yet whether we should use only the zone enemies, 133 | -- or all enemies, or both for different purposes 134 | --if (not zone_id) 135 | -- or (not fred_data.ops_data.assigned_enemies[zone_id]) 136 | -- or (fred_data.ops_data.assigned_enemies[zone_id][enemy_id]) 137 | --then 138 | enemy_power = enemy_power + move_data.unit_infos[enemy_id].current_power 139 | --end 140 | end 141 | end 142 | --std_print('ratings: ' .. pos_rating, neg_rating, is_significant_threat) 143 | end 144 | 145 | local status = { leader = { 146 | exposure = leader_exposure, 147 | is_protected = is_protected, 148 | is_significant_threat = is_significant_threat, 149 | enemy_power = enemy_power 150 | } } 151 | 152 | 153 | local n_castles_threatened, castle_hexes = 0, {} 154 | for x,y,_ in FGM.iter(move_data.close_castles_map[wesnoth.current.side]) do 155 | --std_print('castle: ', x .. ',' .. y) 156 | for enemy_id,_ in pairs(move_data.enemies) do 157 | --DBG.show_fgm_with_message(virtual_reach_maps[enemy_id], 'moves_left', 'virtual_reach_map', move_data.unit_copies[enemy_id]) 158 | -- enemies with 0 HP are not in virtual_reach_maps 159 | if virtual_reach_maps[enemy_id] and FGM.get_value(virtual_reach_maps[enemy_id], x, y, 'moves_left') then 160 | n_castles_threatened = n_castles_threatened + 1 161 | table.insert(castle_hexes, { x, y }) 162 | break 163 | end 164 | end 165 | end 166 | --std_print('n_castles_threatened: ' .. n_castles_threatened) 167 | status.castles = { 168 | n_threatened = n_castles_threatened , 169 | exposure = 3 * math.sqrt(n_castles_threatened), 170 | locs = castle_hexes 171 | } 172 | 173 | 174 | status.villages = {} 175 | for xy,village in pairs(villages) do 176 | --std_print(' check protection of village: ' .. village.loc[1] .. ',' .. village.loc[2]) 177 | local is_protected, exposure = true, 0 178 | for enemy_id,_ in pairs(move_data.enemies) do 179 | local moves_left = virtual_reach_maps[enemy_id] and FGM.get_value(virtual_reach_maps[enemy_id], village.loc[1], village.loc[2], 'moves_left') 180 | if moves_left then 181 | --std_print(' can reach this: ' .. enemy_id, moves_left) 182 | is_protected = false 183 | exposure = village.exposure 184 | break 185 | end 186 | end 187 | --std_print(' is_protected: ', is_protected) 188 | status.villages[xy] = { 189 | is_protected = is_protected, 190 | exposure = exposure 191 | } 192 | end 193 | 194 | 195 | status.units = {} 196 | for id,loc in pairs(units) do 197 | local target = {} 198 | target[id] = loc 199 | local counter_outcomes = FAU.calc_counter_attack( 200 | target, nil, nil, nil, virtual_reach_maps, cfg_attack, fred_data 201 | ) 202 | local exposure, is_protected = 0, true 203 | if counter_outcomes then 204 | --DBG.dbms(counter_outcomes.rating_table, false, 'counter_outcomes.rating_table') 205 | is_protected = false 206 | exposure = fred_status.unit_exposure(counter_outcomes.rating_table.pos_rating, counter_outcomes.rating_table.neg_rating) 207 | end 208 | status.units[id] = { 209 | exposure = exposure, 210 | is_protected = is_protected 211 | } 212 | end 213 | 214 | 215 | -- Add a terrain bonus if the combo hexes have better terrain defense for the enemies 216 | -- than those adjacent to the combo hexes that the enemies can reach with the combo in place 217 | -- Potential TODOs: 218 | -- - The value of status.terrain does not mean anything. Currently it is only used to 219 | -- determine whether good terrain is taken away from the enemies, so it's essentially used as a boolean 220 | -- - We add up all terrain (dis)advantages for all enemy units on the best adjacent hexes they 221 | -- can get to, even if there are more enemies than hexes 222 | -- - We might want to add something more sophisticated, indicating how useful the protected 223 | -- terrain is or something. It's also only a single-hex (plus adjacents) analysis for now. 224 | -- Might want to consider all hexes not reachable for the enemies any more. 225 | local unit_values = {} 226 | status.terrain = 0 227 | if combo then 228 | for src,dst in pairs(combo) do 229 | local dst_x, dst_y = math.floor(dst / 1000), dst % 1000 230 | --std_print(dst_x, dst_y) 231 | for enemy_id,_ in pairs(move_data.enemies) do 232 | --std_print(enemy_id) 233 | if FGM.get_value(move_data.reach_maps[enemy_id], dst_x, dst_y, 'moves_left') then 234 | local defense = 100 - COMP.unit_defense(move_data.unit_copies[enemy_id], wesnoth.get_terrain(dst_x, dst_y)) 235 | --std_print(' ' .. dst_x .. ',' .. dst_y .. ': ' .. enemy_id , defense) 236 | 237 | local max_adj_defense = - math.huge 238 | for xa,ya in H.adjacent_tiles(dst_x, dst_y) do 239 | if FGM.get_value(virtual_reach_maps[enemy_id], xa, ya, 'moves_left') then 240 | local adj_defense = 100 - COMP.unit_defense(move_data.unit_copies[enemy_id], wesnoth.get_terrain(xa, ya)) 241 | --std_print(' can reach adj: ' .. xa .. ',' .. ya, adj_defense) 242 | if (adj_defense > max_adj_defense) then 243 | max_adj_defense = adj_defense 244 | end 245 | end 246 | end 247 | --std_print(' defense, adj_defense: ', defense, max_adj_defense) 248 | 249 | if (defense > max_adj_defense) then 250 | local unit_value = unit_values[enemy_id] 251 | if not unit_value then 252 | unit_value = move_data.unit_infos[enemy_id].unit_value 253 | --std_print(' value: ' .. enemy_id, unit_value) 254 | unit_values[enemy_id] = unit_value 255 | end 256 | 257 | local terrain_bonus = (defense - max_adj_defense) / 100 * unit_value 258 | status.terrain = status.terrain + terrain_bonus 259 | end 260 | end 261 | end 262 | end 263 | end 264 | --std_print('terrain advantage: ' .. status.terrain) 265 | 266 | return status 267 | end 268 | 269 | return fred_status 270 | -------------------------------------------------------------------------------- /AI-demos/lua/fred_village_utils.lua: -------------------------------------------------------------------------------- 1 | local FU = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_utils.lua" 2 | local FGM = wesnoth.require "~/add-ons/AI-demos/lua/fred_gamestate_map.lua" 3 | local FDI = wesnoth.require "~/add-ons/AI-demos/lua/fred_data_incremental.lua" 4 | local FCFG = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_config.lua" 5 | local FAU = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_attack_utils.lua" 6 | local FBU = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_benefits_utilities.lua" 7 | local DBG = wesnoth.dofile "~/add-ons/AI-demos/lua/debug.lua" 8 | local COMP = wesnoth.require "~/add-ons/AI-demos/lua/compatibility.lua" 9 | 10 | local fred_village_utils = {} 11 | 12 | function fred_village_utils.village_objectives(zone_cfgs, side_cfgs, zone_maps, fred_data) 13 | local move_data = fred_data.move_data 14 | 15 | local village_objectives, villages_to_grab = { zones = {} }, {} 16 | for x,y,_ in FGM.iter(move_data.village_map) do 17 | local village_zone 18 | -- Note that we iterate over @zone_cfgs here, as @zone_maps can also contain maps for other zones 19 | for zone_id,_ in pairs(zone_cfgs) do 20 | if FGM.get_value(zone_maps[zone_id], x, y, 'in_zone') then 21 | village_zone = zone_id 22 | break 23 | end 24 | end 25 | if (not village_zone) then village_zone = 'other' end 26 | 27 | local eld_vill = wesnoth.map.distance_between(x, y, fred_data.move_data.enemy_leader[1], fred_data.move_data.enemy_leader[2]) 28 | 29 | local my_infl = FGM.get_value(move_data.influence_map, x, y, 'my_influence') or 0 30 | local enemy_infl = FGM.get_value(move_data.influence_map, x, y, 'enemy_influence') or 0 31 | local infl_ratio = my_infl / (enemy_infl + 1e-6) 32 | 33 | local owner = FGM.get_value(move_data.village_map, x, y, 'owner') 34 | 35 | -- Only add village protect objective if AI units can actually protect it. 36 | -- Even if the influence is positive there might be an enemy unit on the village. 37 | if (not FGM.get_value(fred_data.move_data.enemy_map, x, y, 'id')) 38 | and (infl_ratio >= fred_data.ops_data.behavior.orders.value_ratio) 39 | then 40 | local is_threatened = false 41 | for enemy_id,enemy_loc in pairs(move_data.enemies) do 42 | if FGM.get_value(move_data.reach_maps[enemy_id], x, y, 'moves_left') then 43 | is_threatened = true 44 | 45 | if (not village_objectives.zones[village_zone]) then 46 | village_objectives.zones[village_zone] = { villages = {}, enemies = {} } 47 | end 48 | 49 | village_objectives.zones[village_zone].enemies[enemy_id] = enemy_loc[1] * 1000 + enemy_loc[2] 50 | end 51 | end 52 | 53 | if is_threatened then 54 | -- TODO: this is currently duplicated in the benefits functions 55 | local raw_benefit = 3 56 | if (owner ~= wesnoth.current.side) and (owner ~= 0) then 57 | raw_benefit = raw_benefit * 2 58 | end 59 | local v = { 60 | x = x, y = y, 61 | type = 'village', 62 | eld = eld_vill, 63 | raw_benefit = raw_benefit 64 | } 65 | table.insert(village_objectives.zones[village_zone].villages, v) 66 | end 67 | end 68 | 69 | if (owner ~= wesnoth.current.side) then 70 | table.insert(villages_to_grab, { 71 | x = x, y = y, 72 | owner = owner, 73 | zone_id = village_zone 74 | }) 75 | end 76 | end 77 | 78 | for _,objectives in pairs(village_objectives.zones) do 79 | table.sort(objectives.villages, function(a, b) return a.eld < b.eld end) 80 | end 81 | 82 | return village_objectives, villages_to_grab 83 | end 84 | 85 | 86 | function fred_village_utils.village_grabs(villages_to_grab, reserved_actions, interactions, fred_data) 87 | local move_data = fred_data.move_data 88 | local value_ratio = fred_data.ops_data.behavior.orders.value_ratio 89 | local cfg_attack = { value_ratio = value_ratio } 90 | 91 | -- Units with MP need to be taken off the map, for counter attack calculation 92 | local extracted_units = {} 93 | for id,loc in pairs(move_data.my_units_MP) do 94 | local unit_proxy = COMP.get_unit(loc[1], loc[2]) 95 | COMP.extract_unit(unit_proxy) 96 | table.insert(extracted_units, unit_proxy) -- Not a proxy unit any more at this point 97 | end 98 | 99 | local village_grabs = {} 100 | for _,village in pairs(villages_to_grab) do 101 | local x, y = village.x, village.y 102 | --std_print('village at ' .. x .. ',' .. y) 103 | 104 | -- Exclude reserved locations here (and units below) without checking for 105 | -- combined unit/hex availability, as we do not want to enter those units 106 | -- and villages into the pool for grabbable villages 107 | local penalty_loc = FBU.action_penalty({ { loc = { x, y } } }, reserved_actions, interactions, move_data) 108 | local ids = {} 109 | if (penalty_loc == 0) then 110 | ids = FGM.get_value(move_data.my_move_map[1], x, y, 'eff_reach_ids') or {} 111 | end 112 | 113 | local influence = FGM.get_value(move_data.influence_map, x, y, 'full_move_influence') or 0 114 | local my_number = FGM.get_value(move_data.influence_map, x, y, 'my_number') or 0 115 | local enemy_number = FGM.get_value(move_data.influence_map, x, y, 'enemy_number') or 0 116 | --std_print(' influence, numbers: ' .. influence, my_number, enemy_number) 117 | 118 | for _,id in pairs(ids) do 119 | local loc = move_data.my_units[id] 120 | 121 | local penalty_unit = FBU.action_penalty({ { id = id } }, reserved_actions, interactions, move_data) 122 | if (penalty_unit == 0) then 123 | local target = {} 124 | target[id] = { x, y } 125 | 126 | -- TODO: do we take prerecruits into account here already? 127 | local counter_outcomes = FAU.calc_counter_attack( 128 | target, { loc }, { { x, y } }, nil, nil, cfg_attack, fred_data 129 | ) 130 | --if counter_outcomes then DBG.dbms(counter_outcomes.def_outcome, false, 'counter_outcomes.def_outcome') end 131 | 132 | local allow_grab = true 133 | local die_chance = 0 134 | local counter_rating 135 | if counter_outcomes then 136 | die_chance = counter_outcomes.def_outcome.hp_chance[0] 137 | counter_rating = counter_outcomes.rating_table.rating 138 | local value_loss = counter_outcomes.rating_table.pos_rating + counter_outcomes.rating_table.neg_rating 139 | --std_print(' ' .. id, die_chance, value_loss) 140 | 141 | if move_data.unit_infos[id].canrecruit then 142 | local min_hp = move_data.unit_infos[id].max_hitpoints 143 | for hp,chance in pairs(counter_outcomes.def_outcome.hp_chance) do 144 | --std_print(hp,chance) 145 | if (chance > 0) and (hp < min_hp) then 146 | min_hp = hp 147 | end 148 | end 149 | if (min_hp < move_data.unit_infos[id].max_hitpoints / 2) then 150 | allow_grab = false 151 | end 152 | --std_print(id, min_hp, move_data.unit_infos[id].max_hitpoints, allow_grab) 153 | else 154 | local xp_mult = FU.weight_s(move_data.unit_infos[id].experience / move_data.unit_infos[id].max_experience, 0.5) 155 | local level = move_data.unit_infos[id].level 156 | -- TODO: this equation needs to be tweaked 157 | local die_chance_threshold = (0.5 + 0.5 * (1 - value_ratio)) * (1 - xp_mult) / level^2 158 | 159 | local value_loss_threshold = math.huge 160 | 161 | if (my_number < 3) and (my_number < enemy_number) and (influence < 0) then 162 | --std_print(' *** dangerous village') 163 | die_chance_threshold = 0 164 | value_loss_threshold = 6 165 | end 166 | 167 | if (die_chance > die_chance_threshold) or (value_loss > value_loss_threshold) then 168 | allow_grab = false 169 | end 170 | --std_print(' ' .. id, value_ratio, xp_mult, level, die_chance_threshold, allow_grab) 171 | end 172 | end 173 | 174 | if allow_grab then 175 | local grabber = { 176 | x = x, y = y, 177 | id = id, 178 | die_chance = die_chance, 179 | counter_rating = counter_rating, 180 | zone_id = village.zone_id 181 | } 182 | table.insert(village_grabs, grabber) 183 | end 184 | end 185 | end 186 | end 187 | 188 | -- Put the units back on the map 189 | for _,extracted_unit in ipairs(extracted_units) do COMP.put_unit(extracted_unit) end 190 | 191 | return village_grabs 192 | end 193 | 194 | 195 | function fred_village_utils.assign_scouts(villages_to_grab, unused_units, assigned_units, move_data) 196 | -- Potential TODOs: 197 | -- - Add threat assessment for scout routes; it might in fact make sense to use 198 | -- injured units to scout in unthreatened areas 199 | -- - Actually assigning scouting actions 200 | -- Find how many units are needed in each zone for moving toward villages ('exploring') 201 | local units_needed_villages, zone_villages = {}, {} 202 | local villages_per_unit = FCFG.get_cfg_parm('villages_per_unit') 203 | for _,village in pairs(villages_to_grab) do 204 | if (not units_needed_villages[village.zone_id]) then 205 | units_needed_villages[village.zone_id] = 0 206 | zone_villages[village.zone_id] = {} 207 | end 208 | units_needed_villages[village.zone_id] = units_needed_villages[village.zone_id] + 1 / villages_per_unit 209 | table.insert(zone_villages[village.zone_id], { x = village.x, y = village.y }) 210 | end 211 | --DBG.dbms(units_needed_villages, false, 'units_needed_villages') 212 | --DBG.dbms(zone_villages, false, 'zone_villages') 213 | 214 | local units_assigned_villages = {} 215 | local used_ids = {} 216 | for zone_id,units in pairs(assigned_units) do 217 | for id,_ in pairs(units) do 218 | units_assigned_villages[zone_id] = (units_assigned_villages[zone_id] or 0) + 1 219 | used_ids[id] = true 220 | end 221 | end 222 | --DBG.dbms(units_assigned_villages, false, 'units_assigned_villages') 223 | 224 | 225 | -- Check out what other units to send in this direction 226 | local scouts = {} 227 | for zone_id,units_needed in pairs(units_needed_villages) do 228 | if (units_needed > (units_assigned_villages[zone_id] or 0)) then 229 | --std_print(zone_id, units_needed) 230 | scouts[zone_id] = {} 231 | for _,village in ipairs(zone_villages[zone_id]) do 232 | -- TODO: 'grab_only' is currently not set and the 'if' below is always true. 233 | -- Keep for now anyway, as we might want to reactivate this 234 | if (not village.grab_only) then 235 | --std_print(' ' .. village.x, village.y) 236 | for id,_ in pairs(unused_units) do 237 | --std_print(' ' .. id) 238 | -- The leader is always excluded here, plus any unit that has already been assigned 239 | -- TODO: set up an array of unassigned units? 240 | if (not move_data.unit_infos[id].canrecruit) and (not used_ids[id]) then 241 | local _, cost = wesnoth.find_path(move_data.unit_copies[id], village.x, village.y) 242 | cost = cost + move_data.unit_infos[id].max_moves - move_data.unit_infos[id].moves 243 | --std_print(' ' .. id, cost) 244 | local _, cost_ign = wesnoth.find_path(move_data.unit_copies[id], village.x, village.y, { ignore_units = true }) 245 | cost_ign = cost_ign + move_data.unit_infos[id].max_moves - move_data.unit_infos[id].moves 246 | 247 | local unit_rating = - cost / #zone_villages[zone_id] / move_data.unit_infos[id].max_moves 248 | 249 | --TODO: retreaters are assigned earlier now, but keep code for the time being, just in case 250 | --[[ 251 | -- Scout utility to compare to retreat utility 252 | local int_turns = math.ceil(cost / move_data.unit_infos[id].max_moves) 253 | local int_turns_ign = math.ceil(cost_ign / move_data.unit_infos[id].max_moves) 254 | local scout_utility = math.sqrt(1 / math.max(1, int_turns - 1)) 255 | scout_utility = scout_utility * int_turns_ign / int_turns 256 | --]] 257 | 258 | if scouts[zone_id][id] then 259 | unit_rating = unit_rating + scouts[zone_id][id].rating 260 | end 261 | scouts[zone_id][id] = { rating = unit_rating } 262 | 263 | --if (not scouts[zone_id][id].utility) or (scout_utiltiy > scouts[zone_id][id].utility) then 264 | -- scouts[zone_id][id].utility = scout_utility 265 | --end 266 | end 267 | end 268 | end 269 | end 270 | end 271 | end 272 | --DBG.dbms(scouts, false, 'scouts') 273 | 274 | local sorted_scouts = {} 275 | for zone_id,units in pairs(scouts) do 276 | for id,data in pairs(units) do 277 | --if (data.utility > retreat_utilities[id]) then 278 | if (not sorted_scouts[zone_id]) then 279 | sorted_scouts[zone_id] = {} 280 | end 281 | 282 | table.insert(sorted_scouts[zone_id], { 283 | id = id, 284 | rating = data.rating, 285 | org_rating = data.rating 286 | }) 287 | --else 288 | --std_print('needs to retreat instead:', id) 289 | --end 290 | end 291 | if sorted_scouts[zone_id] then 292 | table.sort(sorted_scouts[zone_id], function(a, b) return a.rating > b.rating end) 293 | end 294 | end 295 | --DBG.dbms(sorted_scouts, false, 'sorted_scouts') 296 | 297 | local keep_trying = true 298 | local zone_id,units = next(sorted_scouts) 299 | if (not zone_id) or (#units == 0) then 300 | keep_trying = false 301 | end 302 | 303 | local scout_assignments = {} 304 | while keep_trying do 305 | keep_trying = false 306 | 307 | -- Set rating relative to the second highest rating in each zone 308 | -- This is 309 | -- 310 | -- Notes: 311 | -- - If only one units is left, we use the original rating 312 | -- - This is unnecessary when only one zone is left, but it works then too, 313 | -- so we'll just keep it rather than adding yet another conditional 314 | for zone_id,units in pairs(sorted_scouts) do 315 | if (#units > 1) then 316 | local second_rating = units[2].rating 317 | for _,scout in pairs(units) do 318 | scout.rating = scout.rating - second_rating 319 | end 320 | else 321 | units[1].rating = units[1].org_rating 322 | end 323 | end 324 | 325 | local max_rating, best_id, best_zone = - math.huge 326 | for zone_id,units in pairs(sorted_scouts) do 327 | local rating = sorted_scouts[zone_id][1].rating 328 | 329 | if (rating > max_rating) then 330 | max_rating = rating 331 | best_id = sorted_scouts[zone_id][1].id 332 | best_zone = zone_id 333 | end 334 | end 335 | --std_print('best:', best_zone, best_id) 336 | 337 | for zone_id,units in pairs(sorted_scouts) do 338 | for i_u,data in ipairs(units) do 339 | if (data.id == best_id) then 340 | table.remove(units, i_u) 341 | break 342 | end 343 | end 344 | end 345 | for zone_id,units in pairs(sorted_scouts) do 346 | if (#units == 0) then 347 | sorted_scouts[zone_id] = nil 348 | end 349 | end 350 | 351 | scout_assignments[best_id] = 'scout:' .. best_zone 352 | 353 | units_assigned_villages[best_zone] = (units_assigned_villages[best_zone] or 0) + 1 354 | 355 | for zone_id,n_needed in pairs(units_needed_villages) do 356 | if (n_needed <= (units_assigned_villages[zone_id] or 0)) then 357 | sorted_scouts[zone_id] = nil 358 | end 359 | end 360 | 361 | -- Check whether we are done 362 | local zone_id,units = next(sorted_scouts) 363 | if zone_id and (#units > 0) then 364 | keep_trying = true 365 | end 366 | end 367 | 368 | return scout_assignments 369 | end 370 | 371 | 372 | return fred_village_utils 373 | -------------------------------------------------------------------------------- /AI-demos/lua/fred_virtual_state.lua: -------------------------------------------------------------------------------- 1 | -- Set up (and reset) hypothetical situations on the map that can be used for 2 | -- analyses of ZoC, counter attacks etc. 3 | 4 | local H = wesnoth.require "helper" 5 | local FGM = wesnoth.require "~/add-ons/AI-demos/lua/fred_gamestate_map.lua" 6 | local FU = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_utils.lua" 7 | local DBG = wesnoth.dofile "~/add-ons/AI-demos/lua/debug.lua" 8 | local COMP = wesnoth.require "~/add-ons/AI-demos/lua/compatibility.lua" 9 | 10 | 11 | -- Two arrays to be made available below via closure 12 | local FVS_stored_units, FVS_ids, FVS_add_units, FVS_extracted_units 13 | 14 | 15 | ----- Begin adjust_move_data_tables() ----- 16 | local function adjust_move_data_tables(old_locs, new_locs, store_units_in_way, move_data, stored_data) 17 | -- Adjust all the move_data tables to the new position, and reset them again later. 18 | -- This is a local function as only the virtual state calculations should have to move units. 19 | -- 20 | -- INPUTS: 21 | -- @old_locs, @new_locs as above 22 | -- @store_units_in_way (boolean): whether to store the locations of the units in the way. Needs to 23 | -- be set to 'true' when moving the units into their new locations, needs to be set to 24 | -- 'false' when moving them back, in which case the stored information will be used. 25 | -- 26 | -- OPTIONAL INPUTS: 27 | -- @stored_data: table for temporary storage of units and ids. This is usually done automatically 28 | -- in the FVS_* variables via closure, but that does not work with nested uses of these functions. 29 | -- 30 | -- Output: 31 | -- Returns nil if no counter attacks were found, otherwise a table with a 32 | -- variety of attack outcomes and ratings 33 | 34 | -- If any of the hexes marked in @new_locs is occupied, we 35 | -- need to store that information as it otherwise will be overwritten. 36 | -- This needs to be done for all locations before any unit is moved 37 | -- in order to avoid conflicts of the units to be moved among themselves 38 | if store_units_in_way then 39 | for i_l,old_loc in ipairs(old_locs) do 40 | local x1, y1 = old_loc[1], old_loc[2] 41 | local x2, y2 = new_locs[i_l][1], new_locs[i_l][2] 42 | 43 | -- Store the ids of the units to be put onto the map. 44 | -- This includes units with MP that attack from their current position, 45 | -- but not units without MP (as the latter are already on the map) 46 | -- Note: this array might have missing elements -> needs to be iterated using pairs() 47 | local id = FGM.get_value(move_data.my_unit_map_MP, x1, y1, 'id') 48 | if id then 49 | if stored_data then 50 | stored_data.FVS_ids[i_l] = id 51 | else 52 | FVS_ids[i_l] = id 53 | end 54 | end 55 | 56 | -- By contrast, we only need to store the information about units in the way, 57 | -- if a unit actually gets moved to the hex (independent of whether it has MP left or not) 58 | if (x1 ~= x2) or (y1 ~= y2) then 59 | -- If there is another unit at the new location, store it 60 | -- It does not matter for this whether this is a unit involved in the move or not 61 | local id2 = FGM.get_value(move_data.my_unit_map, x2, y2, 'id') 62 | if id2 then 63 | if stored_data then 64 | stored_data.FVS_stored_units[id2] = { x2, y2 } 65 | else 66 | FVS_stored_units[id2] = { x2, y2 } 67 | end 68 | end 69 | end 70 | end 71 | end 72 | 73 | -- Now adjust all the move_data tables 74 | for i_l,old_loc in ipairs(old_locs) do 75 | local x1, y1 = old_loc[1], old_loc[2] 76 | local x2, y2 = new_locs[i_l][1], new_locs[i_l][2] 77 | --std_print('Moving unit:', x1, y1, '-->', x2, y2) 78 | 79 | -- We only need to do this if the unit actually gets moved 80 | if (x1 ~= x2) or (y1 ~= y2) then 81 | local id = stored_data and stored_data.FVS_ids[i_l] or FVS_ids[i_l] 82 | 83 | -- Likely, not all of these tables have to be changed, but it takes 84 | -- very little time, so better safe than sorry 85 | move_data.unit_copies[id].x, move_data.unit_copies[id].y = x2, y2 86 | 87 | move_data.units[id] = { x2, y2 } 88 | move_data.my_units[id] = { x2, y2 } 89 | move_data.my_units_MP[id] = { x2, y2 } 90 | 91 | if move_data.unit_infos[id].canrecruit then 92 | end 93 | 94 | -- Note that the following might leave empty orphan table elements, but that doesn't matter 95 | move_data.my_unit_map[x1][y1] = nil 96 | FGM.set_value(move_data.my_unit_map, x2, y2, 'id', id) 97 | move_data.my_unit_map_MP[x1][y1] = nil 98 | FGM.set_value(move_data.my_unit_map_MP, x2, y2, 'id', id) 99 | end 100 | end 101 | 102 | -- Finally, if 'store_units_in_way' is not set (this is, when moving units back 103 | -- into place), restore the stored units into the maps again 104 | if (not store_units_in_way) then 105 | for id,loc in pairs(stored_data and stored_data.FVS_stored_units or FVS_stored_units) do 106 | FGM.set_value(move_data.my_unit_map, loc[1], loc[2], 'id', id) 107 | FGM.set_value(move_data.my_unit_map_MP, loc[1], loc[2], 'id', id) 108 | end 109 | end 110 | end 111 | ----- End adjust_move_data_tables() ----- 112 | 113 | 114 | local fred_virtual_state = {} 115 | 116 | function fred_virtual_state.set_virtual_state(old_locs, new_locs, additional_units, do_extract_units, move_data, stored_data) 117 | 118 | if stored_data then 119 | stored_data.FVS_stored_units, stored_data.FVS_ids, stored_data.FVS_add_units, stored_data.extracted_units = {}, {}, {}, {} 120 | else 121 | FVS_stored_units, FVS_ids, FVS_add_units, FVS_extracted_units = {}, {}, {}, {} 122 | end 123 | 124 | if do_extract_units then 125 | for id,loc in pairs(move_data.my_units_MP) do 126 | local unit_proxy = COMP.get_unit(loc[1], loc[2]) 127 | COMP.extract_unit(unit_proxy) 128 | table.insert(stored_data and stored_data.extracted_units or FVS_extracted_units, unit_proxy) -- Not a proxy unit any more at this point 129 | end 130 | end 131 | 132 | -- Mark the new positions of the units in the move_data tables 133 | adjust_move_data_tables(old_locs, new_locs, true, move_data, stored_data) 134 | 135 | -- Put all units in old_locs with MP onto the map (those without are already there) 136 | -- They need to be proxy units for the counter attack calculation. 137 | for _,id in pairs(stored_data and stored_data.FVS_ids or FVS_ids) do 138 | COMP.put_unit(move_data.unit_copies[id]) 139 | end 140 | 141 | -- Also put the additonal units out there 142 | if additional_units then 143 | for _,add_unit in ipairs(additional_units) do 144 | local place_unit = true 145 | for _,new_loc in ipairs(new_locs) do 146 | if (add_unit[1] == new_loc[1]) and (add_unit[2] == new_loc[2]) then 147 | place_unit = false 148 | break 149 | end 150 | end 151 | if place_unit then 152 | if stored_data then 153 | table.insert(stored_data.FVS_add_units, add_unit) 154 | else 155 | table.insert(FVS_add_units, add_unit) 156 | end 157 | 158 | COMP.put_unit({ 159 | type = add_unit.type, 160 | random_traits = false, 161 | name = "X", 162 | random_gender = false, 163 | moves = 0 164 | }, 165 | add_unit[1], add_unit[2] 166 | ) 167 | end 168 | end 169 | end 170 | end 171 | 172 | function fred_virtual_state.reset_state(old_locs, new_locs, do_extract_units, move_data, stored_data) 173 | -- Extract the units from the map 174 | for _,add_unit in ipairs(stored_data and stored_data.FVS_add_units or FVS_add_units) do 175 | COMP.erase_unit(add_unit[1], add_unit[2]) 176 | end 177 | for _,id in pairs(stored_data and stored_data.FVS_ids or FVS_ids) do 178 | COMP.extract_unit(move_data.unit_copies[id]) 179 | end 180 | 181 | -- And put them back into their original locations 182 | adjust_move_data_tables(new_locs, old_locs, false, move_data, stored_data) 183 | 184 | -- Put the extracted units back on the map 185 | if do_extract_units then 186 | for _,extracted_unit in ipairs(stored_data and stored_data.extracted_units or FVS_extracted_units) do 187 | COMP.put_unit(extracted_unit) 188 | end 189 | end 190 | 191 | if stored_data then 192 | stored_data.FVS_stored_units, stored_data.FVS_ids, stored_data.FVS_add_units, stored_data.extracted_units = nil, nil, nil, nil 193 | else 194 | FVS_stored_units, FVS_ids, FVS_add_units, FVS_extracted_units = nil, nil, nil, nil 195 | end 196 | end 197 | 198 | function fred_virtual_state.virtual_reach_maps(units, to_enemy_locs, to_locs, move_data) 199 | -- Find new reach_maps of @units in a (virtual) situation that has been set up on the map 200 | -- This is only done for units which could previously attack @to_enemies or move onto @to_hexes. 201 | -- The point of this is to test if placing AI units in certain positions changes where 202 | -- enemies can attack or reach. 203 | -- Note that this means that either @to_enemy_locs or @to_locs must be given, 204 | -- otherwise this function will always return empty reachmaps. 205 | -- 206 | -- TODO: this could (should?) eventually also include placing the units in the new positions 207 | 208 | reach_maps = {} 209 | for unit_id,_ in pairs(units) do 210 | reach_maps[unit_id] = {} 211 | 212 | -- Only calculate reach if the unit could get there using its 213 | -- original reach. It cannot have gotten any better than this. 214 | local can_reach = false 215 | if to_enemy_locs then 216 | for _,enemy_loc in ipairs(to_enemy_locs) do 217 | for xa,ya in H.adjacent_tiles(enemy_loc[1], enemy_loc[2]) do 218 | if FGM.get_value(move_data.reach_maps[unit_id], xa, ya, 'moves_left') then 219 | can_reach = true 220 | break 221 | end 222 | end 223 | if can_reach then break end 224 | end 225 | end 226 | if (not can_reach) and to_locs then 227 | for _,loc in ipairs(to_locs) do 228 | if FGM.get_value(move_data.reach_maps[unit_id], loc[1], loc[2], 'moves_left') then 229 | can_reach = true 230 | break 231 | end 232 | end 233 | end 234 | 235 | if can_reach then 236 | -- For sides other than the current, we always use max_moves. 237 | -- For the current side, we always use current moves. 238 | local old_moves 239 | if (move_data.unit_infos[unit_id].side ~= wesnoth.current.side) then 240 | old_moves = move_data.unit_copies[unit_id].moves 241 | move_data.unit_copies[unit_id].moves = move_data.unit_copies[unit_id].max_moves 242 | end 243 | 244 | local reach = wesnoth.find_reach(move_data.unit_copies[unit_id]) 245 | 246 | for _,r in ipairs(reach) do 247 | FGM.set_value(reach_maps[unit_id], r[1], r[2], 'moves_left', r[3]) 248 | end 249 | 250 | if (move_data.unit_infos[unit_id].side ~= wesnoth.current.side) then 251 | move_data.unit_copies[unit_id].moves = old_moves 252 | end 253 | end 254 | end 255 | 256 | -- Eliminate hexes with other units that cannot move out of the way 257 | for id,reach_map in pairs(reach_maps) do 258 | for id_noMP,loc in pairs(move_data.my_units_noMP) do 259 | if (id ~= id_noMP) then 260 | if reach_map[loc[1]] then reach_map[loc[1]][loc[2]] = nil end 261 | end 262 | end 263 | end 264 | 265 | return reach_maps 266 | end 267 | 268 | return fred_virtual_state 269 | -------------------------------------------------------------------------------- /AI-demos/lua/manual_input.lua: -------------------------------------------------------------------------------- 1 | -- Edit the information below to make the AI move the unit 2 | return 'challenger', 19, 21 3 | -------------------------------------------------------------------------------- /AI-demos/lua/test_lua_template.lua: -------------------------------------------------------------------------------- 1 | -- This is the manual 'AI handler' 2 | -- Want this in a separate file, so that it can be changed on the fly 3 | -- It loads the AI from another file which can then be executed by right-click menu 4 | 5 | -- Start test scenario from command line with 6 | -- /Applications/Wesnoth-1.11.app/Contents/MacOS/Wesnoth -d -l AI-demos-test.gz 7 | 8 | -- Include all these, just in case (mostly not needed) 9 | local H = wesnoth.require "helper" 10 | local AH = wesnoth.require "ai/lua/ai_helper.lua" 11 | local MAIH = wesnoth.require "ai/micro_ais/micro_ai_helper.lua" 12 | local MAIUV = wesnoth.require "ai/micro_ais/micro_ai_unit_variables.lua" 13 | local MAISD = wesnoth.require "ai/micro_ais/micro_ai_self_data.lua" 14 | local BC = wesnoth.require "ai/lua/battle_calcs.lua" 15 | local FGU = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_gamestate_utils.lua" 16 | local FGUI = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_gamestate_utils_incremental.lua" 17 | local FU = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_utils.lua" 18 | local FAU = wesnoth.dofile "~/add-ons/AI-demos/lua/fred_attack_utils.lua" 19 | local LS = wesnoth.require "location_set" 20 | local DBG = wesnoth.dofile "~/add-ons/AI-demos/lua/debug.lua" 21 | 22 | -- Clean up the screen 23 | wesnoth.clear_messages() 24 | AH.clear_labels() 25 | std_print('\n---- Side ', wesnoth.current.side, '------------') 26 | 27 | -- Check for debug mode and quit if it is not activated 28 | if (not wesnoth.game_config.debug) then 29 | wesnoth.message("***** This option requires debug mode. Activate by typing ':debug' *****") 30 | return 31 | end 32 | 33 | -- Add shortcut to debug ai table 34 | local ai = wesnoth.debug_ai(wesnoth.current.side).ai 35 | --DBG.dbms(ai, false, 'ai') 36 | 37 | local fn_fred = "~add-ons/AI-demos/lua/fred.lua" 38 | local fn = "ai/micro_ais/cas/ca_fast_move.lua" 39 | local self = { data = {} } 40 | local cfg = {} 41 | 42 | ----------------------------------------------------------------- 43 | 44 | local test_CA, exec_also, exec_loop = true, false, false 45 | 46 | if test_CA then -- Test a specific CA ... 47 | --local cfg = { ca_score = 300000 } 48 | local cfg = { ca_score = 300000, target_id='Bad Orc', { "filter", { type = 'Orcish Grunt' } } } 49 | 50 | eval = 1 51 | 52 | if (wesnoth.current.side == 1) then 53 | while (eval > 0) do 54 | local start_time = wesnoth.get_time_stamp() / 1000. 55 | wesnoth.message('Start time:', start_time) 56 | eval = wesnoth.dofile(fn):evaluation(ai, cfg, self) 57 | wesnoth.message('Eval score:', eval) 58 | wesnoth.message('Time after eval:', wesnoth.get_time_stamp() / 1000., wesnoth.get_time_stamp() / 1000. - start_time) 59 | if (eval > 0) and (exec_also or exec_loop) then 60 | wesnoth.dofile(fn):execution(ai, cfg, self) 61 | wesnoth.message('Time after exec:', wesnoth.get_time_stamp() / 1000., wesnoth.get_time_stamp() / 1000. - start_time) 62 | end 63 | 64 | -- Only do another iteration if ca_exec_loop is set. 65 | -- The loop is also broken if evaluation resulted in a score of 0. 66 | if (not exec_loop) then 67 | eval = 0 68 | end 69 | end 70 | else 71 | wesnoth.message("This only works when you're in control of Side 1") 72 | end 73 | 74 | else -- ... or do manual testing 75 | -- Set up Fred's variables 76 | local move_data = FGU.get_move_data() 77 | local move_cache = {} 78 | --for k,_ in pairs(move_data) do 79 | -- std_print(k) 80 | --end 81 | 82 | 83 | local leader = wesnoth.get_units { side = 1, canrecruit = 'yes' }[1] 84 | local units = wesnoth.get_units { side = 1, canrecruit = 'no' } 85 | local enemies = wesnoth.get_units { side = 2, canrecruit = 'no' } 86 | std_print('#units, #enemies', #units, #enemies) 87 | local unit = units[1] 88 | local enemy = enemies[1] 89 | 90 | local start_time = wesnoth.get_time_stamp() 91 | wesnoth.message('Start time:', start_time) 92 | std_print('Start time:', start_time) 93 | 94 | ------- Begin: Do something with the units here ------- 95 | 96 | 97 | 98 | local end_time = wesnoth.get_time_stamp() 99 | wesnoth.message('Finish time:', end_time .. ' ' .. tostring(end_time - start_time)) 100 | std_print('Finish time:', end_time .. ' ' .. tostring(end_time - start_time)) 101 | end 102 | -------------------------------------------------------------------------------- /AI-demos/maps/Dark_Forecast.map: -------------------------------------------------------------------------------- 1 | border_size=1 2 | usage=map 3 | 4 | _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr 5 | _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr 6 | _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr 7 | _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr 8 | _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr 9 | _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr 10 | _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , Ai , _off^_usr 11 | _off^_usr , Xv , Xv , Xv , Xv , Xv , Xv , Xv , Xv , Xv , Xv , Xv , Xv , Xv , Xv , Xv , Xv , _off^_usr , Ai , _off^_usr , Ai , _off^_usr 12 | _off^_usr , Ww^Vm , Ww , Gs^Fp , Mm , Hh , Gs^Fp , Re , Gs^Fp , Ss , Ss , Ss , Ss , Ds , Ds , Ss , Mm^Xm , Xu , Xu , Ai , Ai , _off^_usr 13 | _off^_usr , Ww , Ww , Mm , Mm , Mm , Re , Re , Gg , Ss^Vhs , Gs^Fp , Gs^Fp , Ds , Ds , Re^Uf , Ss , Mm^Xm , Xu , Cud , Xu , Ai , _off^_usr 14 | _off^_usr , Mm , Gs^Fp , Ww , Ww , Gg , Gg , Re , Gg , Hh , Gs^Fp , Hh , Gg , Gg^Vh , Gg , Re^Uf , Mm^Xm , Xu , Cud , Xu , Ai , _off^_usr 15 | _off^_usr , Hh , Gs , Gs^Ft , Gs , Wwf , Wwf , Gg , Gg , Hh , Gg , Gg , Gg , Gg , Re , Re , Mm^Xm , Xu , Cud , Xu , Ai , _off^_usr 16 | _off^_usr , Gs^Vht , Re , Gs , Gs^Ft , Gs , Ss^Vhs , Ds , Hh , Gg , Gg , Gg , Re , Re , Ch , Ch , Mm^Xm , Xu , Cud , Xu , Ai , _off^_usr 17 | _off^_usr , Re , Re , Re , Gs^Ft , Ds , Ss , Wwf , Gg , Gg , Re , Re , Ch , Ch , Re^Gvs , Ch , Mm^Xm , Xu , Cud , Xu , Ai , _off^_usr 18 | _off^_usr , Gg , Hh , Re , Re , Hh , Gs^Fp , Wwf , Gg , Gg , Re , Ch , Gg^Efm , Re^Gvs , Gg , 4 Kh , Mm^Xm , Xu , Cud , Xu , Ai , _off^_usr 19 | _off^_usr , Gg , Gg^Vh , Re^Uf , Re , Gg , Gg , Gg , Ww , Gg , Re , Gg^Ve , Re^Gvs , Gg , Re^Gvs , Gg^Vh , Mm^Xm , Xu , Cud , Xu , Ai , _off^_usr 20 | _off^_usr , Gg , Gs^Fp , Hh , Re , Re , Gg , Mm , Ww , Gg , Re , Ch , Ch , Re^Gvs , Gg^Efm , 3 Kh , Mm^Xm , Xu , Cud , Xu , Ai , _off^_usr 21 | _off^_usr , Gg , Gg , Gg , Mm , Re , Gg , Ds , Ww , Gs^Fp , Gg , Re , Re , Ch , Ch , Ch , Mm^Xm , Xu , Cud , Xu , Ai , _off^_usr 22 | _off^_usr , Gg , Gg , Gs^Fp , Hh , Re , Gg^Ve , Gs^Fp , Mm , Ww , Gg , Gg , Gs^Fp , Re , Re , Ch , Mm^Xm , Xu , Cud , Xu , Ai , _off^_usr 23 | _off^_usr , Gg , Gs^Ft , Gg , Gg , Re , Re , Gg , Gs^Fp , Ww , Gg , Gs^Fp , Gg , Gg , Gg , Re , Mm^Xm , Xu , Cud , Xu , Ai , _off^_usr 24 | _off^_usr , Gs^Ft , Hd , Dd , Dd , Gg , Gg , Re , Ss^Bw\ , Ss , Gg , Gg , Hh , Gg , Gg , Hh , Mm^Xm , Xu , Cud , Xu , Ai , _off^_usr 25 | _off^_usr , Dd , Md , Dd^Do , Dd , Dd , Dd , Gs^Ft , Ss , Re , Gg , Mm , Ww , Wwf , Gg , Hh , Mm^Xm , Xu , Xu , Xu , Ai , _off^_usr 26 | _off^_usr , Md , Dd^Dc , Hd , Dd , Dd^Vda , Gs^Ft , Hh , Ss , Re , Re , Mm , Ww , Gg^Ve , Gs^Fp , Gg^Fet , Mm^Xm , Xu , Ai , Ai , Ai , _off^_usr 27 | _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr , _off^_usr 28 | -------------------------------------------------------------------------------- /AI-demos/maps/fai_demo.map: -------------------------------------------------------------------------------- 1 | border_size=1 2 | usage=map 3 | 4 | Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gs^Fds , Gg , Gs^Fds , Gg , Gg 5 | Gg , Ch , Kh , Ch , Gg^Ve , Gg , Gs^Fds , Gg , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds 6 | Gg , Ch , Ch , Ch , Gg , Gg , Gg , Gs^Fds , Gs^Fds , Gs^Fds , Gg^Ve , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds 7 | Gs^Fds , Gg^Ve , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gg 8 | Gs^Fds , Gs^Fds , Gs^Fds , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg^Ve , Gs^Fds , Gg , Gg , Gg , Gg , Gg , Gg , Gg 9 | Gs^Fds , Gs^Fds , Gg^Ve , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg 10 | Gs^Fds , Gs^Fds , Gs^Fds , Gg , Gg , Gg , Gg , Gg , Gg^Ve , Gg , Gg , Gg , Gg , Gg , Gg , Gg^Ve , Gg , Gg , Gg , Gg , Gs^Fds 11 | Ms , Ms , Ms , Ms , Ms , Gs^Fds , Gs^Fds , Gg , Gg , Gg , Gg , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Ms , Gs^Fds , Ms , Ms , Ms 12 | Ms , Ms , Gs^Fds , Ms , Gs^Fds , Ms , Gs^Fds , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gs^Fds , Gs^Fds , Ms , Gs^Fds , Ms , Ms 13 | Gg , Gs^Fds , Gg , Gg , Gg^Ve , Gs^Fds , Gg , Gg , Wo , Gg , Wo , Gg , Gg , Gg , Gg , Gg , Gg , Gs^Fds , Gs^Fds , Ms , Gs^Fds 14 | Gg , Gs^Fds , Gg^Ve , Gg , Gg , Gg , Gg , Wo , Wo , Wo , Wot , Wo , Gg , Gg , Gg , Gg , Gg , Gg^Ve , Gg , Gg , Gg 15 | Gg , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gg , Wo , Wot , Wot , Wot , Wo , Wo , Gg , Gg , Gg , Gg , Gs^Fds , Gg , Gg , Gg 16 | Gg , Gg , Gs^Fds , Gs^Fds , Gg , Gs^Fds , Gg , Wo , Wo , Wot , Wot , Wot , Wo , Gg^Ve , Gg , Gg , Gs^Fds , Gs^Fds , Gs^Fds , Gg , Gg 17 | Gg , Gs^Fds , Gs^Fds , Gg , Gs^Fds , Gg , Gg , Gs^Fds , Gg , Wo , Wo , Wo , Gg , Gg , Gg , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gg , Gg 18 | Gg , Gs^Fds , Gg , Gs^Fds , Gg , Gs^Fds , Gg , Gg^Ve , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gs^Fds , Gs^Fds , Gs^Fds , Gg , Gg , Gg 19 | Gg , Gs^Fds , Gg , Gs^Fds , Gg , Gg , Gg , Gg , Gg , Gg , Gg^Ve , Gg , Gg , Gg , Gg , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Gg^Ve , Gg 20 | Gg , Gg , Gs^Fds , Gs^Fds , Gs^Fds , Gs^Fds , Ms , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gs^Fds , Gg , Gg , Gg 21 | Gg , Gg , Gs^Fds , Gg , Gg , Ms , Ms , Ms , Ms , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Cea , Gg , Gg 22 | Gg , Gg , Gg , Gg , Ms , Ms , Ms , Ms , Ms , Gg , Gg , Gg , Gg^Ve , Gg , Gg , Gg , Gg , Cea , Ke , Cea , Gg 23 | Gg , Gg , Gg , Ms , Ms , Ms , Ms , Ms , Ms , Ms , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Cea , Gg , Cea , Gg 24 | Gg , Gg , Gg , Gg , Gg , Ms , Gg , Ms , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg , Gg 25 | -------------------------------------------------------------------------------- /AI-demos/multiplayer/_initial.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | 3 | #define DEMO_MULTIPLAYER_AIS 4 | {~add-ons/AI-demos/ais/ai_fred.cfg} 5 | 6 | [options] 7 | [slider] 8 | id=fred_aggression_multiplier 9 | name=_"Aggression multiplier (x10)" 10 | description=_"Set Fred's overall aggression level relative to default (default: 10)" 11 | default=10 12 | min=1 13 | max=20 14 | [/slider] 15 | [/options] 16 | 17 | [event] 18 | # This needs to be a preload event so that CA debugging mode works with replays 19 | name=preload 20 | 21 | [lua] 22 | code = << 23 | local FSS = wesnoth.require "~/add-ons/AI-demos/lua/fred_scenario_setup.lua" 24 | FSS.fred_scenario_setup() 25 | >> 26 | [/lua] 27 | [/event] 28 | #enddef 29 | -------------------------------------------------------------------------------- /AI-demos/multiplayer/era.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | 3 | [era] 4 | id=AI_Demos 5 | name= _ "AI-demos Experimental AIs" 6 | 7 | {RANDOM_SIDE} 8 | {multiplayer/factions/loyalists-default.cfg} 9 | {multiplayer/factions/rebels-default.cfg} 10 | {multiplayer/factions/northerners-default.cfg} 11 | {multiplayer/factions/undead-default.cfg} 12 | {multiplayer/factions/knalgans-default.cfg} 13 | {multiplayer/factions/drakes-default.cfg} 14 | {QUICK_4MP_LEADERS} 15 | 16 | {DEMO_MULTIPLAYER_AIS} 17 | [/era] 18 | -------------------------------------------------------------------------------- /AI-demos/multiplayer/modification.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | 3 | [modification] 4 | id=AI_Demos 5 | name= _ "AI-demos Experimental AIs" 6 | description= _ "Some experimental AIs" 7 | 8 | disallow_era="AI_Demos" 9 | 10 | {DEMO_MULTIPLAYER_AIS} 11 | [/modification] 12 | -------------------------------------------------------------------------------- /AI-demos/scenarios/Fred.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | 3 | #ifdef TEST 4 | [test] 5 | #else 6 | # wmlindent: start ignoring 7 | [scenario] 8 | # wmlindent: stop ignoring 9 | #endif 10 | id=fred 11 | name=_"Fred" 12 | next_scenario=aid_switchboard 13 | 14 | map_data="{multiplayer/maps/2p_The_Freelands.map}" 15 | 16 | {DEFAULT_SCHEDULE} 17 | random_start_time=yes 18 | turns=50 19 | victory_when_enemies_defeated=yes 20 | 21 | [side] 22 | side=1 23 | controller=ai 24 | id=Fred1 25 | name=Fred 26 | type=Orcish Warrior 27 | persistent=no 28 | facing=sw 29 | 30 | team_name=Fred 31 | user_team_name=_"Fred" 32 | 33 | recruit=Goblin Spearman, Naga Fighter, Orcish Archer, Orcish Assassin, Orcish Grunt, Troll Whelp, Wolf Rider 34 | gold=100 35 | village_gold=2 36 | 37 | {~add-ons/AI-demos/ais/ai_fred.cfg} 38 | [/side] 39 | 40 | [side] 41 | side=2 42 | controller=ai 43 | type=Invisible Unit 44 | id=challenger 45 | name=_"Fred's Challenger" 46 | persistent=no 47 | 48 | team_name=challenger 49 | user_team_name=_"Fred's Challenger" 50 | save_id="Fred's Challenger" 51 | 52 | gold=0 53 | village_gold=2 54 | [village] 55 | x=26 56 | y=22 57 | [/village] 58 | 59 | {~add-ons/AI-demos/ais/ai_fred.cfg} 60 | [/side] 61 | 62 | [event] 63 | # This needs to be a preload event so that CA debugging mode works with replays 64 | name=preload 65 | [lua] 66 | code = << 67 | local FSS = wesnoth.require "~/add-ons/AI-demos/lua/fred_scenario_setup.lua" 68 | FSS.fred_scenario_setup() 69 | >> 70 | [/lua] 71 | [/event] 72 | 73 | [event] 74 | name=prestart 75 | 76 | {VARIABLE scenario_name fred} 77 | 78 | [set_variable] 79 | name=fred_type 80 | rand=Orcish Warrior,Troll,Troll Rocklobber,Orcish Crossbowman,Orcish Slayer 81 | [/set_variable] 82 | 83 | [kill] 84 | id=Fred1 85 | [/kill] 86 | 87 | [unit] 88 | side=1 89 | id=Fred1 90 | name=Fred Side 1 91 | type=$fred_type 92 | canrecruit=yes 93 | 94 | x,y=19,4 95 | facing=sw 96 | [/unit] 97 | 98 | {CLEAR_VARIABLE fred_type} 99 | [/event] 100 | 101 | # Put this into a new turn as the AI_Demos_version variable is not set until the start event 102 | [event] 103 | name=new turn 104 | 105 | [objectives] 106 | [objective] 107 | description=_"Death of Fred" 108 | condition=win 109 | [/objective] 110 | [objective] 111 | description=_"Death of your leader" 112 | condition=lose 113 | [/objective] 114 | {TURNS_RUN_OUT} 115 | [note] 116 | description=_"This is Fred v$AI_Demos_version" # wmllint: no spellcheck 117 | [/note] 118 | [note] 119 | description=_"This scenario can be started directly from the commandline using '-tfred' or by using the test scenario hotkey in the title screen." # wmllint: no spellcheck 120 | [/note] 121 | [/objectives] 122 | 123 | [message] 124 | message=_"Which faction should Fred's challenger (side 2) play? Fred (side 1) always plays Northerners in this scenario." 125 | speaker=narrator 126 | image=wesnoth-icon.png 127 | 128 | [option] 129 | label=_"Northerners" 130 | [command] 131 | {VARIABLE ai_leader (Orcish Warrior,Troll,Troll Rocklobber,Orcish Crossbowman,Orcish Slayer)} 132 | {VARIABLE recruit_types (Orcish Grunt,Troll Whelp,Wolf Rider,Orcish Archer,Orcish Assassin,Naga Fighter,Goblin Spearman)} 133 | [/command] 134 | [/option] 135 | 136 | [option] 137 | label=_"Rebels" 138 | [command] 139 | {VARIABLE ai_leader (Elvish Captain,Elvish Hero,Elvish Ranger,Elvish Marksman,Elvish Druid,Elvish Sorceress,White Mage,Red Mage)} 140 | {VARIABLE recruit_types (Elvish Fighter,Elvish Archer,Mage,Elvish Shaman,Elvish Scout,Wose,Merman Hunter)} 141 | [/command] 142 | [/option] 143 | 144 | [option] 145 | label=_"Undead" 146 | [command] 147 | {VARIABLE ai_leader (Dark Sorcerer,Revenant,Deathblade,Bone Shooter)} 148 | {VARIABLE recruit_types (Skeleton,Skeleton Archer,Walking Corpse,Ghost,Vampire Bat,Dark Adept,Ghoul)} 149 | [/command] 150 | [/option] 151 | 152 | [option] 153 | label=_"Knalgan Alliance" 154 | [command] 155 | {VARIABLE ai_leader (Dwarvish Steelclad,Dwarvish Thunderguard,Dwarvish Stalwart,Rogue,Trapper)} 156 | {VARIABLE recruit_types (Dwarvish Guardsman,Dwarvish Fighter,Dwarvish Ulfserker,Dwarvish Thunderer,Thief,Poacher,Footpad,Gryphon Rider)} 157 | [/command] 158 | [/option] 159 | 160 | [option] 161 | label=_"Drakes" 162 | [command] 163 | {VARIABLE ai_leader (Drake Flare,Fire Drake,Drake Arbiter,Drake Thrasher,Drake Warrior)} 164 | {VARIABLE recruit_types (Drake Burner,Drake Clasher,Drake Glider,Drake Fighter,Saurian Skirmisher,Saurian Augur)} 165 | [/command] 166 | [/option] 167 | 168 | [option] 169 | label=_"Loyalists" 170 | [command] 171 | {VARIABLE ai_leader (Lieutenant,Swordsman,Pikeman,Javelineer,Longbowman,White Mage,Red Mage)} 172 | {VARIABLE recruit_types (Cavalryman,Horseman,Spearman,Fencer,Heavy Infantryman,Bowman,Mage,Merman Fighter)} 173 | [/command] 174 | [/option] 175 | [/message] 176 | 177 | [set_variable] 178 | name=leader_type 179 | rand=$ai_leader 180 | [/set_variable] 181 | 182 | [kill] 183 | side=2 184 | canrecruit=yes 185 | [/kill] 186 | 187 | [unit] 188 | side=2 189 | type=$leader_type 190 | canrecruit=yes 191 | 192 | x,y=19,21 193 | [/unit] 194 | 195 | [allow_recruit] 196 | side=2 197 | type=$recruit_types 198 | [/allow_recruit] 199 | 200 | {CLEAR_VARIABLE ai_leader,recruit_types,leader_type} 201 | 202 | [message] 203 | message=_"Who should play Fred's challenger?" 204 | speaker=narrator 205 | image=wesnoth-icon.png 206 | 207 | [option] 208 | label=_"Let the Fred AI also play side 2." 209 | [command] 210 | [modify_unit] 211 | [filter] 212 | side=2 213 | canrecruit=yes 214 | [/filter] 215 | id=Fred2 216 | name=Fred Side 2 217 | [/modify_unit] 218 | [/command] 219 | [/option] 220 | 221 | [option] 222 | label=_"I want to do it." 223 | [command] 224 | [modify_side] 225 | side=2 226 | controller=human 227 | [/modify_side] 228 | [/command] 229 | [/option] 230 | [/message] 231 | 232 | [message] 233 | message=_"How much gold should Fred's challenger (side 2) have? Fred (side 1) always has 100." 234 | speaker=narrator 235 | image=wesnoth-icon.png 236 | 237 | [option] 238 | label=_"100" 239 | [command] 240 | [gold] 241 | side=2 242 | amount=100 243 | [/gold] 244 | [/command] 245 | [/option] 246 | 247 | [option] 248 | label=_"125" 249 | [command] 250 | [gold] 251 | side=2 252 | amount=125 253 | [/gold] 254 | [/command] 255 | [/option] 256 | 257 | [option] 258 | label=_"150" 259 | [command] 260 | [gold] 261 | side=2 262 | amount=150 263 | [/gold] 264 | [/command] 265 | [/option] 266 | 267 | [option] 268 | label=_"175" 269 | [command] 270 | [gold] 271 | side=2 272 | amount=175 273 | [/gold] 274 | [/command] 275 | [/option] 276 | 277 | [option] 278 | label=_"200" 279 | [command] 280 | [gold] 281 | side=2 282 | amount=200 283 | [/gold] 284 | [/command] 285 | [/option] 286 | 287 | [option] 288 | label=_"75" 289 | [command] 290 | [gold] 291 | side=2 292 | amount=75 293 | [/gold] 294 | [/command] 295 | [/option] 296 | [/message] 297 | [/event] 298 | #ifndef TEST 299 | [/scenario] 300 | #else 301 | # wmlindent: start ignoring 302 | [/test] 303 | # wmlindent: stop ignoring 304 | #endif 305 | -------------------------------------------------------------------------------- /AI-demos/scenarios/fai_demo.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | 3 | #wmllint: local spelling Wehrli Johan Rinaldini Julien Zerg Starcraft 4 | 5 | [scenario] 6 | id="fai_demo" 7 | name=_"Formula AI Demo" 8 | next_scenario= aid_switchboard 9 | 10 | map_data="{~add-ons/AI-demos/maps/fai_demo.map}" 11 | 12 | {DEFAULT_SCHEDULE} 13 | turns=-1 14 | victory_when_enemies_defeated=yes 15 | 16 | [side] 17 | side=1 18 | x,y=2,1 19 | type=Elvish Avenger 20 | id=elf1 21 | canrecruit=yes 22 | persistent=no 23 | 24 | user_team_name=_"Formula AI" 25 | recruit=Elvish Archer,Elvish Fighter,Elvish Scout,Elvish Shaman,Mage 26 | 27 | gold=200 28 | 29 | [ai] 30 | formula=" 31 | if(turn = 1, 32 | [ recruit('Elvish Scout'), 33 | recruit('Elvish Scout'), 34 | recruit('Elvish Scout') ] 35 | ,[]) 36 | " 37 | [/ai] 38 | 39 | [ai] 40 | version=10710 41 | [stage] 42 | engine=fai 43 | name=unit_formulas 44 | [/stage] 45 | [stage] 46 | engine=fai 47 | name=side_formulas 48 | move=" 49 | move(units[0].loc,nearest_loc(units[0].loc,self.enemy_and_unowned_villages)) 50 | where units=filter(my_units,(self.type='Elvish Scout') and (self.movement_left > 0)) 51 | " 52 | [/stage] 53 | [stage] 54 | engine=fai 55 | name=side_formulas 56 | move=" 57 | attack(units[0].loc, head(shortest_path(nearest_loc(units[0].loc,map(enemy_units,self.loc)), units[0].loc)), nearest_loc(units[0].loc,map(enemy_units,self.loc))) 58 | where units=filter(my_units, (self.leader!=1) and (self.movement_left > 0)) 59 | " 60 | [/stage] 61 | [stage] 62 | engine=fai 63 | name=side_formulas 64 | move=" 65 | if(size(units) < 2 and size(filter(my_units,type='Elvish Archer' or type='Elvish Fighter')) != 0 and my_side.gold >= 18, 66 | recruit('Elvish Scout'), 67 | [if(my_side.gold >= 17,recruit('Elvish Archer'),my_side.gold >= 14,recruit('Elvish Fighter'), 68 | [])] 69 | ) 70 | where units=filter(my_units,type='Elvish Scout') 71 | " 72 | [/stage] 73 | [/ai] 74 | [/side] 75 | 76 | [side] 77 | side=2 78 | controller=ai 79 | type=Orcish Warrior 80 | x,y=18,18 81 | persistent=no 82 | 83 | user_team_name=_"Default AI" 84 | 85 | recruit=Goblin Spearman,Orcish Archer,Orcish Assassin,Orcish Grunt,Wolf Rider 86 | gold=100 87 | [/side] 88 | 89 | [side] # This side is only here because we need one persistent side for the game to go on 90 | side=3 91 | controller=null 92 | persistent=yes 93 | save_id=Grnk 94 | type=Goblin Spearman 95 | hidden=yes 96 | [/side] 97 | 98 | [event] 99 | name=start 100 | 101 | {VARIABLE scenario_name fai_demo} 102 | 103 | # wmllint: display on 104 | [message] 105 | id=elf1 106 | message=_"This scenario demonstrates a simple but complete AI written entirely in Formula AI by Wehrli Johan and Rinaldini Julien. Its point is to defeat the enemy leader. Until that is done, it has the same behavior as the 'Zerg rush' (Starcraft). The AI spawns an Elvish Archer and Elvish Fighter on every turn (if it has the gold), or an Elvish Scout if there are less than 2 Elvish Scouts. The Elvish Scouts' goal is to capture the maximum number of villages. If there are no more unowned villages, the scouts support the other units in attacking the enemy. Other units, such as fighters and archers, only attack enemy units. 107 | This results in a very aggressive AI." 108 | [/message] 109 | # wmllint: display off 110 | [/event] 111 | 112 | # Stop if this was the last death on this side 113 | [event] 114 | name=die 115 | first_time_only=no 116 | 117 | [if] 118 | [not] 119 | [have_unit] 120 | side=$unit.side 121 | [/have_unit] 122 | [/not] 123 | [then] 124 | [message] 125 | id=$unit.id 126 | message=_"We lost ..." 127 | [/message] 128 | 129 | # So that game goes on to next scenario 130 | [modify_side] 131 | side=3 132 | controller=human 133 | [/modify_side] 134 | 135 | [endlevel] 136 | result=victory 137 | bonus=no 138 | carryover_percentage=0 139 | carryover_report=no 140 | linger_mode=no 141 | [/endlevel] 142 | [/then] 143 | [/if] 144 | [/event] 145 | [/scenario] 146 | -------------------------------------------------------------------------------- /AI-demos/scenarios/luaai_demo.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | 3 | #wmllint: local spelling SW 4 | 5 | [scenario] 6 | id=luaai-demo 7 | name=_"LuaAI" 8 | next_scenario=aid_switchboard 9 | 10 | map_data="{multiplayer/maps/2p_The_Freelands.map}" 11 | 12 | {DEFAULT_SCHEDULE} 13 | turns=-1 14 | victory_when_enemies_defeated=yes 15 | 16 | [side] 17 | side=1 18 | controller=ai 19 | id=Lua AI 20 | type=Lieutenant 21 | persistent=no 22 | 23 | team_name=north 24 | user_team_name=_"Northern Army" 25 | recruit=Cavalryman,Horseman,Spearman,Fencer,Heavy Infantryman,Bowman,Mage 26 | 27 | gold=200 28 | 29 | [ai] 30 | {MODIFY_AI_ADD_CANDIDATE_ACTION 1 main_loop ( 31 | [candidate_action] 32 | engine=lua 33 | name=move_spearmen 34 | id=move_spearmen 35 | max_score=100010 36 | location="~add-ons/AI-demos/lua/ca_move_unittype.lua" 37 | [args] 38 | type=Spearman 39 | score=100010 40 | goal_x=4 41 | goal_y=20 42 | [/args] 43 | [/candidate_action] 44 | )} 45 | {MODIFY_AI_ADD_CANDIDATE_ACTION 1 main_loop ( 46 | [candidate_action] 47 | engine=lua 48 | name=move_bowmen 49 | id=move_bowmen 50 | max_score=99990 51 | location="~add-ons/AI-demos/lua/ca_move_unittype.lua" 52 | [args] 53 | type=Bowman 54 | score=99990 55 | goal_x=34 56 | goal_y=20 57 | [/args] 58 | [/candidate_action] 59 | )} 60 | [/ai] 61 | [/side] 62 | # We want to make sure there are some spearmen and bowmen 63 | {LIMIT_CONTEMPORANEOUS_RECRUITS 1 "Mage" 1} 64 | {LIMIT_CONTEMPORANEOUS_RECRUITS 1 "Cavalryman" 1} 65 | {LIMIT_CONTEMPORANEOUS_RECRUITS 1 "Horseman" 1} 66 | {LIMIT_CONTEMPORANEOUS_RECRUITS 1 "Heavy Infantryman" 1} 67 | {LIMIT_CONTEMPORANEOUS_RECRUITS 1 "Fencer" 1} 68 | 69 | [side] 70 | side=2 71 | controller=human 72 | id=Player 73 | type=Lieutenant 74 | persistent=no 75 | 76 | team_name=south 77 | user_team_name=_"Southern Army" 78 | recruit=Cavalryman,Horseman,Spearman,Fencer,Heavy Infantryman,Bowman,Mage,Merman Fighter 79 | 80 | gold=200 81 | [/side] 82 | 83 | [side] # This side is only here because we need one persistent side for the game to go on 84 | side=3 85 | controller=null 86 | persistent=yes 87 | save_id=Grnk 88 | type=Goblin Spearman 89 | hidden=yes 90 | [/side] 91 | 92 | # Prestart actions 93 | [event] 94 | name=prestart 95 | 96 | {VARIABLE scenario_name luaai-demo} 97 | 98 | # Goal signposts for AI 99 | {PLACE_IMAGE "scenery/signpost.png" 4 20} 100 | {SET_LABEL 4 20 _"Spearmen go here"} 101 | {PLACE_IMAGE "scenery/signpost.png" 34 20} 102 | {SET_LABEL 34 20 _"Bowmen go here"} 103 | [/event] 104 | 105 | [event] 106 | name=start 107 | 108 | # wmllint: display on 109 | [message] 110 | speaker=narrator 111 | image="wesnoth-icon.png" 112 | caption=_"Note" 113 | message=_"This scenario is meant as a simple demonstration of setting up a Lua AI engine and using it in a scenario. Its purpose is not to provide an AI that plays well or does anything particularly useful, but to serve as code template for your own AI work. The behavior is as follows: 114 | 115 | - All spearmen are sent toward the signpost in the SW. 116 | - All bowmen are sent toward the signpost in the SE. 117 | - Spearmen ignore all enemies and try to circumvent them, while bowmen are distractible and veer off their path to attack enemy units. 118 | 119 | Note that the differences in behavior (unit types, signpost locations and whether units attack or not) are determined by parameters that are passed to the AI functions rather than by different code. In particular, whether units attack or not is determined simply by the candidate action evaluation score." 120 | [/message] 121 | # wmllint: display off 122 | 123 | [objectives] 124 | side=2 125 | [objective] 126 | description=_"Defeat the enemy leader" 127 | condition=win 128 | [/objective] 129 | [objective] 130 | description=_"Death of your leader" 131 | condition=lose 132 | [/objective] 133 | [objective] 134 | description=_"A Side 1 spearman or bowman makes it to a signpost" 135 | condition=lose 136 | [/objective] 137 | [/objectives] 138 | [/event] 139 | 140 | # IF we beat the enemy leader 141 | [event] 142 | name=victory 143 | 144 | # So that game goes on to next scenario 145 | [modify_side] 146 | side=3 147 | controller=human 148 | [/modify_side] 149 | [/event] 150 | 151 | [event] 152 | name=moveto 153 | [filter] 154 | type=Spearman,Bowman 155 | x=4,34 156 | y=20,20 157 | [/filter] 158 | 159 | [message] 160 | id=$unit.id 161 | message=_"I made it. You lost!" 162 | [/message] 163 | 164 | [endlevel] 165 | result=defeat 166 | [/endlevel] 167 | [/event] 168 | [/scenario] 169 | -------------------------------------------------------------------------------- /AI-demos/scenarios/manual_ai.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | 3 | #ifdef TEST 4 | [test] 5 | #else 6 | # wmlindent: start ignoring 7 | [scenario] 8 | # wmlindent: stop ignoring 9 | #endif 10 | id=manual_ai 11 | name=_"Manual AI" 12 | next_scenario=aid_switchboard 13 | 14 | map_data="{multiplayer/maps/2p_The_Freelands.map}" 15 | 16 | {DEFAULT_SCHEDULE} 17 | random_start_time=yes 18 | turns=50 19 | victory_when_enemies_defeated=yes 20 | 21 | [side] 22 | side=1 23 | controller=human 24 | id=Fred 25 | name=Fred 26 | type=Orcish Warrior 27 | persistent=no 28 | facing=sw 29 | 30 | team_name=Fred 31 | user_team_name=_"Fred" 32 | 33 | gold=100 34 | village_gold=2 35 | [/side] 36 | 37 | [side] 38 | side=2 39 | controller=ai 40 | id=challenger 41 | type=Orcish Warrior 42 | persistent=no 43 | 44 | team_name=challenger 45 | user_team_name=_"Fred's Challenger" 46 | save_id="Fred's Challenger" 47 | 48 | recruit=Goblin Spearman, Naga Fighter, Orcish Archer, Orcish Assassin, Orcish Grunt, Troll Whelp, Wolf Rider 49 | gold=100 50 | village_gold=2 51 | [village] 52 | x=26 53 | y=22 54 | [/village] 55 | 56 | {~add-ons/AI-demos/ais/ai_manual.cfg} 57 | [/side] 58 | 59 | [event] 60 | name=start 61 | 62 | [message] 63 | id=Fred 64 | caption=_"Manual External AI Control Testing" 65 | message=_"This is a proof-of-concept scenario demonstrating how one can control units on an AI side with input external to Wesnoth (via a text editor in this case). This method can be used, for example, by a machine learning framework. 66 | 67 | End the side 1 turn and move the side 2 leader (id = challenger) by editing (and saving!) file '~/add-ons/AI-demos/lua/manual_input.lua'. Note that there are no checks built in whether the move is actually meaningful or possible, this is really just a bare-bones proof of concept. 68 | 69 | This scenario can also be started from the commandline as a test scenario with 'wesnoth -d -tmanual_ai' or by using the test scenario hotkey in the title screen." # wmllint: no spellcheck 70 | [/message] 71 | 72 | [objectives] 73 | summary=_"Testing of manual interface to AI" 74 | [/objectives] 75 | [/event] 76 | 77 | #ifndef TEST 78 | [/scenario] 79 | #else 80 | # wmlindent: start ignoring 81 | [/test] 82 | # wmlindent: stop ignoring 83 | #endif 84 | -------------------------------------------------------------------------------- /AI-demos/scenarios/mp_ais.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | 3 | [scenario] 4 | id=MP_AIs 5 | name=_"Experimental MP AIs" 6 | next_scenario=aid_switchboard 7 | 8 | map_data="{multiplayer/maps/2p_The_Freelands.map}" 9 | 10 | {DEFAULT_SCHEDULE} 11 | turns=-1 12 | victory_when_enemies_defeated=no 13 | 14 | # wmllint: recognize grunt 15 | [side] 16 | side=1 17 | controller=human 18 | type=Goblin Spearman 19 | id=Grnk 20 | 21 | persistent=yes 22 | save_id=Grnk 23 | 24 | gold=0 25 | income=-2 # No income whatsoever 26 | [/side] 27 | 28 | [event] 29 | name=prestart 30 | 31 | {VARIABLE scenario_name MP_AIs} 32 | 33 | {UNIT 1 (Orcish Grunt) 15 4 id,facing=grunt,se} 34 | 35 | [modify_unit] 36 | [filter] 37 | id=Grnk 38 | [/filter] 39 | 40 | facing=sw 41 | [/modify_unit] 42 | [/event] 43 | 44 | [event] 45 | name=start 46 | 47 | # wmllint: display on 48 | [message] 49 | id=grunt 50 | message=_"They there. We them get. 51 | 52 | Grunt rush many players." 53 | [/message] 54 | # wmllint: display off 55 | [message] 56 | id=Grnk 57 | message=_"It's multiplayer, not many players. Maybe you better let me explain." 58 | [/message] 59 | [message] 60 | id=grunt 61 | message=_"Little goblin smart." 62 | [/message] 63 | # wmllint: display on 64 | [message] 65 | id=Grnk 66 | message=_"Yeah, well, fortunately you don't need smarts to execute a good grunt rush. Anyway... 67 | 68 | This campaign includes several experimental AIs (not just grunt rushes) that can be used in multiplayer mode. To play one of these AIs: 69 | - Go into the MP lobby (either local game or on the server) 70 | - Select either era or modification 'AI-demos Experimental AIs' 71 | - Select a map and options 72 | - Select 'Computer Player' for the AI side(s) 73 | - Select one of the new AIs 74 | 75 | Note: 76 | - The AI called 'AI-demos: Ron — Generic Rush AI' has by now been added into mainline and is available there as 'Experimental AI'. The mainline and AI-Demos versions are not necessarily exactly the same as changes are first tested in AI-Demos and implemented in mainline later." 77 | [/message] 78 | # wmllint: display off 79 | [message] 80 | id=Grnk 81 | message=_"And now back to the start ..." 82 | [/message] 83 | 84 | [endlevel] 85 | result=victory 86 | bonus=no 87 | carryover_percentage=0 88 | carryover_report=no 89 | linger_mode=no 90 | [/endlevel] 91 | [/event] 92 | [/scenario] 93 | -------------------------------------------------------------------------------- /AI-demos/scenarios/switchboard.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | 3 | [scenario] 4 | id=aid_switchboard 5 | name= _ "Switchboard" 6 | next_scenario=null 7 | 8 | map_data="{~add-ons/AI-demos/maps/Dark_Forecast.map}" 9 | 10 | {DEFAULT_SCHEDULE} 11 | turns=-1 12 | victory_when_enemies_defeated=no 13 | 14 | [story] 15 | [if] 16 | [variable] 17 | name=story_shown 18 | not_equals=yes 19 | [/variable] 20 | [then] 21 | [part] 22 | [background_layer] 23 | image=misc/blank.png 24 | base_layer=yes 25 | [/background_layer] 26 | [background_layer] 27 | image=portraits/goblins/spearman.png 28 | scale=no 29 | [/background_layer] 30 | 31 | story= _ "Hi, I'm Grnk the Frail. You may remember me from such instructional campaigns as 'Grnk the Mighty' and 'Why You Shouldn't Attack Peasants When There's A Mad Goblin Around.' 32 | 33 | This campaign is the home of a number of AI development projects, most notably two MP AIs dubbed 'Ron' and 'Fred' as well as several test and demo scenarios. This is also where the Micro AIs were developed. As the latter have now been moved into mainline, they are no longer part of this add-on. 34 | 35 | If you have comments, problems, suggestions or requests for additional AI behaviors, please visit our thread on the Wesnoth forums at http://tinyurl.com/AI-mods" # wmllint: no spellcheck 36 | [/part] 37 | [/then] 38 | [/if] 39 | [/story] 40 | 41 | [side] 42 | side=1 43 | controller=human 44 | id=Grnk 45 | name= _ "Grnk the Frail" 46 | gender=male 47 | unrenamable=yes 48 | type=Goblin Spearman 49 | max_moves=99 50 | x,y=13,15 51 | 52 | team_name=Grnk 53 | user_team_name= _ "team_name^Grnk" 54 | persistent=yes 55 | save_id=Grnk 56 | 57 | [modifications] 58 | {TRAIT_QUICK} 59 | [/modifications] 60 | 61 | village_gold=0 62 | {GOLD 24 22 19} 63 | income=-2 # No income whatsoever 64 | [/side] 65 | 66 | # The labels and signposts to go on to the next scenario 67 | [event] 68 | name=prestart 69 | 70 | [modify_unit] 71 | [filter] 72 | id=Grnk 73 | [/filter] 74 | 75 | facing=sw 76 | [/modify_unit] 77 | 78 | {PLACE_IMAGE "scenery/signpost.png~GS()" 9 12} 79 | {SET_LABEL 9 12 _"Manual AIs"} 80 | 81 | {PLACE_IMAGE "scenery/signpost.png~GS()" 9 13} 82 | {SET_LABEL 9 13 _"MP AIs"} 83 | 84 | {PLACE_IMAGE "scenery/signpost.png~GS()" 9 14} 85 | {SET_LABEL 9 14 _"Test Scenario"} 86 | 87 | {PLACE_IMAGE "scenery/signpost.png~GS()" 9 15} 88 | {SET_LABEL 9 15 _"Simple Lua AI Demo"} 89 | 90 | {PLACE_IMAGE "scenery/signpost.png~GS()" 9 16} 91 | {SET_LABEL 9 16 _"Fred"} 92 | 93 | #{PLACE_IMAGE "scenery/signpost.png~GS()" 1 10} 94 | #{SET_LABEL 4 10 _"Formula AI Demo"} 95 | 96 | {VARIABLE scenario_name aid_switchboard} 97 | {VARIABLE story_shown yes} 98 | 99 | [set_menu_item] 100 | id=m95_manual 101 | description= _ "Manual AI" 102 | image=units/orcs/grunt.png~CROP(23,15,24,24) 103 | [filter_location] 104 | x,y=9,12 105 | [/filter_location] 106 | [show_if] 107 | {VARIABLE_CONDITIONAL scenario_name equals aid_switchboard} 108 | [/show_if] 109 | [command] 110 | # wmllint: display on 111 | [message] 112 | id=Grnk 113 | caption=_"Manual External AI Control Demo Scenario" 114 | message=_"This is a proof-of-concept scenario demonstrating how one can control units on an AI side with input external to Wesnoth (via a text editor in this case). This method can be used, for example, by a machine learning framework. 115 | 116 | This scenario can also be started from the commandline as a test scenario with 'wesnoth -d -tmanual_ai' or by using the test scenario hotkey in the title screen." # wmllint: no spellcheck 117 | [/message] 118 | # wmllint: display off 119 | [/command] 120 | [/set_menu_item] 121 | [set_menu_item] 122 | id=m91_MP 123 | description= _ "Experimental MP AIs (Lua AI)" 124 | image=units/orcs/grunt.png~CROP(23,15,24,24) 125 | [filter_location] 126 | x,y=9,13 127 | [/filter_location] 128 | [show_if] 129 | {VARIABLE_CONDITIONAL scenario_name equals aid_switchboard} 130 | [/show_if] 131 | [command] 132 | [message] 133 | id=Grnk 134 | caption=_"Experimental MP AIs" 135 | message=_"Story only scenario that explains how to use the experimental multiplayer AIs." 136 | [/message] 137 | [/command] 138 | [/set_menu_item] 139 | 140 | [set_menu_item] 141 | id=m92_test 142 | description= _ "Lua AI Test Scenario" 143 | image=units/orcs/grunt.png~CROP(23,15,24,24) 144 | [filter_location] 145 | x,y=9,14 146 | [/filter_location] 147 | [show_if] 148 | {VARIABLE_CONDITIONAL scenario_name equals aid_switchboard} 149 | [/show_if] 150 | [command] 151 | # wmllint: display on 152 | [message] 153 | id=Grnk 154 | caption=_"Lua AI Test Scenario" 155 | message=_"This is a hard-core test scenario that is here for manual testing of AI functions or candidate actions. It provides a right-click menu option for doing so without having to reload. 156 | 157 | It can also be started from the commandline as a test scenario with 'wesnoth -d -taid_test' or by using the test scenario hotkey in the title screen." # wmllint: no spellcheck 158 | [/message] 159 | # wmllint: display off 160 | [/command] 161 | [/set_menu_item] 162 | 163 | [set_menu_item] 164 | id=m80_Lua_demo 165 | description= _ "Simple Lua AI Demo" 166 | image=units/orcs/grunt.png~CROP(23,15,24,24) 167 | [filter_location] 168 | x,y=9,15 169 | [/filter_location] 170 | [show_if] 171 | {VARIABLE_CONDITIONAL scenario_name equals aid_switchboard} 172 | [/show_if] 173 | [command] 174 | [message] 175 | id=Grnk 176 | caption=_"Simple Lua AI Demo" 177 | message=_"This scenario provides a simple template for setting up a Lua AI engine and scenario. The AI behavior itself is trivial and not particularly useful (some of the AI units simply move toward two map locations, with priority either higher or lower than attacks on enemies), it is only meant as a demonstration how to code this." 178 | [/message] 179 | [/command] 180 | [/set_menu_item] 181 | 182 | [set_menu_item] 183 | id=m93_fred 184 | description= _ "Fred — Freelands MP AI" 185 | image=units/orcs/grunt.png~CROP(23,15,24,24) 186 | [filter_location] 187 | x,y=9,16 188 | [/filter_location] 189 | [show_if] 190 | {VARIABLE_CONDITIONAL scenario_name equals aid_switchboard} 191 | [/show_if] 192 | [command] 193 | [message] 194 | id=Grnk 195 | caption=_"Fred — Freelands MP AI Test Scenario" 196 | message=_"That's what it is." 197 | [/message] 198 | [/command] 199 | [/set_menu_item] 200 | 201 | [set_menu_item] 202 | id=m94_fai 203 | description= _ "Formula AI demo" # Not included yet 204 | image=units/elves-wood/avenger.png~CROP(25,11,24,24) 205 | [filter_location] 206 | x,y=0,0 207 | [/filter_location] 208 | [show_if] 209 | {VARIABLE_CONDITIONAL scenario_name equals aid_switchboard} 210 | [/show_if] 211 | [command] 212 | [message] 213 | id=Grnk 214 | caption=_"Formula AI Demo" 215 | message=_"This scenario contains a simple demonstration of setting up a full AI using Formula AI." 216 | [/message] 217 | [/command] 218 | [/set_menu_item] 219 | [/event] 220 | 221 | [event] 222 | name=start 223 | 224 | {SCROLL_TO 13 18} 225 | 226 | [message] 227 | id=Grnk 228 | message=_"Move me to any of the signposts to go to an AI demonstration. Information about each demonstration can be accessed by right-clicking on the respective signpost." 229 | [/message] 230 | 231 | [objectives] 232 | [objective] 233 | description= _ "Move Grnk to one of the signposts" 234 | condition=win 235 | [/objective] 236 | [note] 237 | description= _ "Right-click on a signpost to get information about the scenario" 238 | [/note] 239 | [/objectives] 240 | [/event] 241 | 242 | [event] 243 | name=moveto 244 | [filter] 245 | x,y=9,12 246 | [/filter] 247 | 248 | [endlevel] 249 | result=victory 250 | next_scenario=manual_ai 251 | bonus=no 252 | carryover_percentage=0 253 | carryover_report=no 254 | linger_mode=no 255 | replay_save=no 256 | [/endlevel] 257 | [/event] 258 | 259 | [event] 260 | name=moveto 261 | [filter] 262 | x,y=9,13 263 | [/filter] 264 | 265 | [endlevel] 266 | result=victory 267 | next_scenario=MP_AIs 268 | bonus=no 269 | carryover_percentage=0 270 | carryover_report=no 271 | linger_mode=no 272 | replay_save=no 273 | [/endlevel] 274 | [/event] 275 | 276 | [event] 277 | name=moveto 278 | [filter] 279 | x,y=9,14 280 | [/filter] 281 | 282 | [endlevel] 283 | result=victory 284 | next_scenario=aid_test 285 | bonus=no 286 | carryover_percentage=0 287 | carryover_report=no 288 | linger_mode=no 289 | replay_save=no 290 | [/endlevel] 291 | [/event] 292 | 293 | [event] 294 | name=moveto 295 | [filter] 296 | x,y=9,15 297 | [/filter] 298 | 299 | [endlevel] 300 | result=victory 301 | next_scenario=luaai-demo 302 | bonus=no 303 | carryover_percentage=0 304 | carryover_report=no 305 | linger_mode=no 306 | replay_save=no 307 | [/endlevel] 308 | [/event] 309 | 310 | [event] 311 | name=moveto 312 | [filter] 313 | x,y=9,16 314 | [/filter] 315 | 316 | [endlevel] 317 | result=victory 318 | next_scenario=fred 319 | bonus=no 320 | carryover_percentage=0 321 | carryover_report=no 322 | linger_mode=no 323 | replay_save=no 324 | [/endlevel] 325 | [/event] 326 | 327 | [event] 328 | name=moveto2 329 | [filter] 330 | x,y=0,0 331 | [/filter] 332 | 333 | [endlevel] 334 | result=victory 335 | next_scenario=fai_demo 336 | bonus=no 337 | carryover_percentage=0 338 | carryover_report=no 339 | linger_mode=no 340 | replay_save=no 341 | [/endlevel] 342 | [/event] 343 | [/scenario] 344 | -------------------------------------------------------------------------------- /AI-demos/scenarios/test.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-AI-demos 2 | 3 | #ifdef TEST 4 | [test] 5 | #else 6 | # wmlindent: start ignoring 7 | [scenario] 8 | # wmlindent: stop ignoring 9 | #endif 10 | id=aid_test 11 | name=_"Test" 12 | next_scenario=aid_switchboard 13 | 14 | map_data="{multiplayer/maps/2p_The_Freelands.map}" 15 | #map_data="{~add-ons/Grnk/part1/maps/11_Escape.map}" 16 | 17 | {DEFAULT_SCHEDULE} 18 | turns=50 19 | victory_when_enemies_defeated=no 20 | 21 | [side] 22 | side=1 23 | controller=human 24 | id=Vanak 25 | name=Vanak 26 | type=Orcish Ruler 27 | persistent=yes 28 | 29 | team_name=Vanak 30 | user_team_name=_"Vanak" 31 | 32 | recruit=Orcish Grunt,Orcish Archer,Orcish Assassin,Wolf Rider,Troll Whelp 33 | gold=200 34 | 35 | {ai/aliases/stable_singleplayer.cfg} 36 | [ai] 37 | id=luaAI_test 38 | description=_"Lua AI Test" 39 | [engine] 40 | name="lua" 41 | code= << 42 | --local ai_stdlib = wesnoth.require('ai/lua/stdlib.lua'); 43 | --ai_stdlib.init(ai, true) 44 | return {} -- Just a dummy return 45 | >> 46 | [/engine] 47 | [/ai] 48 | ################## Or use external CA ########################## 49 | #[ai] 50 | # version=10710 51 | # [stage] 52 | # id=main_loop 53 | # name=ai_default_rca::candidate_action_evaluation_loop 54 | # [candidate_action] 55 | # engine=lua 56 | # name=external_example 57 | # location="~/add-ons/LuaAI_tests/ext_test.lua" 58 | # [/candidate_action] 59 | # [/stage] 60 | #[/ai] 61 | [/side] 62 | 63 | [side] 64 | side=2 65 | controller=human 66 | id=Bad Orc 67 | type=Orcish Ruler 68 | persistent=no 69 | 70 | team_name=Orcs 71 | user_team_name=_"Orcs" 72 | 73 | recruit=Orcish Grunt,Orcish Archer,Orcish Assassin,Wolf Rider,Troll Whelp 74 | gold=0 75 | [ai] 76 | aggression=1 77 | #{MODIFY_AI_DELETE_CANDIDATE_ACTION 2 main_loop combat} 78 | {MODIFY_AI_ADD_CANDIDATE_ACTION 2 main_loop ( 79 | [candidate_action] 80 | engine=lua 81 | name=attack_highca 82 | id=attack_highca 83 | max_score=100010 84 | location="~add-ons/AI-demos/lua/ca_test.lua" 85 | [/candidate_action] 86 | )} 87 | [/ai] 88 | [/side] 89 | 90 | [event] 91 | name=prestart 92 | 93 | {VARIABLE scenario_name aid_test} 94 | 95 | [set_menu_item] 96 | id = m01 97 | description=_"Reload Lua code" 98 | image=items/ring-red.png~CROP(26,26,20,20) 99 | [command] 100 | [lua] 101 | code=<> 102 | [/lua] 103 | [/command] 104 | [default_hotkey] 105 | key=x 106 | [/default_hotkey] 107 | [/set_menu_item] 108 | 109 | [objectives] 110 | summary = _ "This scenario can be started directly from the commandline with '-taid_test' or by using the test scenario hotkey in the title screen." # wmllint: no spellcheck 111 | [/objectives] 112 | [/event] 113 | #ifndef TEST 114 | [/scenario] 115 | #else 116 | # wmlindent: start ignoring 117 | [/test] 118 | # wmlindent: stop ignoring 119 | #endif 120 | -------------------------------------------------------------------------------- /AI-demos/units/Invisible_Unit.cfg: -------------------------------------------------------------------------------- 1 | #textdomain wesnoth-Grnk 2 | 3 | # This is just the Fog Clearer code, but with team color set 4 | [unit_type] 5 | id=Invisible Unit 6 | name=_ "dummy_unit^Invisible Unit" 7 | race=monster 8 | image="misc/blank-hex.png" 9 | ellipse=none 10 | hitpoints=100 11 | movement_type=fly 12 | movement=9 13 | experience=100 14 | level=2 15 | alignment=neutral 16 | advances_to=null 17 | cost=50 18 | usage=scout 19 | 20 | # This unit hides everywhere. It can therefore be used if one wants 21 | # a unit whose ellipse and status bars never show up, by using it on 22 | # an enemy side. If desired, it can be made visible by assigning it 23 | # role=IU_visible. 24 | [abilities] 25 | [hides] 26 | id=hides_everywhere 27 | affect_self=yes 28 | [filter] 29 | [not] 30 | role=IU_visible 31 | [/not] 32 | [/filter] 33 | [/hides] 34 | [/abilities] 35 | 36 | # Use attacks from the Javelineer 37 | [attack] 38 | name=spear 39 | description=_"spear" 40 | type=pierce 41 | range=melee 42 | damage=8 43 | number=3 44 | [specials] 45 | {WEAPON_SPECIAL_FIRSTSTRIKE} 46 | [/specials] 47 | [/attack] 48 | [attack] 49 | name=javelin 50 | description=_"javelin" 51 | icon=attacks/javelin-human.png 52 | type=pierce 53 | range=ranged 54 | damage=11 55 | number=2 56 | [/attack] 57 | [/unit_type] 58 | -------------------------------------------------------------------------------- /AI-demos/version.lua: -------------------------------------------------------------------------------- 1 | return '0.14.15+dev' -------------------------------------------------------------------------------- /AI-demos/version.txt: -------------------------------------------------------------------------------- 1 | 0.14.15+dev -------------------------------------------------------------------------------- /COPYING.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 309 | 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Library General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Wesnoth-AI-Demos 2 | ================ 3 | 4 | Wesnoth AI Modification Demonstration Scenarios 5 | 6 | NOTES: 7 | * This add-on requires Wesnoth version 1.12.0 or later, but using the most recent stable version is recommended. 8 | * If done manually, the add-on needs to be installed into ~add-ons/AI-demos/ or just download it from the Wesnoth add-ons server. 9 | 10 | Home of the formidable Freelands AI, "Fred", and his little brother, the generic rushing AI "Ron"! 11 | 12 | Many other Micro AIs were also developed in this add-on, but have now mostly been moved into mainline, such as: 13 | * Animals 14 | * Bottleneck Defense 15 | * Goto 16 | * Guardians 17 | * Hang Out 18 | * Healer Support 19 | * Lurkers 20 | * Messenger Escort 21 | * Patrol 22 | * Protect Unit 23 | * Recruiting 24 | * Simple Attack 25 | 26 | For a complete and current list of the Micro AIs, see: http://wiki.wesnoth.org/Micro_AIs 27 | 28 | For more information, check out the thread on the official Wesnoth forums: 29 | http://forum.wesnoth.org/viewtopic.php?f=10&t=34976 30 | 31 | There is also a feedback thread specifically for the Micro AIs: 32 | http://forums.wesnoth.org/viewtopic.php?f=10&t=39456 33 | 34 | And of course get the game itself at http://wesnoth.org/ 35 | 36 | All of our code is licensed under the GPLv2+, just like the rest of Wesnoth. 37 | --------------------------------------------------------------------------------