PerfectWorld3

Alright, something is changing tiles that I have in my Land Table. Haven't figured out if it's happening before or after feature generation (most likely before). Should just have to add a check to make sure I'm dealing with land. A big part of why using the look-up tables is faster is you get to skip so many checks. I'm pretty sure the AddLakes() function is responsible... Stupid AddLakes()! :gripe:

I've gotten some of the issues with my Tectonics implementation worked out. Right now I'm working on preventing continents that span from pole to pole (and thereby cutting off circumnavigation via sea). I'm trying to write some fairly simple code to make a table that catalogs what plates are neighboring each other and I have no earthly idea what's going on... That's what happens when you take a 5 month break though. :crazyeye: On top of that, WB is bugging out after 7 or 8 maps generated and putting Old Faithful on every single tile! It doesn't stop the geyser spam unless I restart WB, so it's been pretty slow-going.

The good news is I now spawn a huge map (128x80) in ~12 seconds in WB. :D
 
Heh, saw the geyser bug in the sticky thread. Is that still in the July beta? I must exit just in time :D

Honestly, since replacing the lua compiler as you suggested, huge map roll time in WB is a complete non-issue. It varies slightly, but on my desktop it might take 6 seconds (I count real slow though :sad:). Now if it would just remember the damned settings between maps...

The only trouble I had briefly with LuaJIT (so far) was having to hunt for some syntax nitpicks it had with some of Gedemon's mods. He was quick to point out saveutils.lua though, which seems to have been the only culprit (in my case).

Keep it up with your tectonics project. Can't wait to see the results :)
 
I actually built a stop watch into the script. It operates on os.clock() so it's got a resolution of something like 30ms, but that's fine for what I'm using it for.

Back up to ~16 seconds; ensuring that flipping a plate won't blockade sea travel or create a super-continent takes a lot of ops. :dubious: I've got a lot done. Fixed the issue with features appearing on lakes. It was happening because of the default AddLakes() function so I just brought it in and re-tooled it to use my land table. Shaved about 300ms off of map generation and fixed a bug at the same time. :smoke:

The worse news is that my changes to landmass generation don't scale very well and about 1:4 Duel size maps (42x26 I think) crash with the "Endless loop in Lake siltification" error. Probably just gonna see if I can remove Duel Size maps as I'm really not intending to make a good tiny map in the first place. :deadhorse:
 
Here is a new version of Perfect World, based on the VEM (Vanilla Enhanced Mod by Thalassicus) version, which I called Perfect World 4.0.:)
I updated the file according to this thread, but did some other things as well:



It's awsome what you can do by tuning all those map constants.
My way to "reduce the stringy shapes" or "terrain stripes/lines" as I call them, is a bit more direct and not fully successful either. Hope you have fun with the additional custom options.

The mod doesn't work. I activate it in the menu but it doesn't come up in the world list.
 
The mod doesn't work. I activate it in the menu but it doesn't come up in the world list.

I personally just extracted his script with 7zip (.civ5mod files are just archives) and placed it at \Documents\My Games\Civ V(or whatever it is)\Maps\

Works fine installed that way. This is a common way of installing Map Scripts as it is generally more reliable than the mod browser (especially if you make alterations to the script since Steam may decide to re-download the mod whenever and that will overwrite your altered version) and it allows them to be used in Multiplayer.
 
Alright, I tried that version 4.11 and I think it's strange that it generates grass and forests along all rivers, even in deserts. This way there are no rivers like Egypt or Mesopotamia. Otherwise it's a good map.
 
I believe I have written the fastest spiral iterators known to CiV... I got my script running in ~9.6 seconds (in WorldBuilder, default lua.dll [not the JIT.dll, which drops the run time down to ~5.6 seconds]). I was running the script in ~16 seconds using Cephalo's spiral. I subbed in Plot_GetPlotsInCircleSimple() from the communitas script and cut it down to ~12.

I then looked at the results Plot_GetPlotsInCircleSimple() was returning and noticed it was making a rectangle, not a spiral! So, I set out to write a faster iterator that actually returned a spiral. I knew I'd need to simplify things, so I decided to avoid function calls, c++ method calls, etc. I managed to do it by working with index values and using simple arithmetic (add and subtract).

