Create: Computing AgesBrass Docs
Built-in programs

minecraft

Minecraft in Minecraft (2D)

Runs on PC+ Personal Computer, Microcontroller, Modern Computer · 510 lines
Type this at the prompt of a computer to copy it onto the medium:
Terminal
> examples minecraft
> edit minecraft
Screen
minecraft

The program

minecraft
-- Minecraft in Minecraft: a world to explore, mine and build, drawn with gfx.
-- Left click breaks a block, right click places the chosen block.
-- On a monitor (one kind of click): the pickaxe breaks, a block places.
-- The sky follows the time of your world: night falls here too.
local size = gfx.size()
local PW = size.w
local PH = size.h
local T = 8  -- a block is 8 x 8 pixels
local TW = PW // T  -- blocks across the screen
local WH = (PH - 14) // T  -- rows of blocks; the hotbar is below
local WW = TW * 4  -- the world is four screens wide
local HUD = WH * T + 1  -- first row of pixels of the hotbar

local AIR = 0
local GRASS = 1
local DIRT = 2
local STONE = 3
local SAND = 4
local WOOD = 5
local LEAVES = 6
local PLANKS = 7
local WATER = 8
local COAL = 9
local IRON = 10
local DIAMOND = 11
local BEDROCK = 12

-- Textures: one hex digit per pixel, the colours of term.colors: 0 white,
-- 1 orange, 4 yellow, 5 lime, 6 pink, 7 gray, 8 light gray, 9 cyan,
-- b blue, c brown, d green, f black. "." lets the sky show through.
local TEX = {}
TEX[GRASS] = {"55d5555d", "5ddd5dd5", "dcdcddcd", "cc1ccccc", "ccccc1cc", "c1cccccc", "cccc1ccc", "ccccccc1"}
TEX[DIRT] = {"cc1ccccc", "ccccc1cc", "c1ccccc7", "ccc7cccc", "cccccc1c", "c1cccccc", "cccc1ccc", "7ccccc1c"}
TEX[STONE] = {"87778777", "77787787", "78777777", "77777877", "77877778", "87778777", "77777787", "78777777"}
TEX[SAND] = {"44404444", "44444414", "40444444", "44441444", "44444404", "41444444", "44440444", "44444441"}
TEX[WOOD] = {"cfcccfcc", "cfcccfcc", "ccfccfcc", "ccfcccfc", "cfccccfc", "cfcccfcc", "ccfccfcc", "ccfcccfc"}
TEX[LEAVES] = {"d5dd.dd5", "dd5ddd.d", "5dd.d5dd", "dd5dddd.", ".ddd5d5d", "d5d.ddd5", "dd5dd5dd", "5d.dd.dd"}
TEX[PLANKS] = {"11111111", "1111c111", "cccccccc", "11c11111", "11111111", "111111c1", "cccccccc", "1c111111"}
TEX[WATER] = {"bbbbbbbb", "b33bbb3b", "bbbbbbbb", "bbb33bbb", "bbbbbbbb", "33bbbb33", "bbbbbbbb", "bbb3bbbb"}
TEX[COAL] = {"87778777", "7ff77787", "7fff7777", "777778f7", "778777ff", "8777f777", "77fff787", "78777777"}
TEX[IRON] = {"87778777", "76677787", "77167777", "77777667", "77877716", "86777777", "71677787", "78777777"}
TEX[DIAMOND] = {"87778777", "79977787", "77097777", "77777997", "77877709", "89777777", "70977787", "78777777"}
TEX[BEDROCK] = {"f7ff7f8f", "7ff7fff7", "ff8f7fff", "7fff7f7f", "f7f8ff7f", "ff7fff7f", "7fff8fff", "ff7f7ff7"}
local STARS = {"ffffffff", "ffffff0f", "ffffffff", "ff8fffff", "ffffffff", "fffff8ff", "f0ffffff", "ffffffff"}
local PICKAXE = {".8888...", "8...88..", "....c8..", "...c..8.", "..c....8", ".c......", "c.......", "........"}
local STEVE = {
  "cccccccc", "cccccccc", "c666666c", "66666666", "60f66f06", "66666666", "666cc666", "66666666",
  "99999999", "69999996", "69999996", "69999996", ".bbbbbb.", ".bbbbbb.", ".bb..bb.", ".77..77.",
}

