When an island gets placed too close to the edge of the sphere of
influence, its side facing the edge will likely be formed by the edge.
Looks unnatural, and can give a clue on the location of the other
continent.
Make place_island() prefer sectors away from the edge: instead of
picking one of the admissible sectors with equal probability, reduce
the probability as we get closer to the edge.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
Planned island sizes are random with an expected value that matches
the average size requested by the user. Can be off quite a bit when
the number of islands is small. Also, actual island size can be
smaller than planned size when space is tight.
Instead of picking random island sizes independently, pick a random
split of their requested total size.
To reduce the probability of islands not growing to their planned
size, grow large islands before smaller ones.
To compensate for inability to grow, carry the difference over to the
next island size.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
The previous commit reduced the difference in island size within the
same batch of islands to at most one. Eliminate the remaining
difference by shrinking the bigger islands by one sector.
This invalidates the precomputed exclusive zones, so recompute them.
fairland-test needs a tweak to avoid loss of test coverage.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
The previous commits changed grow_island() to create islands in
batches consisting of one island per continent, all of the same
planned size. grow_island() still places and grows one island after
the other. When an island can't grow to the actual size, the others
in the same batch are not affected. Island size can therefore differ
a lot within the same batch.
Change grow_island() to interleave the work on a batch's island: first
place them all, then add one sector to each in turn. Stop after all
reached the planned size, or one or more could not be grown further.
This is similar to how we grow continents: drift() places them all,
and grow_continent() adds one sector to each continent in turn.
Island size within the same batch can now differ at most by one
sector. The next commit will eliminate that remaining difference.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
The previous two commits put the same number of islands closest to
each continent. This one makes their planned sizes match: instead of
rolling dice separately for each island's size, we roll dice only for
the first continent's islands. The other continent's islands get the
same sizes.
Actual island sizes still differ when islands can't be grown to their
planned size. To be addressed next.
fairland-test needs a tweak to avoid loss of test coverage.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
The previous commit made island distribution more fair by placing
islands close to a continent in turn. This is still unfair when
fairland can't place all the islands.
Make grow_islands() fail when it can't place all islands, and main()
start over then, just like it does when grow_continents() fails.
Deities can no longer fill the world with islands by asking for a
impossibly high number of islands. Tolerable loss, I think.
fairland-test needs a tweak to avoid loss of test coverage.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
fairland places islands of random size in random places, subject to
minimum distances. Results are often less than fair, in particular
when the number of islands per continent is low: some continents have
more land nearby than others. Increasing distances between islands
doesn't help much. Deities commonly run fairland until they find the
result acceptable.
The next few commits will tackle this issue. As a first step, this
one places islands closest to continents in turn, so that each
continent is closest to the same number of islands. A continent is
closest to an island when it is closest to each of its sectors.
The number of islands must be a multiple of the number of continents
now.
Since fairland may be unable to place all islands, a continent may
still get fewer islands than it should. The next commit will address
that.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
A continent's sphere of influence is the set of sectors closer to it
than to any other continent. The next commit needs this. Compute the
spheres with a breadth-first search.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
dirx[] and diry[] are redundant with diroff[][]. Convert the
remaining uses, and drop the arrays.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
grow_one_sector() picks a coastal start sector, then moves along the
coast trying to add an adjacent sector to the island.
It operates in spiking mode with a chance of @sp percent.
When spiking, the neighbors with sea on both sides are preferred. For
instance, when the area around the sector being considered looks like
this
- .
- - .
- .
then the neighbor in direction j is preferred, because it has sea in
directions u and n. No other neighbor is preferred.
The start sector is the first coastal sector found in a linear search
in growth order, starting at the last sector grown when spiking, or
else at a random sector. This is new_try().
grow_one_sector() tries adding a neighbor in clockwise order, starting
with a random direction. When spiking, it goes around the clock two
times, trying only preferred sectors in the first round.
When it can't add a neighbor, it moves to the next coastal sector with
next_coast().
Taken together, this randomly picks one element from the set of
pairs (S, D) where the sector in direction D off base sector S can be
added to the island. How does the probability distribution look like?
Bias: a sector's probability to be added to the island land increases
with the number of base sectors it is adjacent to. This tends to fill
in bays and lakes, which is fine.
Bias: coastal sectors following runs of non-coastal ones are more
likely to be picked as start sector. Perhaps less of an issue when
spiking, where the pick is not intended to be random.
Bias: a pair (S, D) is more likely to be picked when base sector S
follows a run of coastal sectors that aren't base sectors, or
direction D follows a a run of directions that don't permit growth.
The impact of these two biases is not obvious. I suspect they are the
reason behind the tendency of large islands to curve around obstacles
in a counterclockwise direction. This can result in multiple islands
wrapping around a central one like layers of an onion.
Bug: the move along the coast is broken. next_coast() searches for
the first adjacent sea in clockwise order, starting in direction g,
then from there clockwise for a sector belonging to the island.
Amazingly, this does move along the coast in a clockwise direction.
It can get caught in a loop, though. Consider this island:
-
- - -
-
If we start at the central sector (marked 0), the search along the
coast progresses like this:
1
- 0 2
-
It reaches the central sector again after three moves (to 1, to 2,
back to 0), and stops without having reached the two sectors on the
left.
If we start with the leftmost sector, the search loops: 0, 1, 2, 3, 1,
...
2
0 1 3
-
grow_one_sector() ensures termination by giving up after 200 moves.
Nuts!
Because of this, grow_one_sector() can fail when it shouldn't, either
because the search along the coast stops early, or goes into a loop,
or simply because there's too much coast. The latter should not
happen in common usage, where island sizes are in the tens, not the
hundreds.
Clean up this hot mess by rewriting grow_one_sector() to pick a sector
adjacent to the island with a clearly defined probability, as follows.
Use weighted random sampling to pick one sector from the set of
possible adjacent sectors.
When spiking, a sector's weight increases with number of adjacent sea
sectors. This directs the growth away from land, resulting in spikes.
When not spiking, the weight increases with the number of adjacent
land sectors. This makes the island more rounded.
To visit the adjacent sectors, grow_one_sector() iterates over the
neighbors of all island sectors, skipping neighbors that have been
visited already.
This produces comparable results for low spike percentages. The weird
onions are gone, though.
Noticeable differences emerge as the spike percentage grows. Whereas
the old one produces long snakes that tend to curve into themselves,
the new one produces shorter spikes extending from a core, a bit like
tentacles. Moreover, islands are more centered on their first sector
now. The probability for coastal capitals is lower, in particular for
moderate spike percentages.
I consider this improvements.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
A sector is admissible for island placement when land can be grown in
every sector within a certain distance.
place_island() picks a random start sector, then searches linearly for
an admissible sector. If it finds one, it places the island there.
Else, it reduces the distance by one and tries again. It fails when
none is found even for distance zero.
Trying with extra distance is obviously meant to reduce the risk of
islands from running into each other without need. Initial distance
is @di, the minimum distance between continents, which doesn't really
make sense, and is undocumented.
Bug: place_island() never tries the start sector.
Bias: placement probability is higher for sectors immediately
following inadmissible sectors. Because of that, islands are more
often placed to the right of existing islands. Players could exploit
that to guide their search for land.
Rewrite place_island() to pick sectors with equal probability,
dropping the undocumented extra distance feature. If it's missed, we
can bring it back.
The new code visits each sector once. The old code visits only one
sector in the best case, but each sector several times in the worst
case. fairland performance improves measurably for crowded setups
with large @di, else it suffers. For instance, Hvy Fever example
given in the commit before previous runs seven times faster for me.
With @di reduced to 2, its run time more than doubles. Not that it
matters.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
There is no need to pass coordinates back to the caller. Replace
parameters @xp, @yp by local variables @x, @y.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
grow_one_sector() and place_island() pass candidate sectors to
try_to_grow() until it succeeds.
try_to_grow() fails when the candidate isn't water, or there are
sectors belonging to other islands within a minimum distance.
It does that the obvious way: it searches for such sectors.
When there is plenty of space, try_to_grow() rarely fails when it
searches. For each land sector, we visit the sectors within minimum
distance, plus a little extra work for the rare failures.
In a more crowded setup, however, try_to_grow() fails a lot, and we
visit sectors many times more.
Example: Hvy Fever
8 continents
continent size: 60
number of islands: 64
average size of islands: 10
spike: 0%
0% of land is mountain (each continent will have 0 mountains)
minimum distance between continents: 6
minimum distance from islands to continents: 3
World dimensions: 140x68
This is a crowded setup. With -R 1, try_to_grow() fails almost
750,000 times due to minimum distance, and takes more than 18 Million
iterations.
With default minimum distances 2 and 1, it fails less than 700 times,
taking less than 14,000 iterations.
Instead of searching the surrounding sectors every time we check a
candidate, precompute an "exclusive zone" around each island where
only this island may grow the obvious way: when adding a sector, visit
the sectors within minimum distance and add them to the island's
exclusive zone.
When @extra_distance is non-zero, try_to_grow() still has to search,
Only place_island() passes non-zero @extra_distance. The next few
commits will get rid of that.
Complication: since the minimum distance for growing islands may
differ from the minimum distance for growing continents, we have to
recompute exclusive zones between growing continents and islands.
For the Hvy Fever example above, this reduces the number of sector
visits by more than 90%, and run time by more than 70% for me. With
default distances, it's a wash.
Of course, fairland performance is hardly an issue on today's
machines: it takes fairly impractical setups to push run time over a
second.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
Callers pass @di for continents, @id for islands, possibly plus some
extra distance. Pass just the extra distance, and compute the rest in
try_to_grow(). This prepares for the next commit.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
To find the wilderness sector to elevate next, elevate_land() searches
the non-mountain, non-capital sectors of the island for one that
maximizes a function of its distance to mountains and to sea.
The search ignores already elevated sectors in a less than obvious
way: 1. it never picks a sector where the function yields -INFINITY or
less, and 2. when elevating a wilderness, its (cached) distances get
reset to values that make the function return a more negative value.
Use a more direct check of "not yet elevated": elevation is still the
initial -INFINITY.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
next_vector() is kind of cute, but it is also unobvious, cumbersome to
use, and slow for arguments greater than one.
Replace it by hexagon_first(), hexagon_next(). The new code takes
O(1) time, whereas the old code takes O(n). Iterating over a hexagon
changes from
for (i = 0; i < n; ++i)
vector[i] = 0;
do {
x = x0;
y = y0;
for (i = 0; i < n; ++i) {
x = new_x(x + dirx[vector[i]]);
y = new_y(y + diry[vector[i]]);
}
do stuff with @x, @y...
} while (next_vector(n));
to
hexagon_first(&hexit, x0, y0, n, &x, &y);
do {
do stuff with @x, @y...
} while (hexagon_next(&hexit, &x, &y));
In my measurements, it's ~8% slower for n == 1, 25% faster for n == 2,
and more than three times faster for n == 6.
fairland's performance is dominated by it. Creating worlds for the
Common Fever blitz (distance arguments 3 and 2) and the Hvy Fever
Blitz (distance arguments 6 and 3) on my machine speeds up by a factor
of 1.6 and 2.1, respectively.
Of course, fairland performance is hardly an issue on today's
machines: it takes fairly impractical setups to push run time over a
second.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
fairland can create fewer and smaller islands than the user requested.
Report like this:
No room for island #13
6 stunted islands
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
grow_continents() places the first two continent sectors without
checking for collisions or minimum distance. Unlikely to be an issue
in practice, as growing such a continent will almost certainly fail.
Fix it anyway.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
grow_one_sector() sets @fl_status when it fails to grow a continent.
grow_continents() uses @fl_status to find out whether
grow_one_sector() succeeded, and main() uses it to find out whether
grow_continents() succeeded.
Change grow_continents() to return success / failure.
grow_one_sector() already does. Use the return values, and eliminate
@fl_status.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
Move global variable @secs into grow_islands() and grow_continents().
Its other users can use isecs[c] instead.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
isecs[] is left zero for continents. Set it to @sc instead, and
simplify two loops over land sectors.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
Both write_sects() and map_symbol() map from elevation to sector type.
Factor out as elev_to_sct_type(). Inline map_symbol() into output()
and simplify.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
main() rejects the distance between continents when it exceeds WORLD_X
/ 2 and WORLD_Y / 2, and the distance between continents and islands
when it exceeds WORLD_Y and WORLD_Y. Nuts. Has always been that way.
Reject either when it exceeds the maximum distance between sectors.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
This reverts commit d2a7bb6b6f.
parse_args() uses WORLD_X and WORLD_Y to check the distance arguments.
Calling it before reading econfig is wrong, because at that time
WORLD_X and WORLD_Y still have the compiled-in default values instead
of the actual ones set in econfig.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
fairland creates continents of size 1 just fine, but the newcap_script
it emits doesn't work: the newcap command requires a second wilderness
sector to the right of the first sector.
Reject continent size 1.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
fairland silently "corrects" some bad arguments. Reject them instead.
It neglects to check others completely. Fix that.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
fairland prefixes error messages with several variations of "fairland:
error -- ", but also with "ERROR: " and nothing at all. Consistently
prefix them with just the program name.
Some error messages end with a period, most don't. Drop the periods.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
"fairland: unstable drift -- try increasing DRIFT_MAX" is confusing:
it looks like an error, but isn't, and increasing DRIFT_MAX requires a
recompile. I'm not sure it can happen. Replace by just "unstable
drift".
"fairland: error -- continent %c had no room to grow!" is pretty
redundant: it's always followed by "Only managed to grow %d out of %d
sectors." and then "ERROR: World not large enough to hold
continents". All it adds is which of the continents failed to grow,
and that's not actionable. Drop the message.
The message sequence "designating sectors...", "adding resources...",
"setting coastal flags...", and "writing to sectors file..." is a bit
of a lie as these four tasks aren't actually done one after the other.
Replace by just "writing to sectors file..."
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
Commit de81e4e20 "Change fairland not to reject small worlds without
trying" (v4.3.25) downgraded it from error to warning, pointing out it
the size may well work, and when it doesn't, fairland fails cleanly.
When it works, the warning is pointless. When it doesn't, it's
redundant. Drop it.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
With -o, fairland doesn't add resources. This is pretty redundant;
the deity can unset resources with "edit l * i 0 g 0 f 0 c 0 u 0".
Drop the option.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
With -a, fairland makes the capital sector an airfield to "mark the
continents [...] so that you can tell them from the islands". This is
pretty redundant since commit afc0ef94e "Make fairland record the
island number in the deity territory", v4.3.31. Drop it.
The map fairland prints is not affected. The continents are clearly
visible there.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
When write_newcap_script() fails, it complains to stderr and fails.
main() doesn't bother to check for failure. Has always been that way.
Fix main() to check. Also adjust write_newcap_script() to return one
on success, zero on failure, like the other functions do.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>
A comment describing how fairland works was lost some time after
Chainsaw 3.31. This is the last known version:
The algorithm starts out by placing the "capitols" on the torus in
such a way so as to maximize their distances from one another (this
uses the perterbation technique of calculus of variations). Then from
these seeds, the continents are grown. The kind of shape they grow
into is determined by the "spike" argument <sp>--the higher the spike,
the more spindly they will be. If you lower the spike, the continents
will be more round. The continents never touch one another, the
argument <di> specifies how many sectors of water should be between
the continents at all times. The continents grow to the size you
specify with <sc> and they all get the same number of mountains
(specified by <pm>). The other arguments should be self explanitory.
If #define ORE 1, then resources will be placed in the sectors as well.
You can alter the #define ORE_MAX, IRON_MIN, GOLD_MIN, FERT_MAX and
URAN_MIN to affect what kind of resources the world gets.
It leaves much to be desired. Add a more thorough one.
Signed-off-by: Markus Armbruster <armbru@pond.sub.org>