Even though my spiral iterator was already faster than Plot_GetPlotsInCircleSimple() I decided to try to write a second one (that's even faster) using a scan method constrained to the edges of our given spiral instead of actually scanning outwards from the center with a spiral! This proved to be even faster, though it returns the results in a different order than a true spiral iterator (more on that later).

Both functions expect an index value as an input (not (x,y)) and they both return a table of index values (instead of a table of tables of (x,y) coordinates). Working with one index value is faster than working with two coordinates (especially when it comes to storing and reading from the returned table). Both functions assume xWrap so if you're trying to adapt them to a script that doesn't have xWrap or has yWrap (the two are exclusive), good luck! In the vast majority of cases, you can find a way around needing (x,y) as there's already a Map.GetPlotByIndex() method as well as alternative ways to determine certain things. If you absolutely must have x and/or y, either calculate them from the index (x = i%W; y = (i-x)/W) or wait for me to post my next function which will make lookup tables for x and y at initialization and use that to "lookup" the coordinate(s) you need. :goodjob:

Without further adieu, I present to you GetSpiral() and GetCircle():

Meet, GetSpiral(). It returns a table that "grows" outward from the center. If you're using the key values of the returned table to determine distance from the center plot, this is the function to use (see my PlacePossibleOasis() from the file I posted previously in this thread for an example of this).

Edit: Added functionality for minimum radius, and changed the table store so that it inserts "-1" into the table for plots that would be off of the map. If you're calling this, you need to check that i ~= -1 before doing anything with [like running Map.GetPlotByIndex()]. If you don't do this check, it's likely to cause a crash. This change ensures that keys n-nn are exactly z tiles away from the center (without using a minRadius, 1 is the tile itself, 2-7 are adjacent, 8-19 are 2 tiles away, 20-37...).
Spoiler :
Code:
function GetSpiral(i,maxRadius,minRadius)
	local W,H = Map.GetGridSize()
	local WH = W*H
	local x = i%W
	local y = (i-x)/W
	local odd = y%2
	local tab ={}
	local first = true

	
	if minRadius == nil or minRadius == 0 then
		table.insert(tab,i)
		minRadius = 1
	end
	
	for r = minRadius, maxRadius, 1 do
		if first == true then
			--start r to the west on the first spiral
			if x-r > -1 then
				i = i-r
				x = x-r
			else
				i = i+(W-r)
				x = x+(W-r)
			end
			first = false
		else
			--go west 1 tile before the next spiral
			if x ~= 0 then
				i = i-1
				x = x-1
			else
				i = i+(W-1)
				x = W-1
			end
		end
		--Go r times to the NE
		for z=1,r,1 do
			if x ~= (W-1) or odd == 0 then
				i = i+W+odd
				x = x+odd
			else
				i = i + 1
				x = 0
			end
			
			--store the index value or -1 if the plot isn't on the map; flip odd
			if i > -1 and i < WH then table.insert(tab,i) else table.insert(tab,-1) end
			if odd == 0 then odd = 1 else odd = 0 end
		end
		--Go r times to the E
		for z=1,r,1 do
			if x ~= (W-1) then
				i = i+1
				x = x+1
			else
				i = i-(W-1)
				x = 0
			end
						
			--store the index value or -1 if the plot isn't on the map
			if i > -1 and i < WH then table.insert(tab,i) else table.insert(tab,-1) end
		end
		--Go r times to the SE
		for z=1,r,1 do
			if x ~= (W-1) or odd == 0 then
				i = i-W+odd
				x = x+odd
			else
				i = i-(2*W-1)
				x = 0
			end
						
			--store the index value or -1 if the plot isn't on the map; flip odd
			if i > -1 and i < WH then table.insert(tab,i) else table.insert(tab,-1) end
			if odd == 0 then odd = 1 else odd = 0 end
		end
		--Go r times to the SW
		for z=1,r,1 do
			if x ~= 0 or odd == 1 then
				i = i-W-1+odd
				x = x-1+odd
			else
				i = i-(W+1)
				x = (W-1)
			end
						
			--store the index value or -1 if the plot isn't on the map; flip odd
			if i > -1 and i < WH then table.insert(tab,i) else table.insert(tab,-1) end
			if odd == 0 then odd = 1 else odd = 0 end
		end
		--Go r times to the W
		for z = 1,r,1 do
			if x ~= 0 then
				i = i-1
				x=x-1
			else
				i = i+(W-1)
				x = (W-1)
			end
						
			--store the index value or -1 if the plot isn't on the map
			if i > -1 and i < WH then table.insert(tab,i) else table.insert(tab,-1) end
		end
		--Go r times to the NW!!!!!
		for z = 1,r,1 do
			if x ~= 0 or odd == 1 then
				i = i+W-1+odd
				x = x-1+odd
			else
				i = i+W+(W-1)
				x = W-1
			end
						
			--store the index value or -1 if the plot isn't on the map; flip odd
			if i > -1 and i < WH then table.insert(tab,i) else table.insert(tab,-1) end
			if odd == 0 then odd = 1 else odd = 0 end
		end
	end

	return tab
end

Meet, GetCircle() this one is the scan method so it returns a table with the plots sorted ascending by y and then (kindof) by x. Use this function any time that distance from the center doesn't matter. It's performance improvement is exaggerated by how big of a radius you're looking at so functions like Smooth() or Deviate() (which call this function with a pretty large radius once for every single plot on the map) see the most benefit.
Spoiler :
Code:
function GetCircle(i,radius)
	local W,H = Map.GetGridSize()
	local WH = W*H
	local x = i%W
	local xx = x
	local y = (i-xx)/W
	local odd = y%2
	local tab = {}
	local topY = radius
	local bottomY = radius
	local currentY = nil
	local len = 1+radius
	--constrain the top of our circle to be on the map
	if y+radius > H-1 then
		for r=0,radius,1 do
			if y+r == H-1 then
				topY = r
				break
			end
		end
	end
	--constrain the bottom of our circle to be on the map
	if y-radius < 0 then
		for r=0,radius,1 do
			if y-r == 0 then
				bottomY = r
				break
			end
		end
	end
	--adjust starting length, apply the top and bottom limits, and correct odd for the starting point
	len = len+(radius-bottomY)
	currentY = y - bottomY
	topY = y + topY
	odd = (odd+bottomY)%2
	--set starting point
	if x-(radius-bottomY)-math.floor((bottomY+odd)/2) < 0 then
		i = i-(W*bottomY)+(W-(radius-bottomY))-math.floor((bottomY+odd)/2)
		x = x+(W-(radius-bottomY))-math.floor((bottomY+odd)/2)
		-- print(string.format("i for (%d,%d) WOULD have been in outer space. x is (%d,%d) i is (%d)",xx,y,x,y-bottomY,i))
	else
		i = i-(W*bottomY)-(radius-bottomY)-math.floor((bottomY+odd)/2)
		x = x-(radius-bottomY)-math.floor((bottomY+odd)/2)
	end
	--cycle through the plot indexes and add them to a table
	--local str = ""
	--local iters = 0
	while currentY <= topY do
		--insert the start value, scan left to right adding each index in the line to our table
		--str = str..i..","
		table.insert(tab,i)
		local wrapped = false
		for n=1,len-1,1 do
			if x ~= (W-1) then
				i = i + 1
				x = x + 1
			else
				i = i-(W-1)
				x = 0
				wrapped = true
			end
			--str = str..i..","
			table.insert(tab,i)
		end
		if currentY < y then
			--move i NW and increment the length to scan
			if not wrapped then
				i = i+W-len+odd
				x = x-len+odd
			else
				i = i+W+(W-len+odd)
				x = x+(W-len+odd)
			end
			len = len+1
		else
			--move i NE and decrement the length to scan
			if not wrapped then
				i = i+W-len+1+odd
				x = x-len+1+odd
			else
				i = i+W+(W-len+1+odd)
				x = x+(W-len+1+odd)
			end
			len = len-1
		end
		currentY = currentY+1
		if odd == 0 then odd = 1 else odd = 0 end
		-- iters = iters+1
		-- if iters > 300 then
			-- print("infinite loop in GetCircle")
			-- break
		-- end
	end
	-- print(string.format("added "..str.." to table for circle starting at(%d,%d)",xx,y))
	return tab
end

Edit: P.S.: If anyone knows of a faster way of determining "odd" I'd love you forever for sharing :love: (even if it doesn't net any tangible performance difference)! "odd" is a value that determines whether or not the y coordinate we're currently at is even or odd. This value is then used to determine the "every-other-line" offset that affects x values in a hexagonal grid. I initially set odd by calculating y and then doing odd = y%2. I then continue tracking odd by running odd = (odd+1)%2 every time the y value I'm dealing with increments. This makes the modulus operator the only "complex" arithmetic in either function. I know with bitwise operators AND'ing any integer with 1 will tell you if it's even or odd; unfortunately, bitwise operators aren't built-in to lua and including a library that makes them available may affect the distributivity of my script.
 
