wesnoth/data/lua/location_set.lua
Steve Cotton 49ebbb9709 Make fix_whitespace complain about mixing tabs and spaces
Ensures that files either use tabs for indentation or spaces for indentation,
but don't switch between the two within the same file.

This doesn't fix the whitespace, it's a simple check to flag it up
on the assumption that it's better to use an editor or code formatter
to clean up the file.

Elsewhere in the CI we use the luacheck tool - while that can detect
mixing tabs and spaces in a single line's indent, it doesn't check for
inconsistent indentation within a file.
2023-02-05 02:22:53 +01:00

542 lines
14 KiB
Lua

local location_set = {}
local function index(x, y)
-- the 2000 bias ensure that the correct x is recovered for negative y
return x * 16384 + y + 2000
end
local invscale = 1 / 16384
local function revindex(p)
local x = math.floor(p * invscale)
return x, p - x * 16384 - 2000
end
---@alias location_set_operation fun(x:integer, y:integer, value:any):boolean
---@alias location_set_resolver fun(x:integer, y:integer, old:any, new:any):any
---A set of locations, with an optional associated value for each one.
---@class location_set
---@field values table<integer, any>
local methods = {}
local locset_meta = {}
function locset_meta:__call(x, y)
return self:get(x, y)
end
function locset_meta:__index(loc)
if type(loc) == 'string' then
return methods[loc]
elseif loc.x and loc.y then
return self:get(loc.x, loc.y)
else
return self:get(loc[1], loc[2])
end
end
function locset_meta:__newindex(loc, val)
local fcn = methods.insert
if val == nil then fcn = methods.remove end
if loc.x and loc.y then
fcn(self, loc.x, loc.y, val)
else
fcn(self, loc[1], loc[2], val)
end
end
function locset_meta:__bor(other)
local new = self:clone()
new:union(other)
return new
end
function locset_meta:__band(other)
local new = self:clone()
new:inter(other)
return new
end
function locset_meta:__bxor(other)
local new = self:clone()
new:symm(other)
return new
end
if wesnoth.current then
function locset_meta:__bnot(other)
return self:invert(wesnoth.current.map)
end
end
function locset_meta:__sub(other)
local new = self:clone()
new:diff(other)
return new
end
function locset_meta:__tostring()
local res = {}
self:iter(function(x, y, v) res[string.format('(%d,%d)', x, y)] = tostring(v) end)
return '{' .. stringx.join_map(res, '; ', ' = ') .. '}'
end
---Test if the set is empty
---@return boolean
function methods:empty()
return (not next(self.values))
end
---Count the number of locations in the set
---@return integer
function methods:size()
local sz = 0
for p,v in pairs(self.values) do
sz = sz + 1
end
return sz
end
---Remove all locations from the set
function methods:clear()
self.values = {}
end
---Look up a location in the set
---@overload fun(x:integer, y:integer):any
---@overload fun(loc:location):any
function methods:get(...)
local loc = wesnoth.map.read_location(...)
if loc ~= nil then
return self.values[index(loc.x, loc.y)]
end
return nil
end
---Add a location to the set
---@overload fun(set:location_set, x:integer, y:integer, value?:any)
---@overload fun(set:location_set, loc:location, value?:any)
function methods:insert(...)
local loc, n = wesnoth.map.read_location(...)
if loc ~= nil then
local v = select(n + 1, ...)
self.values[index(loc.x, loc.y)] = v or true
end
end
---Remove a location from the set
---@overload fun(x:integer, y:integer)
---@overload fun(loc:location|unit)
function methods:remove(...)
local loc = wesnoth.map.read_location(...)
if loc ~= nil then
self.values[index(loc.x, loc.y)] = nil
end
end
---Create a copy of the set
---This is a shallow copy - if the values are tables they will still
---be referenced by the original set
---@return location_set
function methods:clone()
local new = location_set.create()
for p,v in pairs(self.values) do
new.values[p] = v
end
return new
end
---Take the union of two sets, replacing duplicate values
---@param s location_set The other set to merge in
function methods:union(s)
local values = self.values
for p,v in pairs(s.values) do
values[p] = v
end
end
---Take the union of two sets, merging duplicate values
---@param s location_set The other set to merge in
---@param f location_set_resolver A function which is used to resolve conflicts.
---It will be called for every element of the right-hand set.
function methods:union_merge(s, f)
local values = self.values
for p,v in pairs(s.values) do
local x, y = revindex(p)
values[p] = f(x, y, values[p], v)
end
end
---Take the intersection of two sets, by removing any elements in the left set that
---are missing from the right set.
---@param s location_set The other set to intersect with
function methods:inter(s)
local values = self.values
local nvalues = {}
for p,v in pairs(s.values) do
nvalues[p] = values[p]
end
self.values = nvalues
end
---Take the union of two sets, merging duplicate values
---@param s location_set The other set to merge in
---@param f location_set_resolver A function which is used to resolve conflicts.
---It will be called for every element common to both sets.
function methods:inter_merge(s, f)
local values = s.values
local nvalues = {}
for p,v in pairs(self.values) do
local x, y = revindex(p)
nvalues[p] = f(x, y, v, values[p])
end
self.values = nvalues
end
---Take the relative complement of two location sets.
---This removes any elements from the left set that are also in the right set.
---@param s location_set
function methods:diff(s)
local values = self.values
for p,v in pairs(s.values) do
values[p] = nil
end
end
---Take the symmetric difference of two location sets.
---This removes any elements that are common to both sets,
---while adding new elements to the left set if they only exist in the right.
---@param s location_set
function methods:symm(s)
local values = self.values
for p,v in pairs(s.values) do
if values[p] then
values[p] = nil
else
values[p] = v
end
end
end
---Take the absolute complement of the location set
---The resulting set has no values associated with each location.
---@param width integer The width of the map
---@param height integer The height of the map
---@param border_size integer The border size of the map
---@return location_set
---@overload fun(map:terrain_map):location_set
function methods:invert(width, height, border_size)
if type(width) == 'number' and type(height) == 'number' then
border_size = border_size or 0
elseif type(width) == 'userdata' and getmetatable(width) == 'terrain map' then
local map = width
width = map.playable_width
height = map.playable_height
border_size = map.border_size
else
error('Invalid arguments to location_set:invert - expected a map or map dimensions', 2)
end
local new = location_set.create()
for x = 1 - border_size, width + border_size do
for y = 1 - border_size, height + border_size do
if not self:get(x, y) then
new:insert(x, y)
end
end
end
return new
end
---Filter the set for elements that satisfy a given condition.
---Returns a new set containing only the matching values.
---@param f location_set_operation The condition to test
---@return location_set
function methods:filter(f)
local nvalues = {}
for p,v in pairs(self.values) do
local x, y = revindex(p)
if f(x, y, v) then nvalues[p] = v end
end
return setmetatable({ values = nvalues }, locset_meta)
end
---Iterate over the location set.
---If passed no arguments, it can be used in a range-for loop.
---@param f location_set_operation
---@overload fun():fun()
function methods:iter(f)
if f == nil then
local locs = self
return coroutine.wrap(function()
locs:iter(coroutine.yield)
end)
end
for p,v in pairs(self.values) do
local x, y = revindex(p)
f(x, y, v)
end
end
---Iterate over the location set in a sorted order.
---If passed no arguments, it can be used in a range-for loop.
---@param f location_set_operation
---@overload fun():fun()
function methods:stable_iter(f)
if f == nil then
local locs = self
return coroutine.wrap(function()
locs:stable_iter(coroutine.yield)
end)
end
local indices = {}
for p,v in pairs(self.values) do
table.insert(indices, p)
end
table.sort(indices)
for i,p in ipairs(indices) do
local x, y = revindex(p)
f(x, y, self.values[p])
end
end
---Add a series of locations to the set.
---If the location tables have extra information in them, it will be used as the value.
---@param t location[]
function methods:of_pairs(t)
local values = self.values
local function has_key(v, k)
if type(v) == 'userdata' then
return v[k] ~= nil
elseif type(v) == 'table' then
return rawget(v, k) ~= nil
end
end
for i,v in ipairs(t) do
local value_table = {}
local x_index
local y_index
if has_key(v, 'x') and has_key(v, 'y') then
x_index = "x"
y_index = "y"
else
x_index = 1
y_index = 2
end
for k,val in pairs(v) do
if k ~= x_index and k ~= y_index then
value_table[k] = val
end
end
if next(value_table) then
values[index(v[x_index], v[y_index])] = value_table
else
values[index(v[x_index], v[y_index])] = true
end
end
end
---Add locations stored in a WML variable.
---If the location tables have extra information in them, it will be used as the value.
---@param name string
function methods:of_wml_var(name)
local values = self.values
for i = 0, wml.variables[name .. ".length"] - 1 do
local t = wml.variables[string.format("%s[%d]", name, i)]
local x, y = t.x, t.y
t.x, t.y = nil, nil
values[index(x, y)] = next(t) and t or true
end
end
---Add a series of locations to the set.
---The third element of each location is used as the value.
---@param t location_triple[]
function methods:of_triples(t)
-- Create a location set from a table of 3-element tables
-- Elements 1 and 2 are x,y coordinates, #3 is value to be inserted
for k,v in pairs(t) do
if #v == 0 then
self:insert(v.x, v.y, v.value)
else
self:insert(v[1], v[2], v[3])
end
end
end
--- Add values from a table of location->element mappings
--- Keys can be of the form {x,y} or {x=x,y=y}, or a location-like object such as a unit
---@param t table<location, any>
function methods:of_map(t)
for k,v in pairs(t) do
local loc = wesnoth.read_location(k)
self:insert(loc.x, loc.y, v)
end
end
---Add a series of locations from a shroud data string.
---Each location indicated by a 1 will be added to the set.
---@param data string
function methods:of_shroud_data(data)
self:of_pairs(wesnoth.map.parse_bitmap(data))
end
---Convert the set to an array of locations.
---The value will not be stored in the output array.
---@return location[]
function methods:to_pairs()
local res = {}
self:iter(function(x, y)
table.insert(res, wesnoth.named_tuple({ x, y }, {'x', 'y'}))
end)
return res
end
---Convert the set to an array of locations in a sorted order.
---The value will not be stored in the output array.
---@return location[]
function methods:to_stable_pairs()
local res = {}
self:stable_iter(function(x, y)
table.insert(res, wesnoth.named_tuple({ x, y }, {'x', 'y'}))
end)
return res
end
---Store the set in a WML variable
---@param name string
---@param mode "'always_clear'"|"'append'"|"'replace'"
function methods:to_wml_var(name, mode)
mode = mode or "always_clear"
local is_explicit_index = name[-1] == "]"
local i = 0
-- explicit indexes behave always like "replace"
if not is_explicit_index then
if mode == "append" then
i = wml.variables[name .. ".length"]
elseif mode ~= "replace" then
wml.variables[name] = nil
end
end
self:stable_iter(function(x, y, v)
if wml.valid(v) then
wml.variables[string.format("%s[%d]", name, i)] = v
elseif wml.valid{value = v} then
wml.variables[string.format("%s[%d]", name, i)] = {value = v}
elseif type(v) ~= 'boolean' then
warning('Location set value could not be converted to a WML variable:', v)
end
wml.variables[string.format("%s[%d].x", name, i)] = x
wml.variables[string.format("%s[%d].y", name, i)] = y
i = i + 1
end)
end
---Convert the set to an array of triples - locations with an extra element for the value.
---@return location_triple[]
function methods:to_triples()
local res = {}
self:iter(function(x, y, v)
table.insert(res, wesnoth.named_tuple({ x, y, v }, {"x", "y", "value"}))
end)
return res
end
---Convert the set to a map of location -> value
---@return table<location, any>
function methods:to_map()
local res = {}
self:iter(function(x, y, v)
res[wesnoth.named_tuple({x, y}, {"x", "y"})] = v
end)
return res
end
---Convert the set to a shroud data string
--- Each location in the set will be a 1 in the output string.
---@return string
function methods:to_shroud_data()
return wesnoth.map.make_bitmap(self:to_pairs())
end
---Select a random location from the set
---@return integer #x
---@return integer #y
function methods:random()
-- Select a random hex from the hexes in the location set
-- This seems "inelegant", but I can't come up with another way without creating an extra array
-- Return -1, -1 if empty
local r = mathx.random(self:size())
local i, xr, yr = 1, -1, -1
self:iter( function(x, y, v)
if (i == r) then xr, yr = x, y end
i = i + 1
end)
return xr, yr
end
---Create a new empty location set.
---@return location_set
function location_set.create()
return setmetatable({ values = {} }, locset_meta)
end
---Create a location set from raw indexed data.
---The data must be formatted the same way as the values of a location set.
---@param data table<integer, any>
---@return location_set
function location_set.of_raw(data)
return setmetatable({ values = data }, locset_meta)
end
---Create a set with locations from an array.
---If the location tables have extra information in them, it will be used as the value.
---@param t location[]
---@return location_set
function location_set.of_pairs(t)
local s = location_set.create()
s:of_pairs(t)
return s
end
---Create a set with locations from a WML variable.
---If the location tables have extra information in them, it will be used as the value.
---@param name string
---@return location_set
function location_set.of_wml_var(name)
local s = location_set.create()
s:of_wml_var(name)
return s
end
---Create a set with locations from an array.
---The third element of each location is used as the value.
---@param t location_triple[]
---@return location_set
function location_set.of_triples(t)
local s = location_set.create()
s:of_triples(t)
return s
end
--- Create a set from a table of location->element mappings
--- Keys can be of the form {x,y} or {x=x,y=y}, or a location-like object such as a unit
---@param t table<location, any>
---@return location_set
function location_set.of_map(t)
local s = location_set.create()
s:of_map(t)
return s
end
---Create a set with locations from a shroud data string.
---Each location indicated by a 1 will be added to the set.
---@param data string
---@return location_set
function location_set.of_shroud_data(data)
local s = location_set.create()
s:of_shroud_data(data)
return s
end
return location_set