The ComputerCraft Iceberg
My friend Steffen recently turned me on to the ComputerCraft mod for Minecraft.
For the uninitiated—a group I myself was a member of until a mere 24 hours ago—ComputerCraft is a mod that adds programmable computers and turtles to the game.
"Turtles, you say? What, like these fellas?"
Cute as they may be, the sea variety of turtles are not the ones I'm excited to talk about today.
Let me introduce you to a new kind of turtle:
These turtles—which get their name from turtle graphics—are little robots that you can control programatically. Inside of each one is a ComputerCraft computer. Players are able to write programs in Lua and execute those programs on the turtle.
Programs have access to a number of different APIs, including the turtle
module that provides functions for controlling the turtle.
For instance, calling the turtle.forward()
function will move the turtle forward. Calling turtle.dig()
will have the turtle dig the block in front of it.
Planting the seed sapling
It all started with a video Steffen sent me of a turtle-driven tree farm he had built in his world. The turtle would walk a loop around a patch of trees, checking each spot to see if a tree was grown yet. If it detected a grown tree, it would chop down the tree, replace it with a sapling, and continue on to the next spot.
I decided to start up a new Minecraft world to give it a go.
For my initial foray into working with turtles, I copied the tree farm program using the code that was visible in the video. I transcribed it, making a few tweaks as I went, and soon ended up with an automated tree farm of my own:
tree_farm.lua
-- https://github.com/maxdeviant/computercraft/blob/c295da15680d40884e5111a6048d46685eb3b80f/tree_farm.lua
log_kind = "minecraft:birch_log"
sapling_kind = "minecraft:birch_sapling"
track_kind = "minecraft:cobbled_deepslate"
tree_spacing = 2
function chop_tree()
print("Chopping tree...")
while true do
_, data = turtle.inspect()
if data.name ~= log_kind then
break
end
turtle.dig()
turtle.digUp()
turtle.up()
end
move_to_ground()
plant_sapling(sapling_kind)
end
function plant_sapling(sapling)
has_block, data = turtle.inspect()
if not has_block or data.name ~= sapling then
select_item(sapling)
turtle.place()
end
end
function select_item(item)
for i = 1, 16 do
turtle.select(i)
it = turtle.getItemDetail()
if it and it.name == item then
return true
end
end
return false
end
function move_to_ground()
while not turtle.detectDown() do
turtle.down()
end
end
function move_to_next_tree()
print("Moving to next tree...")
turtle.turnRight()
for i = 1, tree_spacing do
turtle.forward()
turtle.suck()
has_block, data = turtle.inspectDown()
if has_block and data.name ~= track_kind then
print("Turning around...")
turtle.turnLeft()
turtle.turnLeft()
turtle.forward()
turtle.turnLeft()
return
end
end
turtle.turnLeft()
end
function main()
while true do
chop_tree()
move_to_next_tree()
end
end
main()
During the course of building it and trying it out, I even managed to find a bug in the original program that needed fixing:
function plant_sapling(sapling)
has_block, data = turtle.inspect()
if not has_block or data.name ~= sapling then
- select_item(sapling)
- turtle.place()
+ if select_item(sapling) then
+ turtle.place()
+ end
end
end
With my wood situation sorted, I turned my attention to mining.
Initially I wanted to write a branch mining program to assist me in quickly finding more diamonds, but this proved to be somewhat complex. I scoped down the implementation to a simple tunnel miner that would mine a tunnel and place torches on the wall every so often:
tunnel_miner.lua
-- https://github.com/maxdeviant/computercraft/blob/798bfa95472f91d036acbf955a9eb534f7a74727/tunnel_miner.lua
torch = "minecraft:torch"
tunnel_width = 3
tunnel_height = 3
torch_spacing = 5
function has_item(item)
for i = 1, 16 do
it = turtle.getItemDetail(i)
if it and it.name == item then
return true
end
end
return false
end
function select_item(item)
for i = 1, 16 do
turtle.select(i)
it = turtle.getItemDetail()
if it and it.name == item then
return true
end
end
return false
end
function place_torch(height)
if not select_item(torch) then
return false
end
for _ = 1, height - 1 do
turtle.up()
end
turtle.place()
for _ = 1, height - 1 do
turtle.down()
end
return true
end
-- Mines a column of the specified height in front of the turtle.
--
-- Will return down to the starting location, once finished.
function mine_column(height)
for _ = 1, height - 1 do
turtle.dig()
turtle.digUp()
turtle.up()
end
turtle.dig()
for _ = 1, height do
turtle.down()
end
end
function mine_row(row)
mine_column(tunnel_height)
turtle.forward()
turtle.turnLeft()
for i = 1, tunnel_width - 1 do
mine_column(tunnel_height)
if i < tunnel_width - 1 then
turtle.forward()
end
end
if row % torch_spacing == 0 then
place_torch(2)
end
turtle.turnRight()
turtle.turnRight()
for _ = 1, tunnel_width - 1 do
turtle.forward()
end
turtle.turnLeft()
end
function main()
print("Starting tunnel miner")
local row = 0
while true do
if has_item(torch) then
mine_row(row)
row = row + 1
else
print("Out of torches")
sleep(10)
end
end
end
main()
It was at this point that my software engineer brain started screaming at me. I had these two working programs, but was already noticing common functions that were duplicated between the two.
I factored out a new inventory
module to house the helper functions I had written for dealing with the turtle's inventory:
lib/inventory.lua
-- https://github.com/maxdeviant/computercraft/blob/a0047c3844296d5e81cf5cdd1b71dbf195c09d1f/lib/inventory.lua
local inventory = {}
--- Returns whether the turtle has the specified item in its inventory.
---
--- @param item string The name of the item.
function inventory.has_item(item)
for slot = 1, 16 do
local it = turtle.getItemDetail(slot)
if it and it.name == item then
return true
end
end
return false
end
--- Selects the slot containing the specified item.
--- Returns whether the item was selected successfully.
---
--- @param item string The name of the item to select.
function inventory.select_item(item)
for slot = 1, 16 do
turtle.select(slot)
local it = turtle.getItemDetail()
if it and it.name == item then
return true
end
end
return false
end
return inventory
Keeping with the mining theme, the next program I wrote was for digging out vertical mine shafts.
I could imagine wanting to have different-sized mine shafts based on the need, so for this program I explored taking user input as arguments to the program:
lib/shaft_miner.lua
-- https://github.com/maxdeviant/computercraft/blob/53249f2890cc17024ff39801277d53f7278a4ef7/shaft_miner.lua
local function mine_shaft_layer(width, height)
for x = 1, width do
for _ = 1, height - 1 do
turtle.dig()
turtle.forward()
end
if x < width then
if x % 2 == 0 then
turtle.turnLeft()
turtle.dig()
turtle.forward()
turtle.turnLeft()
else
turtle.turnRight()
turtle.dig()
turtle.forward()
turtle.turnRight()
end
end
end
turtle.turnRight()
turtle.turnRight()
end
local function mine_shaft(depth, size)
for _ = 1, depth do
turtle.digDown()
turtle.down()
mine_shaft_layer(size, size)
end
end
local function main()
local depth = arg[1]
local size = arg[2]
print("Starting shaft miner")
print("Depth: " .. depth)
print("Size: " .. size .. "x" .. size)
mine_shaft(tonumber(depth), tonumber(size))
end
main()
While working on that program, I noticed that mine_shaft_layer
could be generalized into a general-purpose function. While in this case we care about mining out a layer of blocks, the core algorithm of moving a turtle around a plane could have lots of different uses.
I pulled this out into its own function:
--- Traverses a plane with the specified width and height, invoking the provided action at each block.
---
--- @param width number
--- @param height number
--- @param action fun(): nil
function move.traverse_plane(width, height, action)
for x = 1, width do
for _ = 1, height - 1 do
action()
turtle.forward()
end
if x < width then
if x % 2 == 0 then
turtle.turnLeft()
action()
turtle.forward()
turtle.turnLeft()
else
turtle.turnRight()
action()
turtle.forward()
turtle.turnRight()
end
end
end
turtle.turnRight()
turtle.turnRight()
end
This refactoring then enabled me to quickly whip up a new program for having a turtle farm wheat for me:
wheat_farmer.lua
local inventory = require("lib.inventory")
local move = require("lib.move")
local std = require("std")
local wheat = "minecraft:wheat"
local wheat_seeds = "minecraft:wheat_seeds"
local function is_above_wheat()
local has_block, data = turtle.inspectDown()
return has_block and data.name == wheat
end
local function is_wheat_grown()
local has_block, data = turtle.inspectDown()
if not has_block then
return false
end
if data.name ~= wheat then
return false
end
return data.state.age == 7
end
local function till_soil()
std.times(2, function()
turtle.digDown()
end)
end
local function plant_wheat()
if not inventory.select_item(wheat_seeds) then
return false
end
turtle.placeDown()
return true
end
local function harvest()
turtle.digDown()
end
local function main()
while true do
move.traverse_plane(9, 9, function()
if is_wheat_grown() then
harvest()
plant_wheat()
elseif is_above_wheat() then
-- Skip over it.
else
till_soil()
plant_wheat()
end
end)
end
end
main()
At this point it was bedtime, and I had wrapped up my first day of working with ComputerCraft.
I had gotten to grips with basics of Lua (as this was my first time using it in any real capacity), written a handful of different programs, pulled some common functionality into modules, and was feeling pretty happy with it all.
As I got ready for bed, I found myself pondering how I would maintain all of this code as I continued to expand my ComputerCraft usage.
Something I had observed during my first day was that I spent a lot of time testing my programs "in production", as it were.
The general flow of creating a new program looked something like:
- Write the first version of a program
- Run it on the turtle
- See something not work as expected
- Refine the program
- Rinse and repeat.
I spent a lot of time watching the turtle churn through its instructions waiting for it to reach the point in the program that needed testing and observation. I even created a separate Minecraft world that I would use to test my programs in before letting the turtles run them in my actual world.
The process was slow and time-consuming.
The answer to this, of course, was testing. I needed a way to write tests that I could run over and over as I made changes to the programs, and test that they were all still working in a variety of different scenarios.
Simulacrum
Bringing forth this vision of automated testing required one crucial component: a way to simulate ComputerCraft in a controlled environment.
I'd spent the previous day steeped in Lua, but I set it aside for a moment and broke ground on a new Rust project.
My initial idea for the simulator was quite simple: create a simplified representation of a Minecraft world, a simulated turtle that exists in that world, and an embedded Lua VM to run the programs.
A few hours of hacking later, and I could write tests like this:
#[test]
fn test_traverse_plane() {
let mut simulator = Simulator::new().unwrap();
set_script_root(&mut simulator);
simulator
.exec_lua(indoc! {r#"
local move = require "lib.move"
move.traverse_plane(3, 3, function()
end)
"#})
.unwrap();
assert_eq!(simulator.turtle().position, Position::new(2, 0, -2));
assert_eq!(simulator.turtle().direction, Direction::South);
assert_eq!(
simulator.turtle().position_history,
vec![
Position::new(0, 0, 0),
Position::new(0, 0, -1),
Position::new(0, 0, -2),
Position::new(1, 0, -2),
Position::new(1, 0, -1),
Position::new(1, 0, 0),
Position::new(2, 0, 0),
Position::new(2, 0, -1),
Position::new(2, 0, -2),
]
);
}
There's still more surface area that the simulator will need to cover, but I'm excited that I was able to prove out the concept quickly.
That's all for now, but I'll likely be writing more about my ComputerCraft adventures in the future.