local SLOTS = {0, DIRT, STONE, SAND, WOOD, PLANKS, LEAVES}  -- 0 is the pickaxe
local ORES = {COAL, IRON, DIAMOND}
local world = {}
local top = {}  -- surface row of each column
local tree = {}  -- columns with a tree
local made = {}  -- columns whose blocks exist
local sea = 0
local inv = {}
local slot = 1
local px = 0
local py = 0  -- the feet; the head is the block above
local cx = 0  -- the screen shows columns cx + 1 to cx + TW
local jump = 0
local hang = 0
local night = false

-- The shape of the world is decided at once: the height of each column
-- (hills, and beaches below the sea level) and where trees grow.
local function plan()
  sea = math.floor(WH * 0.6)
  local s = math.floor(WH * 0.45)
  local last = -10
  for x = 1, WW do
    if math.random(3) == 1 then
      s = s + math.random(-1, 1)
    end
    s = math.max(math.floor(WH * 0.3), math.min(WH - 4, s))
    top[x] = s
    if x >= 3 and x <= WW - 2 and s < sea and s >= 6 and x - last >= 5 and math.random(4) == 1 then
      tree[x] = true
      last = x
    end
  end
end

-- The blocks of a column are only made the first time it is needed, like the
-- chunks of Minecraft: the game starts at once, whatever the size of the world.
local function make(x)
  made[x] = true
  local s = top[x]
  local i = x
  for y = 1, WH do
    local id = AIR
    if y == WH then
      id = BEDROCK
    elseif y < s then
      if y >= sea then
        id = WATER
      end
    elseif y <= s + 2 then
      if s >= sea then
        id = SAND
      elseif y == s then
        id = GRASS
      else
        id = DIRT
      end
    else
      id = STONE
      local r = math.random(100)
      if r <= 6 then
        id = COAL
      elseif r <= 9 then
        id = IRON
      elseif r >= 98 and y >= WH - 3 then
        id = DIAMOND
      end
    end
    world[i] = id
    i = i + WW  -- the same column, one row lower
  end
  -- the trunk of its tree, and the leaves of the trees next to it
  for c = x - 1, x + 1 do
    if tree[c] then
      local ts = top[c]
      for k = 3, 4 do
        if world[(ts - k - 1) * WW + x] == AIR then
          world[(ts - k - 1) * WW + x] = LEAVES
        end
      end
      if c == x then
        for k = 1, 3 do
          world[(ts - k - 1) * WW + x] = WOOD
        end
        world[(ts - 6) * WW + x] = LEAVES
      end
    end
  end
end

local function get(x, y)
  if x < 1 or x > WW or y > WH then
    return BEDROCK
  end
  if y < 1 then
    return AIR
  end
  if not made[x] then
    make(x)
  end
  return world[(y - 1) * WW + x]
end

local function set(x, y, id)
  world[(y - 1) * WW + x] = id
end

-- The player fits in air and water only.
local function free(x, y)
  local id = get(x, y)
  return y >= 1 and (id == AIR or id == WATER)
end

-- Draws one block of the world if it is on screen: the sky first, then its texture.
local function drawTile(x, y)
  local sx = x - cx
  if sx < 1 or sx > TW or y < 1 or y > WH then
    return
  end
  local left = (sx - 1) * T + 1
  local up = (y - 1) * T + 1
  local id = get(x, y)
  if id == AIR or id == LEAVES then
    if night then
      gfx.image(left, up, STARS)
    else
      gfx.rect(left, up, T, T, "light_blue", true)
    end
  end
  if id ~= AIR then
    gfx.image(left, up, TEX[id])
  end
end

local function drawPlayer()
  gfx.image((px - cx - 1) * T + 1, (py - 2) * T + 1, STEVE)
end

-- A whole column at once: one rectangle of sky, then only the blocks that are not air.
local function drawColumn(x)
  local sx = x - cx
  if sx < 1 or sx > TW then
    return
  end
  if not made[x] then
    make(x)
  end
  local left = (sx - 1) * T + 1
  if night then
    gfx.rect(left, 1, T, WH * T, "black", true)
  else
    gfx.rect(left, 1, T, WH * T, "light_blue", true)
  end
  local i = x
  for y = 1, WH do
    local id = world[i]
    if id ~= AIR then
      gfx.image(left, (y - 1) * T + 1, TEX[id])
    elseif night then
      gfx.image(left, (y - 1) * T + 1, STARS)
    end
    i = i + WW  -- one row lower
  end
