Coin Rush – complete code

The finished script from the Build a Coin Rush tutorial, ready to compare against your own build.

-- ============================================================
-- Constants and local state
-- ============================================================

W, H        = 320, 180
PLAYER_SIZE = 8
COIN_SIZE   = 4
SPEED       = 2
COIN_COUNT  = 5
WIN_SCORE   = 10
RESPAWN_DELAY = 120           -- frames (two seconds at 60 FPS)

COL_BG    = 0                 -- black
COL_COIN  = 10                -- yellow
COLORS    = { 8, 12, 11, 14 } -- red, blue, green, pink

state         = "menu"        -- "menu" | "waiting" | "playing" | "over"
is_host       = false
next_color    = 1             -- host only: next entry of COLORS to hand out
respawn_timer = 0             -- host only
claiming      = {}            -- coins we already have a pending lock request for

function _init()
  state      = "menu"
  is_host    = false
  next_color = 1
  respawn_timer = 0
  claiming   = {}
  print("Press H to host a game, J to join one")
end

function clamp(v, lo, hi)
  if v < lo then return lo end
  if v > hi then return hi end
  return v
end

-- ============================================================
-- Session menu (same shape as the Pong tutorial)
-- ============================================================

function update_menu()
  if key_pressed("h") then
    state   = "waiting"
    is_host = true
    net.host({ max_players = 4, title = "Coin rush" }, on_connected)
  elseif key_pressed("j") then
    state   = "waiting"
    is_host = false
    net.join(on_connected)
  end
end

function update_waiting()
  -- Cancelling a dialog fires no callback, so the game stays here. Let the
  -- player re-open host/join directly, or press M to return to the menu.
  if key_pressed("m") then
    state = "menu"
    print("Press H to host a game, J to join one")
  elseif key_pressed("h") then
    is_host = true
    net.host({ max_players = 4, title = "Coin rush" }, on_connected)
  elseif key_pressed("j") then
    is_host = false
    net.join(on_connected)
  end
end

-- ============================================================
-- Session setup: the host provisions players and coins
-- ============================================================

function provision_player(playerId)
  local entry = {
    x     = math.random(8, W - 8 - PLAYER_SIZE),
    y     = math.random(8, H - 8 - PLAYER_SIZE),
    col   = COLORS[next_color],
    score = 0,
  }
  next_color = next_color % #COLORS + 1

  -- A net.state branch only exists once it holds a value, so the first
  -- player entry must create the branch itself
  if net.state.players then
    net.state.players[playerId] = entry
  else
    net.state.players = { [playerId] = entry }
  end
end

function new_coin()
  return {
    x     = math.random(8, W - 8 - COIN_SIZE),
    y     = math.random(8, H - 8 - COIN_SIZE),
    taken = false,
    lock  = net.lock(),   -- each coin guards itself; lives in net.state.coins[i]
  }
end

function on_connected()
  if is_host then
    provision_player(net.id())

    local coins = {}
    for i = 1, COIN_COUNT do
      coins[i] = new_coin()
    end
    net.state.coins = coins

    net.state.respawns = net.queue()   -- shared work queue, host pops from it

    net.on("peer.joined", function(playerId)
      provision_player(playerId)
      print("Player " .. playerId .. " joined")
    end)

    net.on("peer.left", function(playerId)
      net.state.players[playerId] = nil
      print("Player " .. playerId .. " left")
    end)
  end

  net.on("ended", function()
    state = "over"
    print("The host closed the session. Press M for the menu.")
  end)

  state = "playing"
  print("Connected! Collect " .. WIN_SCORE .. " coins to win (arrow keys).")
end

-- ============================================================
-- Movement
-- ============================================================

function my_player()
  local players = net.state.players
  if not players then
    return nil
  end
  return players[net.id()]
end

