whoward69
DLL Minion
The problem
There are many situations where we want "all the plots at distance r from a centre plot". Because of anomalies in the co-ordinate system used by plots on a Civ 5 Map, all of the solutions I've seen to this use scanning techniques to locate actual plots from within a larger set of possible plots - these methods are inefficient and return unordered lists. If you want "the next plot on the border" they quickly become unwieldy.
What we need is a Lua iterator that will efficiently return plots in order around the perimeter.
The Theory
In a rectangular based co-ordinate system, if we want to "walk" the perimeter of a box of "radius" r centered on (x, y) we need to traverse each side of the bounding square in turn, so we need to traverse the points (x-r, y-r) to (x-r, y+r) to (x+r, y+r) to (x+r, y-r) and finally back to (x-r, y-r). For each leg of the journey we keep either x or y constant and vary the other from -r to +r (or vice-versa)
For our Civ 5 hex based map, we need to traverse each side of the bounding hexagon
In trying to keep the Civ 5 map co-ordinate system "simple" by making it look like a rectangular grid, the developers introduced unnecessary complexity - anyone who has tried to iterate over plots on the map will know that the code needs to behave differently for odd and even values of y, and trying to (manually) calculate the distance between two plots is non-trivial (but fortunately there is a Map function for doing this)
If we can use a true hex based co-ordinate system, coding becomes much cleaner as we no longer have to allow for odd/even values of y. Fortunately (at least) one developer was of the same opinion, as we have ToHexFromGrid() and ToGridFromHex() methods, so we can convert our "Map co-ordinate" to a true hex based co-ordinate (with ToHexFromGrid()), work with it and then convert our result(s) back to "Map co-ordinates" (with ToGridFromHex())
Hex based co-ordinate systems have three axis - x, y and z - and have the property that for any (x, y, z) co-ordinate x+y+z is always equal to zero, which means we don't need to calculate or store one of the values (typically z) as it can always be calculated from the other two values, eg z = -(x+y)
To "walk" the edges of a bounding hexagon on a (true) hex based co-ordinate system we need to traverse the points as follows
When coding this we need to bear in mind that if we traverse each side from start to finish we will include each corner twice, so we need to vary from 1 to r (or 0 to r-1)
We could calculate and store all the plots on the perimeter, but we can use Lua coroutines and closures to create an iterator function that will return one plot each time it is called
The scary code!
The above code is a simplified version of the actual iterator (see end of post), which is capable of iterating either clockwise or anticlockwise - default is clockwise as above - and can start in any of the six sectors (N, NE, SE, S, SW, NW) - default is North as above.
Don't worry that it looks complex, as the good news is you don't need to understand it to use it!
As the Lua manual says "This is a common situation with iterators: They may be difficult to write, but are easy to use."
With a working "ring iterator" it's a straightforward task to write an "area iterator" - we just need to iterate each plot in each ring in turn
More scary code!
The above code is a simplified version of the actual iterator (see end of post), which, like the border iterator, is capable of iterating either clockwise or anticlockwise and can start in any of the six sectors, as well as iterating either outwards or inwards - the default is outwards as above.
Once again, don't worry that it looks complex, as the good news is you don't need to understand it to use it!
The plots are returned in order spiraling outwards from the centre, which if we just want all the plots within the area will suffice, however, sometimes we want a "radar sweep" effect, where we get all the points on a radius before moving on to any others. We can write such a "radar sweep" iterator with our ring iterator.
Yet more scary code!
The above code is a simplified version of the actual iterator (see end of post), which, like the spiral iterator, is capable of iterating either clockwise or anticlockwise, starting in any of the six sectors, and iterating either outwards or inwards.
Once again, don't worry that it looks complex, as the good news is you don't need to understand it to use it!
Some Full Examples
PlotRingIterator(pBabylon, 4, SECTOR_NORTH, DIRECTION_CLOCKWISE)

PlotRingIterator(pBabylon, 4, SECTOR_SOUTHWEST, DIRECTION_ANTICLOCKWISE)

PlotAreaSpiralIterator(pBabylon, 4, SECTOR_NORTH, DIRECTION_CLOCKWISE, DIRECTION_OUTWARDS, CENTRE_EXCLUDE)

PlotAreaRadarIterator(pBabylon, 4, SECTOR_NORTH, DIRECTION_CLOCKWISE, DIRECTION_OUTWARDS, CENTRE_EXCLUDE)