end

local function drawWorld()
  for sx = 1, TW do
    drawColumn(cx + sx)
  end
  drawPlayer()
end

-- Moves the player. Near an edge the world slides by one block: gfx.scroll
-- moves the picture, then only the new column is drawn (fast).
local function moveTo(x, y)
  local ox = px
  local oy = py
  px = x
  py = y
  local d = 0
  if px - cx <= TW // 3 and cx > 0 then
    d = -1
  elseif px - cx > TW - TW // 3 and cx < WW - TW then
    d = 1
  end
  if d ~= 0 then
    cx = cx + d
    gfx.scroll(-d * T, 0, 1, 1, TW * T, WH * T)
    if d > 0 then
      drawColumn(cx + TW)
    else
      drawColumn(cx + 1)
    end
  end
  drawTile(ox, oy)
  drawTile(ox, oy - 1)
  drawPlayer()
end

local function onGround()
  return not free(px, py + 1)
end

local function walk(dx)
  local x = px + dx
  if free(x, py) and free(x, py - 1) then
    moveTo(x, py)
  elseif onGround() and free(x, py - 1) and free(x, py - 2) and free(px, py - 2) then
    moveTo(x, py - 1)  -- steps up one block, like auto-jump
  end
end

local function tryJump()
  if onGround() or get(px, py) == WATER then
    jump = 1
  end
end

-- Every 0.15 s: jump, gravity, and day or night.
local function physics()
  if jump > 0 then
    jump = jump - 1
    if free(px, py - 2) then
      moveTo(px, py - 1)
    end
    hang = 1  -- a moment at the top, to land on a block
  elseif hang > 0 then
    hang = hang - 1
  elseif free(px, py + 1) then
    moveTo(px, py + 1)
    if get(px, py) == WATER then
      hang = 1  -- sinks slowly in water
    end
  end
  local t = os.day_time()
  local dark = t >= 13000 and t < 23000
  if dark ~= night then
    night = dark
    drawWorld()
  end
end

-- Sand falls, like in Minecraft.
local function settle(x, y)
  while get(x, y) == SAND and y < WH and free(x, y + 1) and not (x == px and y + 1 >= py - 1 and y + 1 <= py) do
    set(x, y + 1, SAND)
    set(x, y, AIR)
    drawTile(x, y)
    drawTile(x, y + 1)
    y = y + 1
  end
end

local function reach(x, y)
  return math.abs(x - px) <= 4 and y >= py - 5 and y <= py + 3
end

local function count(x, n)
  gfx.rect(x, HUD + 6, 8, 5, "gray", true)
  gfx.text(x, HUD + 6, tostring(math.min(n, 99)), "white")
end

local function drawSlot(i)
  local x = 2 + (i - 1) * 22
  local id = SLOTS[i]
  gfx.rect(x, HUD + 2, 12, 12, "black", true)
  if i == slot then
    gfx.rect(x, HUD + 2, 12, 12, "white")
  end
  if id == 0 then
    gfx.image(x + 2, HUD + 4, PICKAXE)
  else
    gfx.image(x + 2, HUD + 4, TEX[id])
    count(x + 13, inv[id])
  end
end

local function drawHud()
  gfx.rect(1, HUD, PW, PH - HUD + 1, "gray", true)
  for i = 1, #SLOTS do
    drawSlot(i)
  end
  for i = 1, #ORES do
    local x = 2 + #SLOTS * 22 + (i - 1) * 22
    gfx.image(x, HUD + 4, TEX[ORES[i]])
    count(x + 10, inv[ORES[i]])
  end
end

local function mine(x, y)
  local id = get(x, y)
  if not reach(x, y) or id == AIR or id == WATER or id == BEDROCK then
    return
  end
  local item = id
  if id == GRASS then
    item = DIRT  -- grass gives dirt
  end
  inv[item] = inv[item] + 1
  set(x, y, AIR)
  drawTile(x, y)
  local k = y - 1
  while k >= 1 and get(x, k) == SAND do
    settle(x, k)
    k = k - 1
  end
  drawHud()
end