function update_movement()
  local me = my_player()
  if not me then
    return   -- the host has not provisioned us yet
  end

  if key_pressed("ArrowLeft")  then me.x = me.x - SPEED end
  if key_pressed("ArrowRight") then me.x = me.x + SPEED end
  if key_pressed("ArrowUp")    then me.y = me.y - SPEED end
  if key_pressed("ArrowDown")  then me.y = me.y + SPEED end

  me.x = clamp(me.x, 0, W - PLAYER_SIZE)
  me.y = clamp(me.y, 0, H - PLAYER_SIZE)
end

-- ============================================================
-- Collecting coins: one winner per coin, thanks to net.lock
-- ============================================================

function overlaps_coin(me, coin)
  return me.x < coin.x + COIN_SIZE and me.x + PLAYER_SIZE > coin.x
     and me.y < coin.y + COIN_SIZE and me.y + PLAYER_SIZE > coin.y
end

function try_collect(i)
  if claiming[i] then
    return   -- we already have a pending request for this coin
  end
  claiming[i] = true

  net.state.coins[i].lock.acquire(function(release)
    claiming[i] = false

    local coin = net.state.coins[i]
    if not coin.taken then             -- still there: it is ours
      coin.taken = true
      local me = my_player()
      me.score = me.score + 1
      net.state.respawns.push(i)       -- ask the host for a replacement

      if me.score >= WIN_SCORE then
        net.state.winner = net.id()
      end
    end

    release()
  end)
end

function update_collect()
  local me    = my_player()
  local coins = net.state.coins
  if not me or not coins then
    return
  end

  for i = 1, COIN_COUNT do
    local coin = coins[i]
    if coin and not coin.taken and overlaps_coin(me, coin) then
      try_collect(i)
    end
  end
end

-- ============================================================
-- Respawns: a work queue processed by the host
-- ============================================================

function update_respawns()
  respawn_timer = respawn_timer + 1
  if respawn_timer < RESPAWN_DELAY then
    return
  end
  respawn_timer = 0

  net.state.respawns.pop(function(i)
    if i and not net.state.winner then
      net.state.coins[i] = new_coin()
    end
  end)
end

-- ============================================================
-- Game loop
-- ============================================================

function update_playing()
  update_movement()
  update_collect()

  if is_host then
    update_respawns()
  end

  if net.state.winner then
    state = "over"
    if net.state.winner == net.id() then
      print("You win! Press M for the menu.")
    else
      print("Player " .. net.state.winner .. " wins. Press M for the menu.")
    end
  end
end

function _update()
  if state == "menu" then
    update_menu()
  elseif state == "waiting" then
    update_waiting()
  elseif state == "playing" then
    update_playing()
  elseif state == "over" and key_pressed("m") then
    net.leave()
    _init()
  end
end

-- ============================================================
-- Drawing
-- ============================================================

function draw_players()
  local row = 0
  for id, p in pairs(net.state.players or {}) do
    fill_rect(p.col, p.x, p.y, PLAYER_SIZE, PLAYER_SIZE)

    if tonumber(id) == net.id() then
      rect(7, p.x - 2, p.y - 2, PLAYER_SIZE + 4, PLAYER_SIZE + 4)   -- highlight yourself
    end

    for i = 1, p.score do
      fill_rect(p.col, 4 + (i - 1) * 6, 4 + row * 8, 4, 4)
    end
    row = row + 1
  end
end

function draw_coins()
  local coins = net.state.coins
  if not coins then
    return
  end

  for i = 1, COIN_COUNT do
    local coin = coins[i]
    if coin and not coin.taken then
      fill_rect(COL_COIN, coin.x, coin.y, COIN_SIZE, COIN_SIZE)
    end
  end
end

function _draw()
  clear(COL_BG)

  if state == "playing" or state == "over" then
    draw_coins()
    draw_players()
  else
    -- Menu / waiting: show one square per possible player
    for i = 1, #COLORS do
      fill_rect(COLORS[i], 60 * i + 20, (H - PLAYER_SIZE) / 2, PLAYER_SIZE, PLAYER_SIZE)
    end
  end
end