I rewrote function floatMap:GetNeighbor() to take xWrap into consideration. This allows river pathing (and anything else that may use this function) to wrap properly and fixes a very rare crash related to river pathing (the script tries to analyze a plot that doesn't exist). Replace all of function floatMap:GetNeighbor() with the following code:

Spoiler :
PHP:
function FloatMap:GetNeighbor(x,y,dir)
	local xx
	local yy
	local odd = y % 2
	if dir == mc.C then
		return x,y
	elseif dir == mc.W then
		if x == 0 and self.wrapX then
			xx = self.width-1
			yy = y
		else
			xx = x - 1
			yy = y
		end
		return xx,yy
	elseif dir == mc.NW then
		if x == 0 and odd == 0 and self.wrapX then
			xx = self.width-1
			yy = y + 1
		else
			xx = x - 1 + odd
			yy = y + 1
		end
		return xx,yy
	elseif dir == mc.NE then
		if x == self.width-1 and odd == 1 and self.wrapX then
			xx = 0
			yy = y+1
		else
			xx = x + odd
			yy = y + 1
		end
		return xx,yy
	elseif dir == mc.E then
		if x == self.width-1 and self.wrapX then
			xx = 0
			yy = y
		else
			xx = x + 1
			yy = y
		end
		return xx,yy
	elseif dir == mc.SE then
		if x == self.width-1 and odd == 1 and self.wrapX then
			xx = 0
			yy = y - 1
		else
			xx = x + odd
			yy = y - 1
		end
		return xx,yy
	elseif dir == mc.SW then
		if x == 0 and odd == 0 and self.wrapX then
			xx = self.width - 1
			yy = y - 1
		else
			xx = x - 1 + odd
			yy = y - 1
		end
		return xx,yy
	else
		error("Bad direction in FloatMap:GetNeighbor")
	end
	return -1,-1
end

Edit: This may need some more testing, though I'll leave it here for now. I recall rivers usually appearing to be fine before I implemented Xshift. On decent sized maps (standard and larger) with a normal amount of land (less than 40% or so), river wrapping should almost never be an issue. If this does have an issue, I'd still prefer it's use over a version without wrapping so it would require changes to be made to river pathing.
 
So, one of the issues causing the map to crash during generation revolved around siltifying lakes. The function responsible for this relies on the sea level threshold. However, because of the way this threshold was getting set, it was possible to return a sea level threshold of 0.0000000. This resulted in an infinite loop (that errors out around 100,000 iterations iirc).

To fix this issue, I altered FloatMap:FindThresholdFromPercent() to return a mathematically calculated threshold whenever it's possible for the original method to return a value of zero. By default, only sea level could return zero, so only sea level should be changed. However, you should find that the results are extremely similar, if not identical, to the original method (with the exclusion of one possible source of a crash).

To fix this issue, I ended up just adding an extremely small constant to any threshold that could possibly return zero. The results are essentially identical though it appears to exclude the possibility of forcing an endless loop in SiltifyLakes(). Even when the threshold gets set to the constant (because it tried to set the threshold to zero), the map spawns beautifully. I'm pretty confident this issue stems from normilization, but if this constant that doesn't have any side effects fixes the issue, I see no need to rewrite something that's going to have major implications in everything the script does.

Replace FloatMap:FindThresholdFromPercent() with:
Spoiler :
Code:
function FloatMap:FindThresholdFromPercent(percent, greaterThan, excludeZeros)
	local mapList = {}
	local percentage = percent * 100
	local const = 0.0
	
	if not excludeZeros then
		const = 0.000000000000000001
	end
	
	if greaterThan then
		percentage = 100-percentage
	end

	if percentage >= 100 then
		return 1.01 --whole map
	elseif percentage <= 0 then
		return -0.01 --none of the map
	end
	
	for i=0,self.length-1,1 do
		if not (self.data[i] == 0.0 and excludeZeros) then
			table.insert(mapList,self.data[i])
		end
	end
	
	table.sort(mapList, function (a,b) return a < b end)
	local threshIndex = math.floor((#mapList * percentage)/100) 

	return mapList[threshIndex-1]+const
end

This will resolve one of the two possible crashes for SiltifyLakes(). The other crash revolves around the possibility for there to be lakes left over after the body of the function has run. I haven't really dug into why this happens nor do I have any plans to, ever. The lakes left behind are few and far between and I've yet to notice any problems with just commenting out the entire section of code that forces this crash. The offending code is everything after the "while" statement ends.

See here:
Spoiler :
Code:
function RiverMap:SiltifyLakes()
	local lakeList = {}
	local onQueueMapNorth = {}
	local onQueueMapSouth = {}
	local i = 0
	for y = 0,self.elevationMap.height - 1,1 do
		for x = 0,self.elevationMap.width - 1,1 do
			onQueueMapNorth[i] = false
			onQueueMapSouth[i] = false
			if self:isLake(self.riverData[i].northJunction) then
				Push(lakeList,self.riverData[i].northJunction)
				onQueueMapNorth[i] = true
			end
			if self:isLake(self.riverData[i].southJunction) then
				Push(lakeList,self.riverData[i].southJunction)
				onQueueMapSouth[i] = true
			end
			i=i+1
		end
	end

	local longestLakeList = #lakeList
	local shortestLakeList = #lakeList
	local iterations = 0
	local debugOn = false
	--print(string.format("initial lake count = %d",longestLakeList))
	while #lakeList > 0 do
		--print(string.format("length of lakeList = %d",#lakeList))
		iterations = iterations + 1
		if #lakeList > longestLakeList then
			longestLakeList = #lakeList
		end

		if #lakeList < shortestLakeList then
			shortestLakeList = #lakeList
			--print(string.format("shortest lake list = %d, iterations = %d",shortestLakeList,iterations))
			iterations = 0
		end

		if iterations > 50000 then
			debugOn = true
		end

		if iterations > 50300 then
			error(string.format("endless loop in lake siltification; check logs. seaLevelThreashold is %.20f",elevationMap.seaLevelThreshold))
		end

		local junction = Pop(lakeList)
		local i = self.elevationMap:GetIndex(junction.x,junction.y)
		if junction.isNorth then
			onQueueMapNorth[i] = false
		else
			onQueueMapSouth[i] = false
		end

		if debugOn then
			print(string.format("processing (%d,%d) N=%s alt=%f",junction.x,junction.y,tostring(junction.isNorth),junction.altitude))
		end

		local avgLowest = self:GetLowerNeighborAverage(junction)

		if debugOn then
			print(string.format("--avgLowest == %f",avgLowest))
		end

		if avgLowest < junction.altitude + 0.005 then --cant use == in fp comparison
			junction.altitude = avgLowest + 0.005
			if debugOn then
				print("--adding 0.005 to avgLowest")
			end
		else
			junction.altitude = avgLowest
		end

		if debugOn then
			print(string.format("--changing altitude to %f",junction.altitude))
		end

		for dir = mc.WESTFLOW,mc.VERTFLOW,1 do
			local neighbor = self:GetJunctionNeighbor(dir,junction)
			if debugOn and neighbor == nil then
				print(string.format("--nil neighbor at direction = %d",dir))
			end
			if neighbor ~= nil and self:isLake(neighbor) then
				local i = self.elevationMap:GetIndex(neighbor.x,neighbor.y)
				if neighbor.isNorth == true and onQueueMapNorth[i] == false then
					Push(lakeList,neighbor)
					onQueueMapNorth[i] = true
					if debugOn then
						print(string.format("--pushing (%d,%d) N=%s alt=%f",neighbor.x,neighbor.y,tostring(neighbor.isNorth),neighbor.altitude))
					end
				elseif neighbor.isNorth == false and onQueueMapSouth[i] == false then
					Push(lakeList,neighbor)
					onQueueMapSouth[i] = true
					if debugOn then
						print(string.format("--pushing (%d,%d) N=%s alt=%f",neighbor.x,neighbor.y,tostring(neighbor.isNorth),neighbor.altitude))
					end
				end
			end
		end
	end
	--print(string.format("longestLakeList = %d",longestLakeList))

	--print(string.format("sea level = %f",self.elevationMap.seaLevelThreshold))

[COLOR="SeaGreen"]--[[Commented out this section because it's debug code. -Bobert13
	local belowSeaLevelCount = 0
	local riverTest = FloatMap:New(self.elevationMap.width,self.elevationMap.height,self.elevationMap.xWrap,self.elevationMap.yWrap)
	local lakesFound = false
	for y = 0,self.elevationMap.height - 1,1 do
		for x = 0,self.elevationMap.width - 1,1 do
			local i = self.elevationMap:GetIndex(x,y)

			local northAltitude = self.riverData[i].northJunction.altitude
			local southAltitude = self.riverData[i].southJunction.altitude
			if northAltitude < self.elevationMap.seaLevelThreshold then
				belowSeaLevelCount = belowSeaLevelCount + 1
			end
			if southAltitude < self.elevationMap.seaLevelThreshold then
				belowSeaLevelCount = belowSeaLevelCount + 1
			end
			riverTest.data[i] = (northAltitude + southAltitude)/2.0

			if self:isLake(self.riverData[i].northJunction) then
				local junction = self.riverData[i].northJunction
				print(string.format("lake found at (%d, %d) isNorth = %s, altitude = %f!",junction.x,junction.y,tostring(junction.isNorth),junction.altitude))
				riverTest.data[i] = 1.0
				lakesFound = true
			end
			if self:isLake(self.riverData[i].southJunction) then
				local junction = self.riverData[i].southJunction
				print(string.format("lake found at (%d, %d) isNorth = %s, altitude = %f!",junction.x,junction.y,tostring(junction.isNorth),junction.altitude))
				riverTest.data[i] = 1.0
				lakesFound = true
			end
		end
	end

	if lakesFound then
		error("Failed to siltify lakes. check logs")
	end
	riverTest:Normalize()
]]-- -Bobert13[/COLOR]
--	riverTest:Save("riverTest.csv")
end

Edit: My mathematical threshold formula is no good for Perlin noise generated elevation maps. I'm working on a better formula for them (and hopefully it will work just as well for my tectonic generation). The reason why it's no good is normalization. With Perlin noise maps, there may be no high mountain spikes so when the elevation map gets normalized, the average height is higher than the current formula expects. Alternatively, if there is an extremely large height spike, the average becomes much lower than expected. With my tectonic generation, height spikes always happen within a controlled range so the formula works as intended every time.

Edit2: Turns out, no matter what I did, the idea of a mathematical approximation of (median*percent) was a bust due to statistical anomalies. I got a coefficient that could produce a threshold within a certain variance of the threshold pulled through the (median*percent) table operation. It was even decently accurate (give or take ~0.03 from a threshold value that typically ranged between 0.45 and 0.65 in ~90% of all tests) however, every now and then (I'd estimate around 2% of the time), it would be waaaaaaaaay off and cause a map that was either almost completely water or almost completely land.
 
I'm now generating a huge map in ~6 seconds (~2.5 with the JIT.dll)! :D

The first optimization is to PlacePossibleAtoll. It's really simple and cuts about 5 seconds off of map generation. :eek:

Replace:
Code:
local featureAtoll = nil
	for thisFeature in GameInfo.Features() do
		if thisFeature.Type == "FEATURE_ATOLL" then
			featureAtoll = thisFeature.ID;
		end
	end

with:
Code:
	local featureAtoll = GameInfo.Features.FEATURE_ATOLL.ID
That's it... 5 seconds right there. :lol:

The next optimization is regarding my favorite functions, FloatMap:Smooth() and FloatMap:Deviate(). However, the optimization is actually to the functions they call repeatedly; FloatMap:GetAverageInHex() and FloatMap:GetStdDevInHex(). The new versions expect an index value as a parameter instead of (x,y) coordinates. You can either track back from each function in the stack and alter the parameters to pass the index value of the plot along, or calculate the index from (x,y) with FloatMap:GetIndex() and remove my calculations for x,y from index.

I've optimized these two functions by inlining the spiral iterator and working with the values, as they come in, instead of, calling the spiral iterator (which stores the values to a table and the returns that table) and then working with the values in the table. This is literally getting rid of hundreds of thousands of table operations! It's no 5 second optimization or anything :lol:, but it is tangibly faster.

FloatMap:GetAverageInHex():
Spoiler :
Code:
function FloatMap:GetAverageInHex(i,radius)
	local W,H = Map.GetGridSize()
	local WH = W*H
	local x = i%W
	local y = (i-x)/W
	local odd = y%2
	local topY = radius
	local bottomY = radius
	local currentY = nil
	local len = 1+radius
	local avg = 0
	local count = 0
	
	--constrain the top of our circle to be on the map
	if y+radius > H-1 then
		for r=0,radius-1,1 do
			if y+r == H-1 then
				topY = r
				break
			end
		end
	end
	--constrain the bottom of our circle to be on the map
	if y-radius < 0 then
		for r=0,radius,1 do
			if y-r == 0 then
				bottomY = r
				break
			end
		end
	end
	
	--adjust starting length, apply the top and bottom limits, and correct odd for the starting point
	len = len+(radius-bottomY)
	currentY = y - bottomY
	topY = y + topY
	odd = (odd+bottomY)%2
	--set the starting point, the if statement checks for xWrap
	if x-(radius-bottomY)-math.floor((bottomY+odd)/2) < 0 then
		i = i-(W*bottomY)+(W-(radius-bottomY))-math.floor((bottomY+odd)/2)
		x = x+(W-(radius-bottomY))-math.floor((bottomY+odd)/2)
		-- print(string.format("i for (%d,%d) WOULD have been in outer space. x is (%d,%d) i is (%d)",xx,y,x,y-bottomY,i))
	else
		i = i-(W*bottomY)-(radius-bottomY)-math.floor((bottomY+odd)/2)
		x = x-(radius-bottomY)-math.floor((bottomY+odd)/2)
	end
	
	--cycle through the plot indexes and add them to a table
	while currentY <= topY do
		--insert the start value, scan left to right adding each index in the line to our table

		avg = avg+self.data[i]
		local wrapped = false
		for n=1,len-1,1 do
			if x ~= (W-1) then
				i = i + 1
				x = x + 1
			else
				i = i-(W-1)
				x = 0
				wrapped = true
			end
			avg = avg+self.data[i]
			count = count+1
		end
		
		if currentY < y then
			--move i NW and increment the length to scan
			if not wrapped then
				i = i+W-len+odd
				x = x-len+odd
			else
				i = i+W+(W-len+odd)
				x = x+(W-len+odd)
			end
			len = len+1
		else
			--move i NE and decrement the length to scan
			if not wrapped then
				i = i+W-len+1+odd
				x = x-len+1+odd
			else
				i = i+W+(W-len+1+odd)
				x = x+(W-len+1+odd)
			end
			len = len-1
		end
		
		currentY = currentY+1
		if odd == 0 then
			odd = 1
		else
			odd = 0
		end
	end

	avg = avg/count
	return avg
end

FloatMap:GetStdDevInHex():
Spoiler :
Code:
function FloatMap:GetStdDevInHex(i,radius)
	local W,H = Map.GetGridSize()
	local WH = W*H
	local x = i%W
	local y = (i-x)/W
	local odd = y%2
	local topY = radius
	local bottomY = radius
	local currentY = nil
	local len = 1+radius
	local avg = self:GetAverageInHex(i,radius)
	local deviation = 0
	local count = 0
	
	--constrain the top of our circle to be on the map
	if y+radius > H-1 then
		for r=0,radius-1,1 do
			if y+r == H-1 then
				topY = r
				break
			end
		end
	end
	--constrain the bottom of our circle to be on the map
	if y-radius < 0 then
		for r=0,radius,1 do
			if y-r == 0 then
				bottomY = r
				break
			end
		end
	end
	
	--adjust starting length, apply the top and bottom limits, and correct odd for the starting point
	len = len+(radius-bottomY)
	currentY = y - bottomY
	topY = y + topY
	odd = (odd+bottomY)%2
	--set the starting point, the if statement checks for xWrap
	if x-(radius-bottomY)-math.floor((bottomY+odd)/2) < 0 then
		i = i-(W*bottomY)+(W-(radius-bottomY))-math.floor((bottomY+odd)/2)
		x = x+(W-(radius-bottomY))-math.floor((bottomY+odd)/2)
	else
		i = i-(W*bottomY)-(radius-bottomY)-math.floor((bottomY+odd)/2)
		x = x-(radius-bottomY)-math.floor((bottomY+odd)/2)
	end
	
	--cycle through the plot indexes and add them to a table
	while currentY <= topY do
		--insert the start value, scan left to right adding each index in the line to our table
		
		local sqr = self.data[i] - avg
		deviation = deviation + (sqr * sqr)
		local wrapped = false
		for n=1,len-1,1 do
			if x ~= (W-1) then
				i = i + 1
				x = x + 1
			else
				i = i-(W-1)
				x = 0
				wrapped = true
			end
			
			local sqr = self.data[i] - avg
			deviation = deviation + (sqr * sqr)
			count = count+1
		end
		
		if currentY < y then
			--move i NW and increment the length to scan
			if not wrapped then
				i = i+W-len+odd
				x = x-len+odd
			else
				i = i+W+(W-len+odd)
				x = x+(W-len+odd)
			end
			len = len+1
		else
			--move i NE and decrement the length to scan
			if not wrapped then
				i = i+W-len+1+odd
				x = x-len+1+odd
			else
				i = i+W+(W-len+1+odd)
				x = x+(W-len+1+odd)
			end
			len = len-1
		end
		
		currentY = currentY+1
		if odd == 0 then
			odd = 1
		else
			odd = 0
		end
	end

	deviation = math.sqrt(deviation/count)
	return deviation
end
 
Attached you should find my updated version of PerfectWorld3. This is intended to look, play, and feel like PerfectWorld. The main constants have been reorganized and I added some notation, but all of the relevant ones are set to the default values.

ChangeLog:
-Implemented multiple fixes for "ghost tiles" and map seam issues, including Xshift.
-Improved Oasis Placement.
-Changed Marsh Placement.
-Fixed some issues with feature/terrain striping near the equator.
-Added optional terrain matching features.
-Prevented single-tile mountain islands.
-Implemented a multiplayer-ready random.
-Fixed an issue with threshold determination that sometimes caused an infinite loop.
-Removed a rare error status that forced a crash. (The expected result is a river that ends in the middle of a landmass, so if you find this and you can get me some information [particularly the random map seed which will require lua logging to be enabled] I'll look further into this. If you can't get me a random seed, but you have a configuration of mapConstants that produces this often, that information could also be helpful.)
-Implemented a vast number of optimizations. (I spawn a 128x80 map in ~8.5 seconds with this script, where it takes ~70 with the original).
-Probably 40 other things I can't recall right now. :p

This will probably end my support for PerfectWorld3 as I'll be working on my derivative that uses an alternative landmass generation method. If there's major bugs, or I ever figure out how to fix the very low possibility for rivers that can't path to a body of water, I'll be happy to do an update for those. Should Cephalo return to his script, he's more than welcome to use or discard any of the changes I've made for this version at his sole discretion.
 
Great work! And yes, it's exceptionally fast.

One caveat for anyone playing with this script and used to Perfectworld's abundance of rivers... you'll probably want to raise mconst.riverPercent. With the removal of most of the coastal "shorties" you can afford to raise this quite a bit from the default (.19) without generating too many fertile areas. That's my brief impression anyway from studying a bunch of maps in the WB.

edit: ...so I'll be playing with a higher value there and will keep my eye out for the pathing issue you mention, which should be more frequent (if it occurs at all)
 
Same speil as my last post. :lol:

Changelog:
-Minor fix to my Spiral iterator. I wasn't calling it with a minRadius, but if I would have, it wouldn't have returned proper values.
-Minor fix to GetAverageInHex and GetStdDevInHex. I wasn't including the first value of every scanned line after the first line.
-Refined my threshold determination to only add the constant if returning zero is a possibility and lowered the constant significantly.
-Changed the main constant "YtoXRatio" to calculate it's value based on the map's dimensions. Landmasses should appear less stringy and less "vertically-skewed".
-Fixed the possibility for rivers that can't path to a body of water! Cephalo was excluding the possibility that a given junction (read "corner of a hex") could still be a lake after SiltifyLakes() had adjusted it once.
-Lowered the value SiltifyLakes() adjusts by during each iteration. This takes a fraction of a second longer, but it should produce more refined results.

I haven't tested any mostly-land maps to see if my adjustments trigger the maximum iteration limit of SiltifyLakes() but hopefully, it shouldn't. Besides, if you're playing 95% land you probably picked the wrong mapscript. :D

I left the error status check for SiltifyLakes in, though I changed it to print a warning to the log and allow the map to spawn anyways. After god-knows how many spawns, since I added a check during the body that makes sure the junction adjusted isn't still a lake, I haven't seen the error message once.
 
Great work! And yes, it's exceptionally fast.

One caveat for anyone playing with this script and used to Perfectworld's abundance of rivers... you'll probably want to raise mconst.riverPercent. With the removal of most of the coastal "shorties" you can afford to raise this quite a bit from the default (.19) without generating too many fertile areas. That's my brief impression anyway from studying a bunch of maps in the WB.

edit: ...so I'll be playing with a higher value there and will keep my eye out for the pathing issue you mention, which should be more frequent (if it occurs at all)

I actually had a problem with meeting a quota for number of lakes spawned using the default value because it couldn't find any viable plots due to the rivers (and deserts; I exclude lakes from spawning in deserts because that's what oases are for). :lol: I had to put a break into my loop after so many attempts to prevent an infinite loop.
 
Turns out my fix for SiltifyLakes() works, but not because it's check for the possibility that the junction that just got altered is a lake... It's because it's checking that the junction on the opposite side of the hex is a lake! :eek:

Junctions on the opposite side of a hex should not affect each other in this manner!! Only the three junctions that touch the same hex-edges as the junction itself should have anything to do with this!! This is a sign that something else is fundamentally wrong with lake determination...

I discovered all of this while working on optimizing SiltifyLakes (which right now can crash v5a on a 28% land, huge, non-Terra map (I made Terra Maps spawn larger to give everyone some leg-room hopefully) because I set the iteration limit too low.

:hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2: :hammer2:
 
the zip file seems to be corrupted

Tested and confirmed that the zip is not corrupted. Make sure you're using an up-to-date version of 7zip or WinRar. WinZip (last I knew) has not been updated in years and won't be able to handle LZMA2 (the compression method used).

Yeah, when pasting it over the existing file, it disallows selecting perfectworld3 as a maptype in game setup.

The file was renamed so it shouldn't overwrite anything. The map was also renamed so it will appear as "PerfectWorld3 (v5a)" in-game. The extracted contents of this file belong in \My Documents\My Games\Sid Meier's Civilization V\Maps\.

I'm 99.9% sure that I've worked out SiltifyLakes now. I'm still not sure why my band-aid fix worked though I suppose it could just be that it greatly reduced the chance of leaving lakes behind. My newest version doesn't need the opposite side of the hex and I've yet to see a left-over lake after thousands of spawns on a 95% land, 0.4 river size, 128x80 map. I've seen as many as 3 million iterations for it to get the job done... but no lakes left behind.

I'm going to chase down a potential problem with math.random (according to what I've read, we may need to be pausing after seeding and/or purging the first random number rolled to prevent issues with maps with the same exact seed from rolling differently). Once I determine what or if anything needs to be done there, I'll clean up and post v5b (hopefully FINAL).
 
Top Bottom