The Full Code
This should be placed in a separate "PlotIterators.lua" file with VFS set to true and then included into your own code, eg
There are many situations where we want "all the plots at distance r from a centre plot". Because of anomalies in the co-ordinate system used by plots on a Civ 5 Map, all of the solutions I've seen to this use scanning techniques to locate actual plots from within a larger set of possible plots - these methods are inefficient and return unordered lists. If you want "the next plot on the border" they quickly become unwieldy.
What we need is a Lua iterator that will efficiently return plots in order around the perimeter.
The Theory
In a rectangular based co-ordinate system, if we want to "walk" the perimeter of a box of "radius" r centered on (x, y) we need to traverse each side of the bounding square in turn, so we need to traverse the points (x-r, y-r) to (x-r, y+r) to (x+r, y+r) to (x+r, y-r) and finally back to (x-r, y-r). For each leg of the journey we keep either x or y constant and vary the other from -r to +r (or vice-versa)
For our Civ 5 hex based map, we need to traverse each side of the bounding hexagon
In trying to keep the Civ 5 map co-ordinate system "simple" by making it look like a rectangular grid, the developers introduced unnecessary complexity - anyone who has tried to iterate over plots on the map will know that the code needs to behave differently for odd and even values of y, and trying to (manually) calculate the distance between two plots is non-trivial (but fortunately there is a Map function for doing this)
If we can use a true hex based co-ordinate system, coding becomes much cleaner as we no longer have to allow for odd/even values of y. Fortunately (at least) one developer was of the same opinion, as we have ToHexFromGrid() and ToGridFromHex() methods, so we can convert our "Map co-ordinate" to a true hex based co-ordinate (with ToHexFromGrid()), work with it and then convert our result(s) back to "Map co-ordinates" (with ToGridFromHex())
Hex based co-ordinate systems have three axis - x, y and z - and have the property that for any (x, y, z) co-ordinate x+y+z is always equal to zero, which means we don't need to calculate or store one of the values (typically z) as it can always be calculated from the other two values, eg z = -(x+y)
To "walk" the edges of a bounding hexagon on a (true) hex based co-ordinate system we need to traverse the points as follows
- along the North edge of the hex (x-r, y+r, z) to (x, y+r, z-r)
- along the North-East edge (x, y+r, z-r) to (x+r, y, z-r)
- along the South-East edge (x+r, y, z-r) to (x+r, y-r, z)
- along the South edge (x+r, y-r, z) to (x, y-r, z+r)
- along the South-West edge (x, y-r, z+r) to (x-r, y, z+r)
- along the North-West edge (x-r, y, z+r) to (x-r, y+r, z)
When coding this we need to bear in mind that if we traverse each side from start to finish we will include each corner twice, so we need to vary from 1 to r (or 0 to r-1)
We could calculate and store all the plots on the perimeter, but we can use Lua coroutines and closures to create an iterator function that will return one plot each time it is called
The scary code!
Spoiler :
Code:
function PlotRingIterator(pPlot, r)
local hex = ToHexFromGrid({x=pPlot:GetX(), y=pPlot:GetY()})
local x, y = hex.x, hex.y
local function north(x, y, r, i) return {x=x-r+i, y=y+r} end
local function northeast(x, y, r, i) return {x=x+i, y=y+r-i} end
local function southeast(x, y, r, i) return {x=x+r, y=y-i} end
local function south(x, y, r, i) return {x=x+r-i, y=y-r} end
local function southwest(x, y, r, i) return {x=x-i, y=y-r+i} end
local function northwest(x, y, r, i) return {x=x-r, y=y+i} end
local sides = {north, northeast, southeast, south, southwest, northwest}
-- This coroutine walks the edges of the hex centered on pPlot at radius r
local next = coroutine.create(function ()
for _, side in ipairs(sides) do
for i=0, r-1, 1 do
coroutine.yield(side(x, y, r, i))
end
end
return nil
end)
-- This function returns the next edge plot in the sequence, ignoring those that fall off the edges of the map
return function ()
local pEdgePlot = nil
local _, hex = coroutine.resume(next)
while (hex ~= nil and pEdgePlot == nil) do
pEdgePlot = Map.GetPlot(ToGridFromHex(hex.x, hex.y))
if (pEdgePlot == nil) then _, hex = coroutine.resume(next) end
end
return pEdgePlot
end
end
The above code is a simplified version of the actual iterator (see end of post), which is capable of iterating either clockwise or anticlockwise - default is clockwise as above - and can start in any of the six sectors (N, NE, SE, S, SW, NW) - default is North as above.
Don't worry that it looks complex, as the good news is you don't need to understand it to use it!
Code:
for pEdgePlot in PlotRingIterator(pPlot, r) do
print(pEdgePlot:GetX(), pEdgePlot:GetY())
end
As the Lua manual says "This is a common situation with iterators: They may be difficult to write, but are easy to use."
With a working "ring iterator" it's a straightforward task to write an "area iterator" - we just need to iterate each plot in each ring in turn
More scary code!
Spoiler :
Code:
function PlotAreaSpiralIterator(pPlot, r, centre)
local next = coroutine.create(function ()
if (centre) then
coroutine.yield(pPlot)
end
for i=1, r, 1 do
for pEdgePlot in PlotRingIterator(pPlot, i, sector, anticlock) do
coroutine.yield(pEdgePlot)
end
end
return nil
end)
-- This function returns the next plot in the sequence
return function ()
local _, pAreaPlot = coroutine.resume(next)
return pAreaPlot
end
end
The above code is a simplified version of the actual iterator (see end of post), which, like the border iterator, is capable of iterating either clockwise or anticlockwise and can start in any of the six sectors, as well as iterating either outwards or inwards - the default is outwards as above.
Once again, don't worry that it looks complex, as the good news is you don't need to understand it to use it!
Code:
for pAreaPlot in PlotAreaSpiralIterator(pPlot, r, centre) do
print(pEdgePlot:GetX(), pEdgePlot:GetY())
end
The plots are returned in order spiraling outwards from the centre, which if we just want all the plots within the area will suffice, however, sometimes we want a "radar sweep" effect, where we get all the points on a radius before moving on to any others. We can write such a "radar sweep" iterator with our ring iterator.
Yet more scary code!
Spoiler :
Code:
function PlotAreaSweepIterator(pPlot, r, centre)
local next = coroutine.create(function ()
if (centre) then
coroutine.yield(pPlot)
end
local iterators = {}
for i=1, r, 1 do
iterators[i] = PlotRingIterator(pPlot, i, sector, anticlock)
end
for s=1, 6, 1 do
for i=1, r, 1 do
for j=i, r, 1 do
coroutine.yield(iterators[j]())
end
end
end
return nil
end)
-- This function returns the next plot in the sequence
return function ()
local _, pAreaPlot = coroutine.resume(next)
return pAreaPlot
end
end
The above code is a simplified version of the actual iterator (see end of post), which, like the spiral iterator, is capable of iterating either clockwise or anticlockwise, starting in any of the six sectors, and iterating either outwards or inwards.
Once again, don't worry that it looks complex, as the good news is you don't need to understand it to use it!
Code:
for pAreaPlot in PlotAreaSweepIterator(pPlot, r, centre) do
print(pEdgePlot:GetX(), pEdgePlot:GetY())
end
Some Full Examples
PlotRingIterator(pBabylon, 4, SECTOR_NORTH, DIRECTION_CLOCKWISE)

