Idraluna Archives

The Great Antarctic *crawl pt. 11 - Apparatus of Capture

In the last GAH installement, I reworked how 'human' settlements were distributed across the landscape to be more closely linked to (approximate) arable land and trade access. Today, I'll tackle two obvious follow-up questions: are those settlements grouped into polities of some sort, and how big/small are they?

Clustering Polities

Prior versions of Antibor never had a very clear idea of political organization; in an effort to approach things from a 'playability-first' angle, I tried to contrive vague, distant powers that leave most of the world as a lawless hinterland (e.g. ruled by magical/alien 'archons' inspired by Morrowind & Dark Sun). Without scrapping that idea entirely, I've decided that making everything a lawless borderland is just as boring as making everything part of an organized state. So instead, I'm aiming for a mix of decadent empires, maritime city-states, petty despots, and ungovernable hillfolk.

After mulling over a variety of crude (draw circles on the map?) & complicated (cellular automata? k-means clustering?) solutions for grouping settlements into politiesn I opted for simple but relatively elegant approach.

IRL empires are (loosely) limited by the time it takes to send messages (and punitive expeditions) from the core to the periphery. GIS makes it easy to calculate travel times (e.g.) and one can modify the calculation by imposing steeper or lesser penalties to certain biomes, elevations, etc.

So in natural language the algorithm goes like this:

  1. Pick a settlement that is likely to be wealthy (in a cell with a high arable land score) to be the capital of an independent polity.
  2. Calculate a 'power projection map' recording travel time from the capital to all cells in Antibor.
  3. Truncate the power projection map to equal or less than the capital's arable land score, multiplied by a random adjustment factor. (This means that polities in less fertile/governable land are likely to be smaller).
  4. Assign all settlements within the truncated footprint to the capital's polity.
  5. Adjust the power projection cost map to prevent overlapping polities.

The random pick from step (1) uses the squared arable land score, so the wealthiest polities are be around 225 times more likely to get picked than the most peripheral. But the list shrinks rapidly as settlements are grouped into a polity (and thus exempted from being the next capital), especially since arable land is very autocorrelated.

The power projection scores assigned in step (2) are calibrated to approximately match travel time in open landscapes and on water. Penalties for other terrain types are vibes-based, but loosely correspond to how difficult an environment is to 'striate'; biomes resembling 'smooth' (steppe, desert) or 'holey' (jungle, forest) spaces are penalized. The exception is the smooth space of the sea/rivers which I guess have been appropriated into the functioning of the state in this model.

Power projection cost Biome
0.33 Water (ocean and rivers)
1 Open terrain: savannah, chaparral
2 Forest, taiga
3 Jungle, steppe, desert
10 Xenoformed

A cost of 2 means that a capital's power extends half as far in this terrain, etc.

Additionally, I had terrain slope and elevation impose a steep cost (no pun intended) to create natural barriers & bottlenecks and encourage small independent/anarchic mountain villages, and I added a deep-water penalty to make states less likely to span expanses of open ocean.

