Add [find_path] option "nearest_by", and simple_find_path test

Adding this is issue 2 of #4177, changing the behavior when [find_path]
is given a SLF which matches multiple hexes.

The map and tests here should be easy enough for manually editing them. It
duplicates some of the functionality of the existing characterize_pathfinding
tests, however those tests need their expected values to be calculated and
can't be changed by hand.

'''nearest_by''': {DevFeature1.15|2} possible values "movement_cost"
(default), "steps", "hexes". If the [destination] SLF matches multiple hexes,
the one that would need the least movement points to reach may not be the one
that's closest as measured by '''hexes''', or closest as measured by steps,
from the starting point.

Behavior in 1.14 depended on which hex was checked first.
This commit is contained in:
Steve Cotton 2019-07-22 12:44:44 +02:00
parent 2d16a3b410
commit 06dd9a140c
6 changed files with 224 additions and 13 deletions

View file

@ -26,6 +26,19 @@ function wesnoth.wml_actions.find_path(cfg)
local allow_multiple_turns = cfg.allow_multiple_turns
local viewing_side
local nearest_by_cost = true
local nearest_by_distance = false
local nearest_by_steps = false
if (cfg.nearest_by or "movement_cost") == "hexes" then
nearest_by_cost = false
nearest_by_distance = true
nearest_by_steps = false
elseif (cfg.nearest_by or "movement_cost") == "steps" then
nearest_by_cost = false
nearest_by_distance = false
nearest_by_steps = true
end
if not cfg.check_visibility then viewing_side = 0 end -- if check_visiblity then shroud is taken in account
-- only the first location with the lowest distance and lowest movement cost will match.
@ -33,32 +46,57 @@ function wesnoth.wml_actions.find_path(cfg)
local max_cost = nil
if not allow_multiple_turns then max_cost = unit.moves end --to avoid wrong calculation on already moved units
local current_distance, current_cost = math.huge, math.huge
local current_distance, current_cost, current_steps = math.huge, math.huge, math.huge
local current_location = {}
local width,heigth = wesnoth.get_map_size() -- data for test below
for index, location in ipairs(locations) do
-- we test if location passed to pathfinder is invalid (border);
-- if it is, do not return and continue the cycle
-- if it is, do not use it, and continue the cycle
if location[1] == 0 or location[1] == ( width + 1 ) or location[2] == 0 or location[2] == ( heigth + 1 ) then
else
local distance = wesnoth.map.distance_between ( unit.x, unit.y, location[1], location[2] )
-- if we pass an unreachable locations an high value will be returned
-- if we pass an unreachable location then an empty path and high value cost will be returned
local path, cost = wesnoth.find_path( unit, location[1], location[2], { max_cost = max_cost, ignore_units = ignore_units, ignore_teleport = ignore_teleport, viewing_side = viewing_side } )
if distance < current_distance and cost <= current_cost
or cost < current_cost and distance <= current_distance
then
-- avoid changing the hex with one less distance and more cost, or vice versa
current_distance = distance
current_cost = cost
current_location = location
if #path == 0 or cost >= 42424241 then
-- it's not a reachable hex. 42424242 is the high value returned for unwalkable or busy terrains
else
local steps = #path
local is_better = false
if nearest_by_cost and cost < current_cost then
is_better = true
elseif nearest_by_distance and distance < current_distance then
is_better = true
elseif nearest_by_steps and steps < current_steps then
is_better = true
elseif cost == current_cost and distance == current_distance and steps == current_steps then
-- the two options are equivalent. Treating this as not-better probably creates a bias for
-- choosing the north-west option, treating it as better probably biases to south-east.
-- Choosing false is more likely to match the option that 1.14 would choose.
is_better = false
elseif cost <= current_cost and distance <= current_distance and steps <= current_steps then
is_better = true
end
if is_better then
current_distance = distance
current_cost = cost
current_steps = steps
current_location = location
end
end
end
end
if #current_location == 0 then wesnoth.message("WML warning","[find_path]'s filter didn't match any location")
if #current_location == 0 then
-- either no matching locations, or only inaccessible matching locations (maybe enemy units are there)
if #locations == 0 then
wesnoth.message("WML warning","[find_path]'s filter didn't match any location")
end
wml.variables[tostring(variable)] = { hexes = 0 } -- set only hexes, nil all other values
else
local path, cost = wesnoth.find_path(
unit,
@ -77,7 +115,7 @@ function wesnoth.wml_actions.find_path(cfg)
turns = math.ceil( ( ( cost - unit.moves ) / unit.max_moves ) + 1 )
end
if cost >= 42424242 then -- it's the high value returned for unwalkable or busy terrains
if cost >= 42424241 then -- it's the high value returned for unwalkable or busy terrains
wml.variables[tostring(variable)] = { hexes = 0 } -- set only length, nil all other values
-- support for $this_unit
wml.variables["this_unit"] = nil -- clearing this_unit

View file

@ -1149,6 +1149,7 @@
{DEFAULT_KEY check_visibility s_bool no}
{DEFAULT_KEY check_teleport s_bool yes}
{DEFAULT_KEY check_zoc s_bool yes}
{DEFAULT_KEY nearest_by find_path_nearest_by movement_cost}
{FILTER_TAG "traveler" unit (
min=1
{INSERT_TAG}

View file

@ -311,6 +311,10 @@
name="reachable_moves"
value="current|max"
[/type]
[type]
name="find_path_nearest_by"
value="movement_cost|hexes|steps"
[/type]
[type]
name="search_recall_list"
[union]

View file

@ -0,0 +1,11 @@
Xv, Xv, Xv, Xv, Xv, Xv, Xv, Xv, Xv
Xv, Xv, Wwt, Wwt, Wwt, Xv, Xv, Xv, Xv
Xv, Xv, Wwt, lake Gs^Vc, Wwt, Xv, in_void Gs^Vc, Xv, Xv
Xv, Xv, Wwt, Wwt, Wwt, Xv, Xv, Xv, Xv
Xv, Xv, Xv, Wwt, Xv, Xv, Xv, Xv, Xv
Xv, 2 Gs^Vc, Gg, 1 Gg, Xv, u_turn Gs^Vc, Xv, Xv, Xv
Xv, Xv, Xv, Gg, Xv, Gg, Xv, Xv, Xv
Xv, wet_turn Gs^Vc, Xv, Gg, Xv, Gg, Xv, Xv, Xv
Xv, Wwt, Wwt, Gg, Gg, Gg, Xv, Xv, Xv
Xv, Xv, Xv, Xv, spur Gs^Vc, Xv, Xv, Xv, Xv
Xv, Xv, Xv, Xv, Xv, Xv, Xv, Xv, Xv

View file

@ -0,0 +1,157 @@
# This test is called "simple" find_path because the expected values are hand-coded by
# the developer. This is in contrast to the characterize_pathfinding_* tests, which
# need the expected answers to be generated automatically.
#define FIND_ALICES_PATH DESTINATION
[find_path]
[traveler]
id=alice
[/traveler]
[destination]
{DESTINATION}
[/destination]
allow_multiple_turns=no
variable=path
[/find_path]
#enddef
#define FIND_ALICES_PATH_2 DESTINATION NEAREST_BY
[find_path]
[traveler]
id=alice
[/traveler]
[destination]
{DESTINATION}
[/destination]
allow_multiple_turns=no
variable=path
nearest_by={NEAREST_BY}
[/find_path]
#enddef
# A conditional for ASSERT checks
#define PATH_GOES_TO DESTINATION
[have_location]
{DESTINATION}
[and]
x,y=$path.to_x, $path.to_y
[/and]
[/have_location]
#enddef
[test]
name = "Unit Test simple_find_path"
map_data = "{test/maps/simple_find_path.map}"
turns = 1
id = simple_find_path
random_start_time = no
is_unit_test = yes
{DAWN}
[side]
side=1
controller=human
name = "Alice"
type = Elvish Archer
id=alice
fog=no
shroud=no
share_view=no
[/side]
[side]
side=2
controller=human
name = "Bob"
type = Orcish Grunt
id=bob
fog=no
shroud=no
share_view=no
[/side]
[event]
name = side 1 turn 1
# If a path needs multiple turns then [find_path] will include the
# cost of movement points that were left unused at the end of all turns
# except the last. To avoid that, give Alice enough MP to move anywhere.
[modify_unit]
[filter]
id=alice
[/filter]
moves="$({UNREACHABLE} - 1)"
max_moves="$({UNREACHABLE} - 1)"
[/modify_unit]
# A path can go to the hex that the unit is already on
{FIND_ALICES_PATH location_id=1}
{ASSERT {VARIABLE_CONDITIONAL path.hexes equals 0}}
{ASSERT {VARIABLE_CONDITIONAL path.step.length equals 1}}
{ASSERT {VARIABLE_CONDITIONAL path.movement_cost equals 0}}
{FIND_ALICES_PATH location_id=lake}
{ASSERT {VARIABLE_CONDITIONAL path.hexes equals 3}}
{ASSERT {VARIABLE_CONDITIONAL path.step.length equals 4}}
{ASSERT {VARIABLE_CONDITIONAL path.movement_cost equals 7}}
{FIND_ALICES_PATH location_id=spur}
{ASSERT {VARIABLE_CONDITIONAL path.hexes equals 5}}
{ASSERT {VARIABLE_CONDITIONAL path.step.length equals 6}}
{ASSERT {VARIABLE_CONDITIONAL path.movement_cost equals 5}}
{FIND_ALICES_PATH location_id=u_turn}
{ASSERT {VARIABLE_CONDITIONAL path.hexes equals 2}}
{ASSERT {VARIABLE_CONDITIONAL path.step.length equals 9}}
{ASSERT {VARIABLE_CONDITIONAL path.movement_cost equals 8}}
{FIND_ALICES_PATH location_id=wet_turn}
{ASSERT {VARIABLE_CONDITIONAL path.hexes equals 3}}
{ASSERT {VARIABLE_CONDITIONAL path.step.length equals 7}}
{ASSERT {VARIABLE_CONDITIONAL path.movement_cost equals 10}}
# There's no route to the in_void village
{FIND_ALICES_PATH location_id=in_void}
{ASSERT {VARIABLE_CONDITIONAL path.hexes equals 0}}
{FIND_ALICES_PATH_2 terrain=*^V* movement_cost}
{ASSERT {PATH_GOES_TO location_id=spur}}
{ASSERT {VARIABLE_CONDITIONAL path.hexes equals 5}}
{ASSERT {VARIABLE_CONDITIONAL path.step.length equals 6}}
{ASSERT {VARIABLE_CONDITIONAL path.movement_cost equals 5}}
{FIND_ALICES_PATH_2 terrain=*^V* steps}
{ASSERT {PATH_GOES_TO location_id=lake}}
{ASSERT {VARIABLE_CONDITIONAL path.hexes equals 3}}
{ASSERT {VARIABLE_CONDITIONAL path.step.length equals 4}}
{ASSERT {VARIABLE_CONDITIONAL path.movement_cost equals 7}}
{FIND_ALICES_PATH_2 terrain=*^V* hexes}
{ASSERT {PATH_GOES_TO location_id=u_turn}}
{ASSERT {VARIABLE_CONDITIONAL path.hexes equals 2}}
{ASSERT {VARIABLE_CONDITIONAL path.step.length equals 9}}
{ASSERT {VARIABLE_CONDITIONAL path.movement_cost equals 8}}
# Without ignoring units, we can't move to Bob's starting hex
{FIND_ALICES_PATH location_id=2}
{ASSERT {VARIABLE_CONDITIONAL path.hexes equals 0}}
# If we ignore other units, we can move to Bob's starting hex
[find_path]
[traveler]
id=alice
[/traveler]
[destination]
location_id=2
[/destination]
allow_multiple_turns=no
variable=path
check_zoc=false
[/find_path]
{ASSERT {VARIABLE_CONDITIONAL path.hexes equals 2}}
{ASSERT {VARIABLE_CONDITIONAL path.step.length equals 3}}
{ASSERT {VARIABLE_CONDITIONAL path.movement_cost equals 2}}
{SUCCEED}
[/event]
[/test]

View file

@ -111,8 +111,8 @@
#
0 store_locations_one
0 store_locations_range
0 simple_find_path
# This test occasionally takes too long... (FIXME): 0 characterize_pathfinding_one
# The following tests segfault becasue of http://gna.org/bugs/?23188
0 characterize_pathfinding_reach_1
0 characterize_pathfinding_reach_2
0 characterize_pathfinding_reach_3