Marshall Bowers

Conjurer of code. Devourer of art. Pursuer of æsthetics.

The ComputerCraft Iceberg

Saturday, July 5, 2025
1,860 words
10 minute read

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?"

A turtle in Minecraft.

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:

A ComputerCraft turtle in Minecraft.

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:

  1. Write the first version of a program
  2. Run it on the turtle
  3. See something not work as expected
  4. Refine the program
  5. 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.