1. We have added a Gift Upgrades feature that allows you to gift an account upgrade to another member, just in time for the holiday season. You can see the gift option when going to the Account Upgrades screen, or on any user profile screen.
    Dismiss Notice

FractalWorld:DetermineXShift() Issues

Discussion in 'Civ5 - Creation & Customization' started by Barathor, Dec 29, 2012.

  1. Barathor

    Barathor Emperor

    Joined:
    May 7, 2011
    Messages:
    1,202
    I’ve posted about this issue before a while back: http://forums.civfanatics.com/showthread.php?t=470720

    I’ve been getting a little more comfortable with Lua and I’ve recently been trying to adjust and correct this issue within a map script of mine. Shifting along the Y axis seems to work just fine; the land masses are usually centered (unless it's Pangaea, then I believe it's intentionally off center for more varied terrain instead of always having one big continent in the middle with jungle cutting it in half). It's the shifting along the X axis that has problems.

    Here’s the existing function:
    Spoiler :
    Code:
    function FractalWorld:DetermineXShift()
    	--[[ This function will align the most water-heavy vertical portion of the map with the 
    	vertical map edge. This is a form of centering the landmasses, but it emphasizes the
    	edge not the middle. If there are columns completely empty of land, these will tend to
    	be chosen as the new map edge, but it is possible for a narrow column between two large 
    	continents to be passed over in favor of the thinnest section of a continent, because
    	the operation looks at a group of columns not just a single column, then picks the 
    	center of the most water heavy group of columns to be the new vertical map edge. ]]--
    
    	-- First loop through the map columns and record land plots in each column.
    	local land_totals = {};
    	for x = 0, self.iNumPlotsX - 1 do
    		local current_column = 0;
    		for y = 0, self.iNumPlotsY - 1 do
    			local i = y * self.iNumPlotsX + x + 1;
    			if (self.plotTypes[i] ~= PlotTypes.PLOT_OCEAN) then
    				current_column = current_column + 1;
    			end
    		end
    		table.insert(land_totals, current_column);
    	end
    	
    	-- Now evaluate column groups, each record applying to the center column of the group.
    	local column_groups = {};
    	-- Determine the group size in relation to map width.
    	local group_radius = math.floor(self.iNumPlotsX / 10);
    	-- Measure the groups.
    	for column_index = 1, self.iNumPlotsX do
    		local current_group_total = 0;
    		for current_column = column_index - group_radius, column_index + group_radius do
    			local current_index = current_column % self.iNumPlotsX;
    			if current_index == 0 then -- Modulo of the last column will be zero; this repairs the issue.
    				current_index = self.iNumPlotsX;
    			end
    			current_group_total = current_group_total + land_totals[current_index];
    		end
    		table.insert(column_groups, current_group_total);
    	end
    	
    	-- Identify the group with the least amount of land in it.
    	local best_value = self.iNumPlotsY * (2 * group_radius + 1); -- Set initial value to max possible.
    	local best_group = 1; -- Set initial best group as current map edge.
    	for column_index, group_land_plots in ipairs(column_groups) do
    		if group_land_plots < best_value then
    			best_value = group_land_plots;
    			best_group = column_index;
    		end
    	end
    	
    	-- Determine X Shift
    	local x_shift = best_group - 1;
    	return x_shift;
    end
    


    I've tried different modifications to this function within my script, but none of them seem to work.

    1) local group_radius = math.floor(self.iNumPlotsX / 10);
    The default divisor is 10, for a total "group" column width of 20% the length of a map (8 + 8 = 16 tiles on a Standard 80 width map). This "should" work fine with maps like Continents with large spans of ocean, because if you took smaller group samples, your edge would favor the left and not really be centered. But, on scripts with many continents and snaky shapes, taking a sample this large will usually fail.

    So, I tried using larger divisors (25+) so that the group column is thinner, but still no success. There were still some maps being shifted incorrectly when there was a clear column to base the edge off of.

    2) I tried simply setting group_radius equal to 0 so that only a single column is used. Yes, this would create edges that favored the left side and are not centered, but I wanted to test it anyway. Still, even with this there were times that the edge sliced through land when there were visible columns of only ocean.

    Though, I was a little wary of this method because I'm not sure if it messes with the "For" loop if I set group_radius = 0. "for current_column = column_index - group_radius, column_index + group_radius do" would basically be: "for current_column = column_index, column_index do" -- would that even run the loop if the start and end value are the same?

    3) I then tried setting group_radius to a hard value of simply 1. This way, each group column would be the width of 3 columns. Still no luck.

    4) I tried keeping the default loop and group radius, but edited the last part of the function which identifies the group with the least amount of land in it. I tried adding in an additional check which not only chooses the center column of the group with the least amount of land in it, but also checks to make sure the center column equals zero land. There were still times where it didn't work. I then tried a very low value check of <= math.ceil(self.iNumPlotsY / 20) in case of islands or small strips of land crossing through and still no luck.

    Here's a simple diagram which shows part of the problem (using squares for clarity and only a group radius of 4) which is also described in the topmost comment of the function itself. One would think the obvious choice to base the edge on would be within Group A. But, the way the land tiles are totaled in groups, that isn't the case.



    Here are some example of the script being ran. I photoshopped how the minimap should look and where the shifted edge should be and also the group column the script chose, which would be 10% of the map width on either side of the column line. Lands within the group are highlighted.









    Examples A and B get screwed up because the places where it should shift the edge to are narrow columns of water with long strips of land running from north to south. C and D work out well because the water is a little wider and the land is broken-up more instead of being long walls of land on either side.

    Is there a way to rewrite this function so that it accurately shifts the map for these types of map scripts?

    Also, is there some kind of error in this function that could be causing problems sometimes (outside of the problem displayed in the diagram)? Because, even the examples in my older post a while back were shifted incorrectly and they had VERY clear ways of doing so: http://forums.civfanatics.com/showthread.php?t=470720 I'm not sure what the problem is with those; something's not right.
     
  2. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,407
    Location:
    Near Portsmouth, UK
    The algorithm is working as described, in that it is looking for the least amount of land in a group, and not for a column of mainly water.

    So the problem is with the algorithm. Try the following which weights the land in each column by how far it is from the centre. Land in the columns at the edge of the "radius" are given a weight of 1, whereas the centre column is given a weight of (radius+1)

    Code:
    	-- Measure the groups.
    	for column_index = 1, self.iNumPlotsX do
    		local current_group_total = 0;
    		[COLOR="red"][B]-- loop from -radius to +radius as we need the distance from the centre as a weighting value[/B][/COLOR]
    		[B][COLOR="red"]for offset = -group_radius, group_radius do[/COLOR][/B]
    			[COLOR="red"][B]-- calculate the column we are working with (was the old loop control value)[/B][/COLOR]
    			[B][COLOR="red"]local current_column = column_index + offset[/COLOR][/B]
    			local current_index = current_column % self.iNumPlotsX;
    			if current_index == 0 then -- Modulo of the last column will be zero; this repairs the issue.
    				current_index = self.iNumPlotsX;
    			end
    			[COLOR="red"][B]-- add the land in this column but weight it by the distance it is from the centre[/B][/COLOR]
    			current_group_total = current_group_total + [COLOR="red"][B](land_totals[current_index] * (group_radius - math.abs(offset) + 1)[/B][/COLOR];
    		end
    		table.insert(column_groups, current_group_total);
    	end
    	
    	-- Identify the group with the least amount of land in it.
    	local best_value = self.iNumPlotsY * (2 * group_radius + 1) [COLOR="Red"][B]* (2 * (group_radius + 1))[/B][/COLOR]; -- Set initial value to max possible [COLOR="red"][B](allowing for weighting the columns)[/B][/COLOR]
    	local best_group = 1; -- Set initial best group as current map edge.
    
     
  3. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,407
    Location:
    Near Portsmouth, UK
    Another approach would be to pre-filter the centre columns for consideration, only looking at the ones with the least land in them.

    However, that may give weird results for single tile islands in the middle of a large mass of water, so I'd probably consider all the columns with the least land and also those with (least land + delta) - where delta is choosen between 2 and 5 - and then combine that with the weighted land algorithm above

    [[Imagine two continents, approx 1/3 of the map wide, seperated from each other by a narrow strait of water 2 to 4 tiles wide and an ocean in the other 1/3 of the map. Now join the continents across the strait by a land-bridge one tile high, and place lots of small (1-3 tile) islands in the ocean such that there are at least 2 land tiles in each column of the ocean. Personally I'd expect the edges to be in the ocean and not down the strait, hence the need for a delta]]
     
  4. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,407
    Location:
    Near Portsmouth, UK
    Untested, but something like this

    Code:
    function FractalWorld:DetermineXShift()
    	--[[ This function will align the most water-heavy vertical portion of the map with the 
    	vertical map edge. This is a form of centering the landmasses, but it emphasizes the
    	edge not the middle. If there are columns completely empty of land, these will tend to
    	be chosen as the new map edge, but it is possible for a narrow column between two large 
    	continents to be passed over in favor of the thinnest section of a continent, because
    	the operation looks at a group of columns not just a single column, then picks the 
    	center of the most water heavy group of columns to be the new vertical map edge. ]]--
    
    	-- First loop through the map columns and record land plots in each column.
    	local delta = 4; -- Difference from least_land that we are willing to consider columns for
    	local least_land = self.iNumPlotsY;
    	local land_totals = {};
    	for x = 0, self.iNumPlotsX - 1 do
    		local current_column = 0;
    		for y = 0, self.iNumPlotsY - 1 do
    			local i = y * self.iNumPlotsX + x + 1;
    			if (self.plotTypes[i] ~= PlotTypes.PLOT_OCEAN) then
    				current_column = current_column + 1;
    			end
    		end
    		table.insert(land_totals, current_column);
    		
    		if (current_column < least_land) then
    			least_land = current_column;
    		end
    	end
    	
    	-- Loop all the columns and only keep the best ones
    	local column_consider = {};
    	for column_index = 1, self.iNumPlotsX do
    		column_consider[column_index] = (land_totals[column_index] < (least_land+delta));
    	end
    	
    	-- Now evaluate column groups, each record applying to the center column of the group.
    	local column_groups = {};
    	-- Determine the group size in relation to map width.
    	local group_radius = math.floor(self.iNumPlotsX / 10);
    	-- Measure the groups.
    	for column_index = 1, self.iNumPlotsX do
    		if (column_consider[column_index]) then
    			local current_group_total = 0;
    			-- loop from -radius to +radius as we need the distance from the centre as a weighting value
    			for offset = -group_radius, group_radius do
    				-- calculate the column we are working with (was the old loop control value)
    				local current_column = column_index + offset
    				local current_index = current_column % self.iNumPlotsX;
    				if current_index == 0 then -- Modulo of the last column will be zero; this repairs the issue.
    					current_index = self.iNumPlotsX;
    				end
    				-- add the land in this column but weight it by the distance it is from the centre
    				current_group_total = current_group_total + (land_totals[current_index] * (group_radius - math.abs(offset) + 1));
    			end
    			table.insert(column_groups, current_group_total);
    		else
    			table.insert(column_groups, -1);
    		end
    	end
    	
    	-- Identify the group with the least amount of land in it.
    	local best_value = self.iNumPlotsY * (2 * group_radius + 1) * (2 * (group_radius + 1)); -- Set initial value to max possible (allowing for weighting the columns)
    	local best_group = 1; -- Set initial best group as current map edge.
    	for column_index, group_land_plots in ipairs(column_groups) do
    		if (column_consider[column_index] and group_land_plots < best_value) then
    			best_value = group_land_plots;
    			best_group = column_index;
    		end
    	end
    	
    	-- Determine X Shift
    	local x_shift = best_group - 1;
    	return x_shift;
    end
    
    Edit: Corrected the bug as noted in post #6 below
     
  5. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,407
    Location:
    Near Portsmouth, UK
    Lua is a BASIC/FORTRAN like langauge and executes loops from start to end INCLUSIVE (so for i=5,5,1 do executes for i=5 only), unlike the practice in Pascal/C/C++/Java where loops tend to execute from start to end-1 inclusive (but only from convention of how they are coded - mainly due to arrays starting at 0, so you don't want to process array[length] as the last item is at array[length-1])
     
  6. Barathor

    Barathor Emperor

    Joined:
    May 7, 2011
    Messages:
    1,202
    Wow, again, this is awesome; thank you! :goodjob: Your explanation sounds solid. Adding less weight to the columns as they distance themselves from the main center is a really good idea.

    Alas, though, I generated a bunch of maps real quick and they're still not shifting properly. I then tried lowering the group radius to half by raising the divisor to 20 since these maps are so filled with snaky continents, and still no luck. I'll try running some tests with the default map scripts too.

    If you test this out as well, to save you a little time from checking the debug logs, just add an extra ")" here:

    Code:
    -- add the land in this column but weight it by the distance it is from the centre
    				current_group_total = current_group_total + (land_totals[current_index] * (group_radius - math.abs(offset) + 1)[B][COLOR="Red"])[/COLOR][/B];
    
     
  7. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,407
    Location:
    Near Portsmouth, UK
    Good grief! You expect me to test it as well as write it! :mischief:
     
  8. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,407
    Location:
    Near Portsmouth, UK
    Which script are you using, as I'm getting sensible shifts for continents, small continents and tiny islands based scripts
     
  9. Barathor

    Barathor Emperor

    Joined:
    May 7, 2011
    Messages:
    1,202
    :lol:

    It's a slightly modified Small Continents script. Darn, I knew I should've thrown the file up here before leaving; I won't be back home until Tuesday. :(

    The only modifications to it that I think would matter with regards to this test is the smaller amount of ice that I have generating on it and, most importantly, the much lower water percentage of 60%.

    Once I'm home, I'll run some more tests and let you know what happens. I haven't tried it yet on the default scripts, but I'm glad it's working on those. The problem is even worse on those maps, like Continents, because the map just looks awful when it doesn't shift right. For my custom script, it's at least not as bad since the land is snaking all over the place.

    --------

    I have a question, though. The method I'm using is copying the whole function into the map script so that it's overridden. This is okay for this one, right? There's no special case involved with this function where it needs to remain in a copy of the whole Fractal World Lua file? All the other functions I've copied into the script seem to work fine. Just wanted to make sure.
     
  10. Barathor

    Barathor Emperor

    Joined:
    May 7, 2011
    Messages:
    1,202
    Happy New Year, Mr. Howard (and CFC)!

    Well, I performed some more tests and I'm still not having any luck. :( There has to be something going on in the default code which isn't working correctly.

    I then went back to the default function and added a bunch of print commands to it so I can debug it and observe the value of things. I also disabled the middle part with the loop that evaluates the groups and totals them up to keep things very simple. I then edited the last loop which identifies the one with the least amount of land in it by changing best_value to simply self.iNumPlotsY and using ipairs(land_totals) in the loop so that in uses the first table instead with individual column land totals.

    According to the output numbers, it's correctly identifying the single column with the least amount of land in it (Well, the first one it comes across with the least since it's a < instead of a <=). It's also (by the numbers - not visually in game) shifting the correct amount of columns to place that column at the map's edge.

    It would say that it shifted to a single column equal to 0 land (and adjacent columns would be very low in land as well) and when I look at the minimap there's a large continent running through the edge! :crazyeye: Remember, I removed the group totals and these are just single column totals. Something doesn't seem right.

    Oh, I also tried things by adding in a nice rift to guarantee that there's a clear gap of ocean within the fractal of land by adding a rift_grain = 3 to the call of FractalWorld:InitFractal(). I hoped it would make the shifting easier to perform, but it didn't help and the visible rift would just be anywhere "but" the edge -- when it didn't shift properly.

    --------------------------------------------------------

    1) How does the game analyze columns since the hexes are oriented with vertices pointing up? I assumed it does a zigzag NE>NW>NE... or SE>SW>SE...

    2) Would there happen to be any problems in this function?

    Code:
    function FractalWorld:ShiftPlotTypesBy(xshift, yshift)
    	if(xshift > 0 or yshift > 0) then
    		local iWH = self.iNumPlotsX * self.iNumPlotsY
    		local buf = {};
    		for i = 1, iWH + 1 do
    			buf[i] = self.plotTypes[i];
    		end
    		
    		[COLOR="Red"]for iDestY = 0, self.iNumPlotsY do
    			for iDestX = 0, self.iNumPlotsX do[/COLOR]
    				local iDestI = self.iNumPlotsX * iDestY + iDestX;
    				local iSourceX = (iDestX + xshift) % self.iNumPlotsX;
    				local iSourceY = (iDestY + yshift) % self.iNumPlotsY;
    				
    				local iSourceI = self.iNumPlotsX * iSourceY + iSourceX
    				self.plotTypes[iDestI] = buf[iSourceI]
    			end
    		end
    	end
    end
    
    Should the highlighted parts in red be changed to the code below? I have trouble following the code because I'm not very experienced with Lua, so I just added print commands to observe values again. iWH had the correct value of total hexes present on the map. Though, the DestY / DestX loop runs past the total amount of tiles on the map and adds an additional amount equal to an extra row of tiles.

    I just want to make sure this is intentional. It could be that it needs that extra row as it's shifting tiles by switching values between tables -- but, I don't know.

    Code:
    	
    [COLOR="red"]		for iDestY = 0, self.iNumPlotsY [B][COLOR="blue"]- 1[/COLOR][/B] do
    			for iDestX = 0, self.iNumPlotsX [B][COLOR="blue"]- 1[/COLOR][/B] do[/COLOR]
    
    
    :hammer2:
     
  11. whoward69

    whoward69 DLL Minion

    Joined:
    May 30, 2011
    Messages:
    8,407
    Location:
    Near Portsmouth, UK
    Would now be a good time to point out that the mini-map is always centred on the players start position?

    You can only test the shift code in World Builder
     
  12. Barathor

    Barathor Emperor

    Joined:
    May 7, 2011
    Messages:
    1,202
    lol, yes, I know about that. (... or do I? hmm)

    I know when you start a regular game, the minimap is centered on your initial revealed section of the map and zooms-out as you reveal more until the map is at its full size.

    The method I've been using to test things out is using Firetuner and the "Reveal Terrain" button within the "Map" tab. It reveals the entire map and my start position, along with the blinking indicator, which are not centered on the minimap (unless it actually "is" in a central location of the map).

    So, is this representing the minimap inaccurately? :eek:

    Also, just now, I saved the game and then I reloaded it and the map was the same.

    I also saved the map itself and loaded it into Worldbuilder and it was, again, the same. On a side note, I've never actually used Worldbuilder since I prefer random scripts -- but it's an awesome way to get a full, zoomed-out analysis of a map! I'll be utilizing this more for other things. :D It also displays the map edge without wrapping, which is neat.

    Oh well, I think I'm about to give up on this. It's only a small peeve of mine that I can easily live with. It's not worth all the trouble, and I guess it doesn't really trouble anybody else either. I appreciate the help, whoward. If you're in need of any graphics for your projects, please let me try to return the favor. :goodjob:
     
  13. Barathor

    Barathor Emperor

    Joined:
    May 7, 2011
    Messages:
    1,202
    Success!! :D

    I used your function to generate more maps and saved half a dozen of them whose minimap looked off after revealing the entire map. I opened each one in World Builder and every one of them had perfect shifts! You were right about testing the shift code in WB!

    The only tweak I made for those tests was raising the divisor to 20 for a group radius of half the size, since the areas of water tend to be smaller/narrower on this script. For other scripts like Continents, or ones with higher water percentages, 10 is probably ideal.

    (The single test I did in the post above was using 10. It may have looked slightly off to me, but I suppose it really was the best edge to shift to when using that radius. I guess it displayed correctly on both the minimap reveal and in World Builder and deceived me.)

    Also, I created a few new games and chose the maps I saved as the ones to play on. This time, when I revealed the maps with Firetuner, they were shifted correctly on the minimap.

    So, I guess the minimap doesn't respond well to revealing the entire map on a newly generated script, but a static one will reveal correctly. :crazyeye: Hopefully, it displays the edge correctly through the course of a normal game as terrain is revealed.
     
  14. biship

    biship Warlord

    Joined:
    Dec 7, 2003
    Messages:
    186
    Which map script is this going into may I ask? :)
     

Share This Page