The Children Yearn for the Mines

2025-07-31 games
Two great block-map-based games which taste great together

UPDATE: i talked about this in a lightning talk at PyConAU 2025

I’ve been a bit obsessed with Ultima IV for a while now and when MineCraft first came out in Beta in, ummm, 2012 or so, I got nostalgia something bad.

Blocky graphics! Getting lost in the trees! Attacked by goddamn skeletons! The children yearn for the mines.

Almost immediately I thought: I wonder if it’d be possible to bring Ultima IV’s beautifully designed continent into 3D life.

The world of Ultima IV The world of Ultima IV I did get as far as messing with Minecraft’s maps, but the file formats changed a few times and many other things kept me busy, and I never really got anywhere with this idea, until …

Luanti (fka Minetest)

Luanti, formerly known as Minetest, is a pretty close knockoff1 of Minecraft, although written in a more modular and customizable way.

It’s also open source and fairly well documented and written in C++ and Lua rather than Java, all of which make my attempts to understand it quite a lot easier.

So, I started this project to understand a little bit more about the challenges

First task was to work out if I could write to a Luanti map. The map is stored zstd compressed in an sqlite table which seems a little perverse but let’s leave that aside for the moment. At least it is documented.

Each Luanti mapblock includes a mapping from a 16-bit cell type to a “itemstring” which identifies the plugin and item name of cells with this ID. Each cell in the mapblock has three parameters: the 16-bit cell type called “param0” and two 8-bit numbers: I don’t know what “param1” is for but “param2” mostly seems to be used for cell orientation. There’s also some metadata around lighting and event timers but I just set all that to zero.

I wrote some very nasty code to stash the correct bytes in the table, handed it a bunch of random tiles and found to my delight that there was now a big cube of nonsense at (0, 0, 0). After a few rounds of failures and debug messages anyway.

big cubes of nonsense big cubes of nonsense

Reading the World

The next challenge was to read the Ultima IV world map. I happened to have the map from the PC edition laying around from earlier Ultima IV adventures and so that wasn’t too difficult to dredge up.

Looping over the world and writing each tile as a big 16x16x16 mapblock went okay, got me to the point of having something which looked a tiny bit like a map …

behold, a familiar river (in Ultima IV) behold, a familiar river in Ultima IV

behold, a familiar river (in Luanti) behold, a familiar river in Luanti

… and then it was just a matter of finding the right Luanti materials to correspond to what I wanted.

Scaling

Initially I was mapping easy Ultima IV tile to a 16x16x16 mapblock, because that was the easiest way.

But I knew I wanted to include the towns in the map. Each town is represented by a single tile in the world map, and entering the town opens a separate 32x32 town map. So it seemed like the obvious thing was to make each world map tile into a 32x32 area, so the towns would fit neatly.

But even scaling each tile to 16x16 seemed a bit … big? The rivers were vast, crossing forests took too bloody long, flying high enough to spot landmarks put you up in the clouds.

Also, 16 was a very convenient scale for the file transformation process but it felt like a mistake to be muddling up a game-play requirement (how big is the world) with a technical requirement (how to read and write files).

So, I split the code into two sections: reading and writing, and joined the two with an intermediate representation. My first thought was to make this one huge bytearray and calculate the offset of each chunk within it, but the representation I settled on is a collection of 4096-element bytearrays, each of which maps into a Luanti 16x16x16 mapblock.

In Python terms, World is a mapping from a triple of integers to 4096 element bytearrays. Using defaultdict means that a new empty chunk gets created whenever it is needed.

It’s easy to write a function set_block(x, y, z, b) which finds the appropriate chunk and offset for a location (x, y, z) and writes a block b to that position.

CHUNK = 16

World = defaultdict(lambda: bytearray(CHUNK*CHUNK*CHUNK))

def set_block(x, y, z, b):
    chunk = World[(x//CHUNK,y//CHUNK,z//CHUNK)]
    chunk[(z%CHUNK)*CHUNK*CHUNK + (y%CHUNK)*CHUNK + (x%CHUNK)] = b

The bytearray values default to 0 so we’ll map that to “air”. By storing the bytes in the same order as the file format expects, we can use the bytearrays directly to write the chunks.

That map gets populated by looping over the Ultima IV world map, scaling it up by an arbitrary SCALE factor. Trying different values, I’ve come to think that 12 or 14 might be appropriate.

With any SCALE of less than 32 the towns hang over the edges of their tiles into adjoining tiles but towns aren’t right next to each other so it doesn’t matter too much.

Britain and Castle Britannia (in Ultima IV) Britain and Castle Britannia in Ultima IV

Britain and Castle Britannia (in Luanti) Britain and Castle Britannia in Luanti

Castle Britannia has two maps stacked on top of each other and also has two “ends” to make it look grander but we can just ignore those and fill them in manually later.

Transformation

There’s a lot of flexibility in the Luanti map format which I don’t want to deal with so this is strictly a write only format for me.

So the steps are:

Further Work

Smoothing

The current map is very blocky. I mean, of course it is blocky, but it’s blockier than it needs to be. What I want to do is run some kind of filter over it which will smooth out the square corners into more natural looking bends.

Averaging the values isn’t right because that would generate a strip of “shallow water” (2) where “normal water” (1) touches land (3), and similar artifacts in other places.

averaging values averaging filter: blech

Instead I’ve used a filter which sets each cell to the median value of an (N*2+1) x (N*2+1) area around the cell. Since this is always an odd number of cells, the median value is always one of the values in the area, and so no extraneous block types are added. It results in some quite nice rounded features:

swampy inlet in Ultima IV swampy inlet in Ultima IV

swampy inlet with median values swampy inlet with median filter: much nicer

swampy inlet from the air, in Luanti swampy inlet with median filter, in Luanti

Terrain

At the moment the landscape is pretty boring … flat … because it is. The continent of Britain is hilly as anything.

So I suspect what it needs is a (256 * SCALE × 256 * SCALE) elevation map, letting the landscape “fall up” from the coast to the mountains, and down from the coast to the depths.

For SCALE = 12 this is about 10M values, so could be sensibly stored as a single bytearray, and then we take many passes to fill in the heights and depths.

Trees

It also needs trees! I can generate saplings, I’m not sure whether they’ll sprout on their own.

Shrines and Dungeons

At the moment there’s no mapping for shrines and dungeons. Or either end of Britannia, which is supposed to look like a little castle with towers at each end.

Toroidal Maps

The Ultima IV map is toroidal, meaning that if you go far enough North you end up back in the South and vice-versa and the same for East and West. Luanti doesn’t have this property, instead the world has an edge (about 30,000 cells in every direction).

It doesn’t look impossible to modify the map loading code to fix this, making the world toroidal or 3-toroidal (where the vertical axis wraps around as well, so falling through the bottom of the world you’d find yourself falling back in at the top.)

Roofs and Letters

Giant letter blocks are a big feature in the Ultima IV maps. There’s some packages of letters available to install … there’s a big advantage of Open Source!

Roofs in the towns are trickier, as there’s maybe not a clear way to tell from the map if an area is indoors or outdoors. Worth looking at anyway.

I’m happy if this mapping process can get 90% of the way. I’m happy if it gets any of the way, really.

There’s going to be stuff to finish off manually, for example to merge the towns in with the landscape around them, but without the map conversion getting this finished would be virtually impossible without a vast and dedicated crew.

Repo

Stay tuned …

  1. I mean, you can argue that Minecraft started as a pretty close knockoff of Infiniminer and we’re all standing on each other’s feet all the way down.