Teaching programming with Lua: a Minetest mod
Since my daughter liked playing Minecraft, we thought of doing something like a
mod, but that turned out to be hard and borderline illegal due to decompilation
of Java bytecode involved. So instead, I started looking for an open-source
alternative, found a few and decided to use
Minetest that appeared to be quite like Minecraft,
had large enough community, and mods in Lua
with public API and plenty of
examples.
We chose this project: a mod that is capable of generating a world out of a hand-drawn map, where height is represented by brightness, there’s water (painted blue) and grass (painted green). In the end, the main part of the program that my daughter actually wrote herself turned out to be less than 100 lines of Lua.
By that time, we had mastered loops and tables and the project seemed doable.
Algorithm
The algorithm is quite simple really: for each pixel of the original image, determine its color (blue or green) and brightness. Dividing brightness by 10 gives a nice max hills height of 25 blocks, so there should be a loop that places blocks of dirt from 1 to the calculated height, followed by a block chosen depending on the color of the corresponding pixel - water or grass.
We decided that water should always be at the “sea level”, so we ignore brightness of blue pixels and set it to be 2. Another minor complication is what to do when the color is neither green nor blue? We chose to place stone blocks in those places; in real examples, those appeared where we darkened the image too much to create high hills. This way, hilltops are sometimes rocky. Nice touch, come to think of it.
Helper functions
There’s quite a bit of explanation involved in determining color of a pixel. After all, we see it as green, while the program sees a tuple of numbers, usually different for even nearby green pixels. For instance, these are all greens: (113, 245, 80), (54, 173, 80), (83, 204, 128).
As an easily explainable solution (remember, this had to be parseable by an 8-year-old), I chose this solution:
local function is_grass(px)
return px.G >= 150 and px.R < px.G and px.B < px.G
end
For blue color we came up with something very similar, just tweaked the constant a bit (see below for actual code).
Tweaking
After we did all the straight-forward things, my daughter asked if we can make sandy riverbanks. I was a bit reluctant at first, because it involved a lot of coding (by beginner’s standards) with lots of repetition, but enthusiasm won and we implemented a function that checks nearby pixels to see if any is blue and when it is, place sand block instead of the usual grass. Worked like a charm!
Demo
So the original watercolor-painted map turned to be looking like this:
Its’ surface is very uneven because the original was painted; computer-drawn maps where, unlike in the real world, you have to try hard to create noise, are, of course, a lot smoother.
Check out a live demo:
Code
This is meat of the mod that was written by my daughter; full code of the imagemap mod is also available, but it may differ from the code below because we continued to work on it and kept updating the github repo.
local function clean()
math.randomseed(os.time())
for x = 0, 100 do
for z = 0, 100 do
for y = 0, 30 do
local pos = {x=x, y=y, z=z}
minetest.remove_node(pos)
end
end
end
end
local function is_grass(px)
return px.G >= 150 and px.R < px.G and px.B < px.G
end
local function is_water(px)
return px.G < px.B and px.R < px.B and px.B >= 100
end
local function get_height(px)
if is_water(px) then
return 2
end
local b = get_lum(px)
return math.floor(b/10) + 3
end
local function near_water(img, z, x)
if z > 1 then
local p = img:getPixel(z-1, x)
if is_water(p) then
return true
end
end
if z < img.width then
local p = img:getPixel(z+1, x)
if is_water(p) then
return true
end
end
if x > 1 then
local p = img:getPixel(z, x-1)
if is_water(p) then
return true
end
end
if x < img.height then
local p = img:getPixel(z, x+1)
if is_water(p) then
return true
end
end
return false
end
local function generate_blocks(img)
clean()
for x = 1, img.height do
for z = 1, img.width do
p = img:getPixel(z, x)
h = get_height(p)
for y = 1, h do
pos = {x = x, y = y, z = z}
minetest.set_node(pos, blocks.dirt)
end
if is_water(p) then
b = blocks.water
elseif is_grass(p) then
b = blocks.grass
else
b = blocks.stone
end
if not is_water(p) and near_water(img, z, x) then
b = blocks.sand
end
pos = {x = x, y = h+1, z = z}
minetest.set_node(pos, b)
end
end
end
References
- Very dense Lua reference that I used to learn the language myself.
- Quite a basic intro into Minetest modding, just to get you started.
- More thorough intro into Minetest modding
- Minetest Lua API, an essential resource for any mod author.
- A much large scale mod that generates worlds from varios kinds of maps: realterrain.
- A demo of the mod in action on youtube: imagemap - a Minetest mod.
- Code of the imagemap mod.