I wrote a small 2D map pattern replacement utility.
It's simple, but solves a problem I keep running into with procedural world generation. You can generate a perfectly valid 2D tile map, but it often has that raw, algorithmic look. Water touches grass. Mountains abruptly become meadow. Rivers stop being rivers in awkward places. The data is fine, but the world needs another pass.
So this tool is that pass.
The input is a very simple text map format, .m2d, containing tile indices. Right now that is basically the only map format it supports, which is fine. The script file, .m2s, describes patterns to find and what to replace them with. The program reads the map, applies the rules, and writes out a transformed map, along with TypeScript output for the project that consumes it.
The example here uses Ultima V tiles. In the first image, the map is already recognizable as land and water, but the coastlines are harsh. The map is made of legal tiles, but it does not yet have that pleasing old RPG coastline logic where deep water becomes shallow water, shallow water touches a shore tile, and the edge of the land knows which direction the ocean is.
Before the replacement pass, it looks like this:
After the replacement pass, it looks like this:
Much better.
The script starts with named lists:
lists
deep = 1
shallow = 2
shoals = 3
water = deep|shallow|shoals
swamp = 4
meadow = 5
shrub = 6
grasses = swamp|meadow|shrub
land = flatland|mounts
shore_w = 51
shore_s = 50
shore_e = 49
shore_n = 48
This is mostly to keep the rules readable. I do not want to remember that 1|2|3 means water every time I write a rule. I want to say water.
Then come the rules. A rule is just a little rectangular pattern. Each cell is find:replace. If there is no replacement, it just means find. Wildcards are supported with ? or *.
For example, one early rule turns land next to water into shoals:
water land
water land:shoals
water land:shoals
water land
There are other rules for removing one-tile islands and promontories, placing the correct directional shore tiles, fixing rivers, and cleaning up mountains. The rules are applied in order, which is important. First you make rough decisions, then later rules refine them. This is the way I tend to think about procedural generation anyway: build a crude version, then gradually make it less ugly.
The implementation goal was also modest but deliberate. I wanted to write it in modern C++, without the old habit of scattering new and delete everywhere. The map is just a small generic map2d<T> backed by std::vector. Files are read into strings. Rules are parsed into objects. Storage owns itself. The code leans on RAII instead of manual cleanup.
That sounds obvious now, but if you have written C++ long enough, you know it was not always the default style. There was a time when every little utility wanted to become a memory-management exercise. This one does not. It reads a file, transforms a map, writes a file, and exits. No ceremony.
I also like that the script format is crude in the right way. It's not JSON or XML. It's not a beautiful general-purpose language. It's a little thing for describing little grids of tile indices.
That said, it already gets surprisingly expressive. The coastline example is the obvious win. The original map has land and water in the right places, but the tile transitions are wrong. The transformed map gets shoals, beaches, shore corners, and more convincing edge behavior without needing the original generator to know about every possible local tile relationship.
That separation is useful. The world generator can think in broad strokes: continents, islands, forests, mountains, rivers. The matcher can think locally: “this water tile has land to the east, so make that land a west-facing shore.” Those are different jobs.
The next obvious improvements would be support for more map formats, better diagnostics when a script rule is malformed, and maybe some way to visualize which rules fired. I could also imagine adding probabilities, so a rule could replace a matched pattern with one of several tiles. That would make it easier to break up repeated edges and add a little controlled noise.
For now, though, it does what I need. It takes a rough procedural map and gives it one more pass of tile-level intelligence. The result still looks like a map from an old computer RPG, which is exactly the point,


No comments:
Post a Comment