local function place(x, y)
  local id = SLOTS[slot]
  if id == 0 or inv[id] < 1 or not reach(x, y) or not free(x, y) then
    return
  end
  if x == px and (y == py or y == py - 1) then
    return
  end
  inv[id] = inv[id] - 1
  set(x, y, id)
  drawTile(x, y)
  settle(x, y)
  drawSlot(slot)
end

local function choose(i)
  local old = slot
  slot = i
  drawSlot(old)
  drawSlot(slot)
end

-- C turns one wood into four planks, like the crafting grid.
local function craft()
  if inv[WOOD] > 0 then
    inv[WOOD] = inv[WOOD] - 1
    inv[PLANKS] = inv[PLANKS] + 4
    drawHud()
  end
end

local function onKey(e)
  if e.name == "key" then
    if e.key == "left" then
      walk(-1)
    elseif e.key == "right" then
      walk(1)
    elseif e.key == "up" then
      tryJump()
    end
    return
  end
  local ch = string.lower(e.char)
  if ch == "q" or ch == "a" then
    walk(-1)
  elseif ch == "d" then
    walk(1)
  elseif ch == " " or ch == "z" or ch == "w" then
    tryJump()
  elseif ch == "c" then
    craft()
  else
    local n = tonumber(ch)
    if n and n >= 1 and n <= #SLOTS then
      choose(n)
    end
  end
end

-- e.px, e.py: the pixel clicked; e.button: 1 left, 2 right.
local function onClick(e)
  if e.py >= HUD then
    local i = (e.px - 2) // 22 + 1
    if i >= 1 and i <= #SLOTS then
      choose(i)
    end
    return
  end
  local x = cx + (e.px - 1) // T + 1
  local y = (e.py - 1) // T + 1
  if e.button == 2 or (e.source == "monitor" and SLOTS[slot] ~= 0) then
    place(x, y)
  else
    mine(x, y)
  end
end

-- Starts on the grass closest to the middle of the world.
local function spawn()
  local mid = WW // 2
  local d = 0
  while d < mid - 1 and get(mid + d, top[mid + d]) ~= GRASS and get(mid - d, top[mid - d]) ~= GRASS do
    d = d + 1
  end
  px = mid - d
  if get(mid + d, top[mid + d]) == GRASS then
    px = mid + d
  end
  py = top[px] - 1
  while py > 2 and not (free(px, py) and free(px, py - 1)) do
    py = py - 1  -- on top of a tree if there is one
  end
  cx = math.max(0, math.min(WW - TW, px - TW // 2))
end

-- The title screen: a dirt background and a logo made of pixels.
term.set_bg(term.colors.black)
term.clear()
for y = 1, PH, T do
  for x = 1, PW, T do
    gfx.image(x, y, TEX[DIRT])
  end
end
local logo = (PW - 105) // 2
gfx.text(logo + 2, 8, "MINECRAFT", "black", 3)
gfx.text(logo, 6, "MINECRAFT", "light_gray", 3)
gfx.text(logo + 70, 25, "ALSO IN 2D!", "yellow")
gfx.rect(1, 37, PW, 63, "black", true)
term.set_fg(term.colors.white)
term.set_cursor(2, 5)
term.write("Arrows, Q or D: walk")
term.set_cursor(2, 6)
term.write("Space or Z: jump")
term.set_cursor(2, 7)
term.write("Left click: break a block")
term.set_cursor(2, 8)
term.write("Right click: place the chosen block")
term.set_cursor(2, 9)
term.write("1 to 7: choose, C: craft planks")
term.set_cursor(2, 10)
term.set_fg(term.colors.yellow)
term.write("A key or a click to play")
local started = false
while not started do
  local e = os.pull_event()
  started = e.name == "key" or e.name == "char" or e.name == "click"
end
term.clear()
term.set_cursor(2, 2)
term.write("Generating the world...")
plan()
term.clear()
for id = 0, BEDROCK do
  inv[id] = 0
end
spawn()
gfx.clear("black")
drawWorld()
drawHud()
os.start_timer(0.15)
while true do
  local e = os.pull_event()
  if e.name == "timer" then
    physics()
    os.start_timer(0.15)
  elseif e.name == "key" or e.name == "char" then
    onKey(e)
  elseif e.name == "click" then
    onClick(e)
  end
end