package overlay import ( "math" "sort" ) // Roads: the least-cost paths between the settlements that were just placed. // // A road is the one mark whose shape is not a judgement at all. Given where two towns are, the line between // them is whatever the ground allows - up the valley, round the spur, across the saddle - and that is a // shortest-path problem with a cost function, not a drawing. It is also the single most tedious thing to // paint by hand, because getting it right means reading a heightmap pixel by pixel. // // Three decisions worth stating: // // - **Water is impassable, so roads never swim.** Each landmass gets its own network. A bridge or a ferry // is a deliberate act and belongs to the author, and a generator that guessed at them would put a // motorway across a strait it has no idea is thirty kilometres wide. // - **A minimum spanning tree, not every pair.** Joining all pairs gives a cobweb; the tree gives exactly // enough road to reach everywhere, which is both what a road network minimally is and the thing an // author can most easily add to. Edges are weighted by path *cost*, not by straight-line distance, so // two towns either side of a range are correctly further apart than the map says. // - **It runs on a coarsened grid.** A road at the overlay's full resolution would be a Dijkstra over // twenty-nine million cells per settlement. The cost surface is smooth at the scale a road cares about, // so it is pooled to a few hundred cells across, solved there, and the resulting polyline is stamped // back at full resolution with the mark's real width. // roadGrid is the coarsened cost surface the paths are solved on. type roadGrid struct { w, h int step int // overlay pixels per coarse cell cost []float32 // per coarse cell, +Inf where impassable scale float64 // overlay pixels per coarse cell, as a float } func buildRoadGrid(d *genData, maxSlopeDeg float64) *roadGrid { in := d.in // About six hundred cells around the world: fine enough that a coarse cell is well under a kilometre on // any world this tool makes, coarse enough that fifty Dijkstras are a second's work. step := int(math.Max(1, math.Round(float64(in.W)/600))) gw := (in.W + step - 1) / step gh := (in.H + step - 1) / step g := &roadGrid{w: gw, h: gh, step: step, scale: float64(step), cost: make([]float32, gw*gh)} inf := float32(math.Inf(1)) for gy := 0; gy < gh; gy++ { for gx := 0; gx < gw; gx++ { // Pool the block: any sea in it makes the cell water, because a road that clips a bay is a road // in the sea. The slope taken is the worst in the block, for the same reason. var worst float64 wet := false for y := gy * step; y < (gy+1)*step && y < in.H; y++ { for x := gx * step; x < (gx+1)*step && x < in.W; x++ { i := y*in.W + x if in.Sea[i] { wet = true break } if s := float64(d.slopeDeg[i]); s > worst { worst = s } } if wet { break } } gi := gy*gw + gx switch { case wet: g.cost[gi] = inf case worst > maxSlopeDeg: g.cost[gi] = inf default: // Slope is what a road pays for. Quadratic rather than linear so that a route prefers a long // gentle way round to a short steep one, which is what a real road does. t := worst / math.Max(maxSlopeDeg, 1e-6) g.cost[gi] = float32(1 + 12*t*t) } } } return g } func (g *roadGrid) idx(x, y int) int { return y*g.w + x } // dijkstra returns the cost to every reachable coarse cell from a source, and the predecessor chain to walk // a path back. A binary heap over a few hundred thousand cells; the graph is eight-connected and X wraps. func (g *roadGrid) dijkstra(src int) (cost []float32, pred []int32) { n := g.w * g.h cost = make([]float32, n) pred = make([]int32, n) inf := float32(math.Inf(1)) for i := range cost { cost[i] = inf pred[i] = -1 } if math.IsInf(float64(g.cost[src]), 1) { return cost, pred } cost[src] = 0 h := &costHeap{keys: []float32{0}, items: []int32{int32(src)}} for h.Len() > 0 { c := int(h.pop()) cx, cy := c%g.w, c/g.w base := cost[c] for _, o := range neighbours8 { nx := (cx + o[0] + g.w) % g.w ny := cy + o[1] if ny < 0 || ny >= g.h { continue } n := g.idx(nx, ny) cc := g.cost[n] if math.IsInf(float64(cc), 1) { continue } // Diagonal steps cost their real length, or the network shows a bias along the axes. step := float32(1.0) if o[0] != 0 && o[1] != 0 { step = float32(math.Sqrt2) } next := base + cc*step if next < cost[n] { cost[n] = next pred[n] = int32(c) h.push(int32(n), next) } } } return cost, pred } // costHeap is a binary min-heap of coarse cells. Lazy deletion is not needed because a cell is only pushed // when its cost strictly improves, and a stale entry pops with a cost no better than the settled one. type costHeap struct { keys []float32 items []int32 } func (h *costHeap) Len() int { return len(h.items) } func (h *costHeap) push(item int32, key float32) { h.keys = append(h.keys, key) h.items = append(h.items, item) i := len(h.items) - 1 for i > 0 { p := (i - 1) / 2 if h.keys[p] <= h.keys[i] { break } h.keys[p], h.keys[i] = h.keys[i], h.keys[p] h.items[p], h.items[i] = h.items[i], h.items[p] i = p } } func (h *costHeap) pop() int32 { top := h.items[0] last := len(h.items) - 1 h.keys[0], h.items[0] = h.keys[last], h.items[last] h.keys = h.keys[:last] h.items = h.items[:last] i := 0 for { l := 2*i + 1 if l >= last { break } if r := l + 1; r < last && h.keys[r] < h.keys[l] { l = r } if h.keys[l] >= h.keys[i] { break } h.keys[l], h.keys[i] = h.keys[i], h.keys[l] h.items[l], h.items[i] = h.items[i], h.items[l] i = l } return top } // paintRoads traces a spanning tree over the settlements of each landmass and stamps it. func (l *Legend) paintRoads(m *Mark, d *genData, out *Raster, idx uint8, placed []Placed) (int, int) { if len(placed) < 2 { return 0, 0 } g := m.Generate maxSlope := g.MaxSlopeDeg if maxSlope <= 0 { maxSlope = 22 } widthM := g.WidthM if widthM <= 0 { widthM = m.WidthM } if widthM <= 0 { widthM = 8 } // A road eight metres wide is less than one overlay pixel at 12.9 m, and a mark thinner than a pixel is // not a mark. It is painted at least one pixel wide and the true width travels in the legend, which is // exactly how `width_m` is meant to be read. halfPx := int(math.Max(0, math.Round(widthM/d.in.CellM/2))) rg := buildRoadGrid(d, maxSlope) // Settlements grouped by landmass: a spanning tree per island, never between islands. byRegion := map[int][]int{} for i, p := range placed { if p.Region < 0 { continue } byRegion[p.Region] = append(byRegion[p.Region], i) } regions := make([]int, 0, len(byRegion)) for r := range byRegion { regions = append(regions, r) } sort.Ints(regions) // deterministic order, cross-cutting rule 12 total, pieces := 0, 0 for _, r := range regions { members := byRegion[r] if len(members) < 2 { continue } total += l.connectRegion(rg, d, out, idx, placed, members, halfPx, &pieces) } return total, pieces } // connectRegion solves the paths among one landmass's settlements and stamps its spanning tree. func (l *Legend) connectRegion(rg *roadGrid, d *genData, out *Raster, idx uint8, placed []Placed, members []int, halfPx int, pieces *int) int { n := len(members) src := make([]int, n) for k, pi := range members { p := placed[pi] gx := (p.X / rg.step) % rg.w gy := p.Y / rg.step if gy >= rg.h { gy = rg.h - 1 } src[k] = rg.idx(gx, gy) } // One Dijkstra per settlement, kept: the coarse grid is a few hundred thousand cells and a landmass has // a handful of towns, so holding the predecessor chains costs a few megabytes and saves solving twice. costs := make([][]float32, n) preds := make([][]int32, n) for k := range members { costs[k], preds[k] = rg.dijkstra(src[k]) } // Prim's, on path cost. Unreachable pairs are skipped, so a landmass whose towns are separated by ground // too steep for a road comes out as two networks rather than one impossible line. inTree := make([]bool, n) inTree[0] = true painted := 0 for added := 1; added < n; added++ { bestA, bestB := -1, -1 best := float32(math.Inf(1)) for a := 0; a < n; a++ { if !inTree[a] { continue } for b := 0; b < n; b++ { if inTree[b] { continue } if c := costs[a][src[b]]; c < best { best, bestA, bestB = c, a, b } } } if bestA < 0 || math.IsInf(float64(best), 1) { break // nothing else on this landmass is reachable by road } inTree[bestB] = true painted += stampPath(rg, preds[bestA], src[bestA], src[bestB], d, out, idx, halfPx) *pieces++ } return painted } // stampPath walks the predecessor chain back from dst to src and paints it at full resolution. func stampPath(rg *roadGrid, pred []int32, src, dst int, d *genData, out *Raster, idx uint8, halfPx int) int { var chain []int for c := dst; c >= 0; { chain = append(chain, c) if c == src { break } p := pred[c] if p < 0 { return 0 // no route; leave the ground unpainted rather than drawing a guess } c = int(p) } painted := 0 for k := 0; k+1 < len(chain); k++ { ax, ay := coarseCentre(rg, chain[k]) bx, by := coarseCentre(rg, chain[k+1]) painted += stampSegment(out, d, ax, ay, bx, by, halfPx, idx) } return painted } func coarseCentre(rg *roadGrid, c int) (int, int) { gx, gy := c%rg.w, c/rg.w return gx*rg.step + rg.step/2, gy*rg.step + rg.step/2 } // stampSegment draws one straight run between two coarse-cell centres, wrapping in X the short way so a road // crossing the seam is one road rather than a line back across the whole map. func stampSegment(out *Raster, d *genData, ax, ay, bx, by, halfPx int, idx uint8) int { dx := wrapDelta(bx-ax, out.W) dy := by - ay steps := int(math.Max(math.Abs(float64(dx)), math.Abs(float64(dy)))) if steps == 0 { return stampDisc(out, d, ax, ay, halfPx, idx, true) } painted := 0 for s := 0; s <= steps; s++ { t := float64(s) / float64(steps) x := ax + int(math.Round(float64(dx)*t)) y := ay + int(math.Round(float64(dy)*t)) if y < 0 || y >= out.H { continue } painted += stampDisc(out, d, x, y, halfPx, idx, true) } return painted }