PlotRingIterator(pBabylon, 4, SECTOR_SOUTHWEST, DIRECTION_ANTICLOCKWISE)

PlotAreaSpiralIterator(pBabylon, 4, SECTOR_NORTH, DIRECTION_CLOCKWISE, DIRECTION_OUTWARDS, CENTRE_EXCLUDE)

PlotAreaRadarIterator(pBabylon, 4, SECTOR_NORTH, DIRECTION_CLOCKWISE, DIRECTION_OUTWARDS, CENTRE_EXCLUDE)

The Full Code
This should be placed in a separate "PlotIterators.lua" file with VFS set to true and then included into your own code, eg
Code:
include("PlotIterators")
for pAreaPlot in PlotAreaSweepIterator(pPlot, r) do
print(pEdgePlot:GetX(), pEdgePlot:GetY())
end
Code:
--
-- Plot iterator functions
-- All functions create an iterator centered on pPlot starting in the specified sector and working in the given direction around the ring/area
-- PlotRingIterator(pPlot, r, sector, anticlock) returns each plot around a border of radius r
-- PlotAreaSpiralIterator(pPlot, r, sector, anticlock, inwards, centre) returns each plot within an area of radius r in concentric rings
-- PlotAreaSweepIterator(pPlot, r, sector, anticlock, inwards, centre) returns each plot within an area of radius r by radial
--
-- By converting the Map (x,y) co-ordinates to hex (x,y,z) co-ordinates we can simply walk the edges of the bounding hex
-- To walk an edge we keep one of x, y or z invariant, iterate the others from 0 to r-1 and r-1 to 0 (r-1 otherwise we include each vertex twice)
--
-- By using Lua coroutines we can keep the walking code (fairly) simple
--
-- Use these functions as follows
--
-- for pEdgePlot in PlotRingIterator(pPlot, r, sector, anticlock) do
-- print(pEdgePlot:GetX(), pEdgePlot:GetY())
-- end
--
-- for pAreaPlot in PlotAreaSpiralIterator(pPlot, r, sector, anticlock, inwards, centre) do
-- print(pAreaPlot:GetX(), pAreaPlot:GetY())
-- end
--
-- for pAreaPlot in PlotAreaSweepIterator(pPlot, r, sector, anticlock, inwards, centre) do
-- print(pAreaPlot:GetX(), pAreaPlot:GetY())
-- end
--
SECTOR_NORTH = 1
SECTOR_NORTHEAST = 2
SECTOR_SOUTHEAST = 3
SECTOR_SOUTH = 4
SECTOR_SOUTHWEST = 5
SECTOR_NORTHWEST = 6
DIRECTION_CLOCKWISE = false
DIRECTION_ANTICLOCKWISE = true
DIRECTION_OUTWARDS = false
DIRECTION_INWARDS = true
CENTRE_INCLUDE = true
CENTRE_EXCLUDE = false
function PlotRingIterator(pPlot, r, sector, anticlock)
--print(string.format("PlotRingIterator((%i, %i), r=%i, s=%i, d=%s)", pPlot:GetX(), pPlot:GetY(), r, (sector or SECTOR_NORTH), (anticlock and "rev" or "fwd")))
-- The important thing to remember with hex-coordinates is that x+y+z = 0
-- so we never actually need to store z as we can always calculate it as -(x+y)
-- See http://keekerdc.com/2011/03/hexagon-grids-coordinate-systems-and-distance-calculations/
if (pPlot ~= nil and r > 0) then
local hex = ToHexFromGrid({x=pPlot:GetX(), y=pPlot:GetY()})
local x, y = hex.x, hex.y
-- Along the North edge of the hex (x-r, y+r, z) to (x, y+r, z-r)
local function north(x, y, r, i) return {x=x-r+i, y=y+r} end
-- Along the North-East edge (x, y+r, z-r) to (x+r, y, z-r)
local function northeast(x, y, r, i) return {x=x+i, y=y+r-i} end
-- Along the South-East edge (x+r, y, z-r) to (x+r, y-r, z)
local function southeast(x, y, r, i) return {x=x+r, y=y-i} end
-- Along the South edge (x+r, y-r, z) to (x, y-r, z+r)
local function south(x, y, r, i) return {x=x+r-i, y=y-r} end
-- Along the South-West edge (x, y-r, z+r) to (x-r, y, z+r)
local function southwest(x, y, r, i) return {x=x-i, y=y-r+i} end
-- Along the North-West edge (x-r, y, z+r) to (x-r, y+r, z)
local function northwest(x, y, r, i) return {x=x-r, y=y+i} end
local side = {north, northeast, southeast, south, southwest, northwest}
if (sector) then
for i=(anticlock and 1 or 2), sector, 1 do
table.insert(side, table.remove(side, 1))
end
end
-- This coroutine walks the edges of the hex centered on pPlot at radius r
local next = coroutine.create(function ()
if (anticlock) then
for s=6, 1, -1 do
for i=r, 1, -1 do
coroutine.yield(side[s](x, y, r, i))
end
end
else
for s=1, 6, 1 do
for i=0, r-1, 1 do
coroutine.yield(side[s](x, y, r, i))
end
end
end
return nil
end)
-- This function returns the next edge plot in the sequence, ignoring those that fall off the edges of the map
return function ()
local pEdgePlot = nil
local success, hex = coroutine.resume(next)
--if (hex ~= nil) then print(string.format("hex(%i, %i, %i)", hex.x, hex.y, -1 * (hex.x+hex.y))) else print("hex(nil)") end
while (success and hex ~= nil and pEdgePlot == nil) do
pEdgePlot = Map.GetPlot(ToGridFromHex(hex.x, hex.y))
if (pEdgePlot == nil) then success, hex = coroutine.resume(next) end
end
return success and pEdgePlot or nil
end
else
-- Iterators have to return a function, so return a function that returns nil
return function () return nil end
end
end
function PlotAreaSpiralIterator(pPlot, r, sector, anticlock, inwards, centre)
--print(string.format("PlotAreaSpiralIterator((%i, %i), r=%i, s=%i, d=%s, w=%s, c=%s)", pPlot:GetX(), pPlot:GetY(), r, (sector or SECTOR_NORTH), (anticlock and "rev" or "fwd"), (inwards and "in" or "out"), (centre and "yes" or "no")))
-- This coroutine walks each ring in sequence
local next = coroutine.create(function ()
if (centre and not inwards) then
coroutine.yield(pPlot)
end
if (inwards) then
for i=r, 1, -1 do
for pEdgePlot in PlotRingIterator(pPlot, i, sector, anticlock) do
coroutine.yield(pEdgePlot)
end
end
else
for i=1, r, 1 do
for pEdgePlot in PlotRingIterator(pPlot, i, sector, anticlock) do
coroutine.yield(pEdgePlot)
end
end
end
if (centre and inwards) then
coroutine.yield(pPlot)
end
return nil
end)
-- This function returns the next plot in the sequence
return function ()
local success, pAreaPlot = coroutine.resume(next)
return success and pAreaPlot or nil
end
end
function PlotAreaSweepIterator(pPlot, r, sector, anticlock, inwards, centre)
print(string.format("PlotAreaSweepIterator((%i, %i), r=%i, s=%i, d=%s, w=%s, c=%s)", pPlot:GetX(), pPlot:GetY(), r, (sector or SECTOR_NORTH), (anticlock and "rev" or "fwd"), (inwards and "in" or "out"), (centre and "yes" or "no")))
-- This coroutine walks each radial in sequence
local next = coroutine.create(function ()
if (centre and not inwards) then
coroutine.yield(pPlot)
end
local iterators = {}
for i=1, r, 1 do
iterators[i] = PlotRingIterator(pPlot, i, sector, anticlock)
end
for s=1, 6, 1 do
-- In ring (n+1) there are (n+1)*6 or 6n + 6 plots,
-- so we need to call the (n+1)th iterator six more times than the nth, or once more per outer loop
-- eg for 3 rings we need a plot from ring 1, 2, 3, 2, 3, 3 repeated 6 times
if (inwards) then
for i=r, 1, -1 do
for j=r, i, -1 do
coroutine.yield(iterators[j]())
end
end
else
for i=1, r, 1 do
for j=i, r, 1 do
coroutine.yield(iterators[j]())
end
end
end
end
if (centre and inwards) then
coroutine.yield(pPlot)
end
return nil
end)
-- This function returns the next plot in the sequence
return function ()
local success, pAreaPlot = coroutine.resume(next)
return success and pAreaPlot or nil
end
end
--
-- Test functions
--
highlights = {
RED = {x=1.0, y=0.0, z=0.0, w=1.0},
GREEN = {x=0.0, y=1.0, z=0.0, w=1.0},
BLUE = {x=0.0, y=0.0, z=1.0, w=1.0},
CYAN = {x=0.0, y=1.0, z=1.0, w=1.0},
YELLOW = {x=1.0, y=1.0, z=0.0, w=1.0},
MAGENTA = {x=1.0, y=0.0, z=1.0, w=1.0},
BLACK = {x=0.5, y=0.5, z=0.5, w=1.0}
}
function TestPlotHighlight(pPlot, highlight)
print(pPlot:GetX(), pPlot:GetY())
if (highlight ~= nil) then
Events.SerialEventHexHighlight(ToHexFromGrid(Vector2(pPlot:GetX(), pPlot:GetY())), true, highlight)
end
end
function TestPlotRingIterator(pPlot, r, sector, anticlock, highlight)
for pEdgePlot in PlotRingIterator(pPlot, r, sector, anticlock) do
TestPlotHighlight(pEdgePlot, highlight)
end
end
function TestPlotAreaSpiralIterator(pPlot, r, sector, anticlock, inwards, centre, highlight)
for pAreaPlot in PlotAreaSpiralIterator(pPlot, r, sector, anticlock, inwards, centre) do
TestPlotHighlight(pAreaPlot, highlight)
end
end
function TestPlotAreaSweepIterator(pPlot, r, sector, anticlock, inwards, centre, highlight)
for pAreaPlot in PlotAreaSweepIterator(pPlot, r, sector, anticlock, inwards, centre) do
TestPlotHighlight(pAreaPlot, highlight)
end
end
-- TestPlotRingIterator(Players[0]:GetCapitalCity():Plot(), 4, SECTOR_NORTH, DIRECTION_CLOCKWISE, highlights.RED)
-- TestPlotAreaSpiralIterator(Players[0]:GetCapitalCity():Plot(), 3, SECTOR_SOUTH, DIRECTION_ANTICLOCKWISE, DIRECTION_OUTWARDS, CENTRE_INCLUDE)
-- TestPlotAreaSweepIterator(Players[0]:GetCapitalCity():Plot(), 3, SECTOR_SOUTH, DIRECTION_ANTICLOCKWISE, DIRECTION_OUTWARDS, CENTRE_INCLUDE)