TownSquare The Town Square Gazette

Dev Log: when map lines start behaving like roads

The TownSquare map is learning to treat connections as roads: curved, layered, ranked by importance, and routed around the landscape.

The first TownSquare map drew a line between two towns when their owners linked them together. It did the job, but it also looked exactly like what it was: a graph laid over a map.

If you would prefer the in-universe version, The Town Square Gazette has also published Gazette Notice: the cartographers have at last learned what roads are.

The lines were thin, even and happy to pass straight through anything in their way. Forest in the middle? Straight through it. Mountain range? No problem. A town that had nothing to do with the connection? Right across the centre.

That is fine for a network diagram. It is odd for a landscape.

I have been working on a new road renderer that asks a more useful question. Instead of only asking which towns are connected?, it also asks how would a road get from one to the other?

The new road renderer on the TownSquare map The first terrain-aware roads on the TownSquare map.

Before and after

The change is easiest to see when the same part of the map is shown at the same zoom level.

Old straight-line connections Before: connections behaved like simple graph lines, cutting straight through the landscape.

New terrain-aware roads After: roads curve, avoid terrain, and carry different visual weight based on their importance.

The non-technical version

The old paths behaved like lines drawn with a ruler. The new ones behave more like roads on a hand-drawn map.

A road has more than one line

Each road is now drawn in layers. There is a darker outer edge, a lighter road surface, and a small broken centre marking. Trails are thinner and use a rougher dashed surface.

It is a small change, but it gives the road a physical width. It looks like something a person could follow rather than a relationship in a spreadsheet.

Roads pay attention to the land

The map now gives different parts of the landscape different levels of resistance. Empty land is easy. Going near an unrelated town is awkward. Forests are harder, mountains are much harder, and water is expensive to cross.

The route then looks for a sensible way through. It may curve around a group of trees, pass between towns, or head towards one of the preferred crossing points on a river.

This does not mean every road takes a huge scenic detour. Sometimes the direct route is still the best one. The important change is that the landscape now has a vote.

Not every connection is equally important

A link between two highly connected towns can become a major road. Ordinary two-way links become local roads, while one-way or weak links look more like trails.

That makes the map easier to read at a glance. A busy route should carry more visual weight than a small path to one neighbour.

Roads can follow one another

Real roads often share a route for a while and split later. The new router remembers which parts of the map have already been used and makes those corridors cheaper for the next road.

This first version gets roads to gather into common corridors. It does not yet merge every shared section into one perfect piece of road, but it is a useful step away from drawing every connection in complete isolation.

The technical section

The renderer turns the map into a small invisible grid. Each grid cell gets a movement cost:

Terrain Cost
Empty land 1
Near another town 6
Forest 8
Mountain 20
Water 30
Preferred bridge area 1

Those numbers are not distances. They are preferences. Moving through ten cells of open ground is usually better than cutting through several mountain cells, even when the mountain route is shorter on screen.

Finding a route with A*

For every connection, the renderer runs the A pathfinding algorithm between the two towns. A keeps track of two things:

  • how expensive the journey has been so far;
  • a reasonable estimate of the distance still left to travel.

That lets it search useful parts of the grid without trying every possible route across the world.

The map currently has 119 towns, 737 pieces of scenery and 38 visible connections. On that real dataset, a complete routing pass takes roughly 40 to 156 milliseconds on the development machine. That is reasonable for rebuilding the public map after its structure changes, although it is too much work to repeat on every frame while drawing scenery in the admin editor. That editor path still needs some caching and delayed recalculation.

Stable roads instead of fresh randomness

Random bends can make a map feel organic, but they also make roads jump whenever the page reloads. TownSquare derives its fallback curve from the IDs of the two connected towns, so the same pair produces the same bend.

The terrain route is deterministic too. Given the same towns, connections and scenery, it produces the same road network.

This matters more than it might sound. A map becomes familiar through repetition. A road that moves every ten seconds never gets the chance to feel like a place.

Stopping at the town boundary

The pathfinder works from town centre to town centre, but the finished SVG path is trimmed at both ends using the size of each town marker. Large towns have larger markers, so their roads stop farther from the centre.

After trimming, consecutive grid points in the same direction are removed. The remaining corners are smoothed into quadratic curves. That keeps the geography-aware route without leaving it looking like a staircase.

Finally, the same path is rendered three times:

wide dark stroke      road edge
medium light stroke   road surface
thin dashed stroke    centre marking

The three strokes live in one SVG group. Selecting a town can highlight every related road without recalculating the route.

Encouraging shared corridors

Roads are calculated from most connected to least connected. When one route uses an ordinary land cell, that cell becomes cheaper for later routes. Major roads therefore establish likely corridors and smaller roads are encouraged to join them.

This is a lightweight form of edge bundling. A fuller version would turn shared grid segments into a separate road network and draw each shared segment only once. That would reduce repeated strokes and scale better as more towns link to one another.

What happens when the map changes?

The road network is derived from the current map rather than stored as a separate drawing.

When a new verified town appears, TownSquare lays out the towns again, rebuilds the connection graph, recalculates road importance, and runs the router for the new set of links. A town only gets a road when one of its neighbour URLs matches another public TownSquare.

Scenery works the same way. Add trees, mountains or water and the next structural map refresh builds a new cost grid from those features. Roads may move because the geography has changed.

What still needs work

This is the first terrain-aware version, not the end of the road.

Three parts need another pass before I am happy with them:

  • The scenery editor should not recalculate every route while the pointer is moving.
  • River crossings currently prefer bridge areas, but they do not strictly forbid other crossings.
  • Shared corridors should eventually become shared road segments rather than several overlapping paths.

There are also smaller details to tune: how much a road bends, how strongly forests repel it, and when a local road becomes a major one. Those choices are easier to judge on a real 119-town map than in a small test fixture.

The current public map is available at townsquare.cauenapier.com/map. Have a look, follow a few roads, and see whether the network feels more like a place.