Step (3) required some arbitrary finagling; Rome at its height spanned over three weeks of travel time from the capital. Unfortunately, an empire that size could occupy about half of Antibor, depending on where the capital is located. After some experiments, I settled on a 1:1 mapping of arable score to power projection (but with a minimum reach of one day's travel), meaning that the biggest possible empires could encompass around 15 days of travel from the capital.

Travel time from one point 21 (adjusted) power-projection days from a random settlements (red dot) -- this example is unusually small, I think because the settlement is in a hilly area.

Results

After running the algorithm overnight, here's the resulting map of independent polities:

Antibor's 5,555 settlements were grouped into 394 polities. The largest empire has a land area of 244,000 sq. km. (equivalent to the UK), and boasts 467 subject settlements. At the other end of the scale, 200 settlements are independent (in polities that only include themselves).

Eyeballing the distribution of polity sizes, it looks like it fits something like a power law or lognormal distribution.

Just for fun, I used the poweRlaw R package to confirm that the lognormal fit is substantially better (above right; red is power law, green is lognormal). This is immensely gratifying, because real-world country sizes also follow an approximate log-normal distribution (below, using data from the World Bank). Since my algorithm doesn't pre-suppose anything about the resulting size distribution, it suggests that my intuitions weren't wholly off-base.

Regardless, both distributions meet the basic goal of having a few big empires, more medium-sized polities, and lots of small city-states.

Next Steps

The next task will be to assign each settlement a population -- I'll be tackling this & revisiting the idea of power law distributions in a future post. Then, of course, each polity needs a name, a culture, a history, and a set of problems for adventurers to engage with.

I may also tinker with the borders -- there are a few too many cases where a polity oozes ameoba-like around the borders of one generated earlier. But since my inclination is to eventually display the allegiance of individual POIs without clearly demarcated borders (closer, I think, to how pre-modern people experienced these things), it's low priority.

I'll also need to regenerate the road and trail networks to make this a proper pointcrawl. I'd like to give more attention to road networks, maybe proceeding in different phases to link subject settlements to their capital and linking capitals via major trade routes, followed by trails connecting landmarks and lairs to their nearest settlements.

Notes & Interesting Results

The purple polity below is a good example of borders being limited by mountains:

The Spine (mountainous rain forests) has a great mix of large and small polities:

A few polities generated with their zones of control spread out along rivers:

This region in Knorthern Antibor has a neat cluster of medium-sized states, maybe a recently-collapsed empire?

Code

all_sites.shp <- vect(read.csv(file.path(mapdir, 'OntoHex', 'Pointcrawl_sites.csv')), geom=c('x', 'y'))
settlements.shp <- all_sites.shp %>% filter(Type=='Settlement') %>% mutate(Allegiance=NA)

cost.r <- biome.r %>% classify(rcl = data.frame('is'=Biome_map$Code, 'becomes'=Biome_map$Cost))
cost.r[!is.na(slope.r)] <- slope.r/2 + elevation.r/1000 + cost.r
cost.r[big_rivers.r==1] <- 0.33
cost.r <- cost.r + (dist_land.r/25)  # impose a penalty for sailing out of sight of land
plot(cost.r, range=c(0,15))
writeRaster(cost.r, file.path(mapdir, 'OntoHex', 'TravelCost.tif'), overwrite=T)

cost.r <- rast(file.path(mapdir, 'OntoHex', 'TravelCost.tif'))
cost.r <- aggregate(cost.r, 2, 'min')
# test_city.shp <- sample(settlements.shp, 1)
# test_city.r <- test_city.shp %>% rasterize(cost.r)
# test_cd <- costDist(ifel(test_city.r==1, -1, cost.r), target=-1, scale=40000)
# plot(test_cd); plot(test_city.shp, add=T)
# plot(as.int(test_cd))
# 
# reach <- test_city.shp$arable_score
# plot(ifel(test_cd <= reach, 1, NA)); plot(settlements.shp, add=T)
# 
# hist(settlements.shp$arable_score)
hegemonies.r <- rast(cost.r, vals=NA)

while(sum(is.na(settlements.shp$Allegiance))>0){
  # pick a random settlement weighted by arable score
  hegemon <- sample(settlements.shp[is.na(settlements.shp$Allegiance),], 1, prob=settlements.shp$arable_score[is.na(settlements.shp$Allegiance)]**2) 
  hegemon.r <- rasterize(hegemon, cost.r)
  
  reach <- as.integer(runif(1, min=0.33, max=1) * hegemon$arable_score) 
  
  reach.r <- costDist(ifel(hegemon.r==1, -1, cost.r), maxiter=25, target=-1, scale=40000)
  cost.r <- cost.r + ifel(reach.r<=reach, reach-reach.r, 0)
  reach.r <- ifel(reach.r<=reach, hegemon$ID, NA)
  
  hegemonies.r[is.na(hegemonies.r)] <- reach.r
  settlements.shp$Allegiance <- terra::extract(hegemonies.r, settlements.shp, ID=F)
  
  print(sum(is.na(settlements.shp$Allegiance)))
  plot(elevation.r, col=colorRampPalette(c('gray', 'white'))(100)); plot(hegemonies.r, type='classes', add=T, col=manyCols, alpha=0.5)
  
}
writeRaster(hegemonies.r, file.path(mapdir, 'OntoHex', 'hegemonies.tif'), overwrite=T)

plot(elevation.r, col=colorRampPalette(c('gray', 'white'))(100)); plot(hegemonies.r, type='classes', add=T, col=manyCols, alpha=0.5)


# analysis
length(unique(values(hegemonies.r)))
hegemonies.df <- as.data.frame(c(hegemonies.r, aggregate(arable.r, 2))) %>%
  filter(arable_score>0, !is.na(class)) %>%
  group_by(class) %>%
  summarise(area_sqkm = n(), arable_sum=sum(arable_score), arable_mean=sum(arable_score)/n())
settlement_counts <- settlements.shp %>% as.data.frame() %>% group_by(Allegiance) %>% summarise(count=n())

hist(hegemonies.df$area_sqkm, breaks=100)
median(hegemonies.df$area_sqkm)
ggplot(data=hegemonies.df, aes(x=log(area_sqkm), y=arable_mean)) + geom_point()

library(poweRlaw)
pl_fit <- displ$new(hegemonies.df$area_sqkm)
est <- estimate_xmin(pl_fit)
pl_fit$setXmin(est)

m_ln = dislnorm$new(hegemonies.df$area_sqkm)
est2 = estimate_xmin(m_ln)
m_ln$setXmin(est2)

par(mfrow=c(1,2))
hist(hegemonies.df$area_sqkm, breaks=100)
plot(pl_fit)
lines(pl_fit, col=2, lwd=2)
lines(m_ln, col=3, lwd=2)

#DIY #GIS #antibor #lore24