Pong – complete code

The finished script from the Build Multiplayer Pong tutorial, ready to compare against your own build.

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

W, H       = 320, 180
PAD_W      = 4
PAD_H      = 28
PAD_SPEED  = 3
BALL_SIZE  = 4
WIN_SCORE  = 5

COL_BG    = 0    -- black
COL_LEFT  = 12   -- blue
COL_RIGHT = 8    -- red
COL_BALL  = 7    -- white

state   = "menu"   -- "menu" | "waiting" | "playing" | "over"
is_host = false
side    = nil      -- "left" (host) or "right" (joiner)

function _init()
  state   = "menu"
  is_host = false
  side    = nil
  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
-- ============================================================

function update_menu()
  if key_pressed("h") then
    state   = "waiting"
    is_host = true
    net.host({ max_players = 2, title = "Pong" }, 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 = 2, title = "Pong" }, on_connected)
  elseif key_pressed("j") then
    is_host = false
    net.join(on_connected)
  end
end

-- ============================================================
-- Session setup (runs once, on the successful host/join)
-- ============================================================

function on_connected()
  side = is_host and "left" or "right"

  if is_host then
    net.state.pads    = { left = (H - PAD_H) / 2, right = (H - PAD_H) / 2 }
    net.state.score   = { left = 0, right = 0 }
    net.state.playing = false
    reset_ball(1)

    net.on("peer.joined", function(playerId)
      net.state.playing = true
      print("Player " .. playerId .. " joined -- game on!")
    end)

    net.on("peer.left", function(playerId)
      net.state.playing = false
      print("Player " .. playerId .. " left -- waiting for a new opponent")
    end)
  end

  net.on("event:point", function(from, scorer)
    print("Point for the " .. scorer .. " side!")
  end)

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

  state = "playing"
  print("Connected! You are the " .. side .. " paddle (arrow keys).")
end

function reset_ball(direction)
  net.state.ball = {
    x  = (W - BALL_SIZE) / 2,
    y  = (H - BALL_SIZE) / 2,
    dx = 2 * direction,
    dy = 1.5,
  }
end

-- ============================================================
-- Gameplay: own paddle, host-simulated ball
-- ============================================================

function update_paddle()
  local pads = net.state.pads
  if not pads then
    return   -- state not replicated yet
  end

  local y = pads[side]
  if key_pressed("ArrowUp") then
    y = y - PAD_SPEED
  end
  if key_pressed("ArrowDown") then
    y = y + PAD_SPEED
  end

  pads[side] = clamp(y, 0, H - PAD_H)
end

function update_ball()
  if not net.state.playing or net.state.winner then
    return
  end

  local ball = net.state.ball
  local pads = net.state.pads

  ball.x = ball.x + ball.dx
  ball.y = ball.y + ball.dy

  -- Bounce off the top and bottom
  if ball.y <= 0 or ball.y >= H - BALL_SIZE then
    ball.dy = -ball.dy
  end

  -- Bounce off the paddles
  if ball.dx < 0 and ball.x <= 8 + PAD_W
     and ball.y + BALL_SIZE >= pads.left and ball.y <= pads.left + PAD_H then
    ball.dx = -ball.dx
  elseif ball.dx > 0 and ball.x + BALL_SIZE >= W - 8 - PAD_W
     and ball.y + BALL_SIZE >= pads.right and ball.y <= pads.right + PAD_H then
    ball.dx = -ball.dx
  end

  -- Out on the left: right scores (and vice versa)
  if ball.x < -BALL_SIZE then
    score_point("right", 1)
  elseif ball.x > W then
    score_point("left", -1)
  end
end

function score_point(scorer, serve_direction)
  net.state.score[scorer] = net.state.score[scorer] + 1
  net.emit("point", scorer)
  print("Point for the " .. scorer .. " side!")

  if net.state.score[scorer] >= WIN_SCORE then
    net.state.winner = scorer
  else
    reset_ball(serve_direction)
  end
end

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

function update_playing()
  update_paddle()

  if is_host then
    update_ball()
  end

  if net.state.winner then
    state = "over"
    print("The " .. net.state.winner .. " side wins! Press M for the menu.")
  end
end

function update_over()
  if key_pressed("m") then
    net.leave()
    _init()
  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" then
    update_over()
  end
end

-- ============================================================
-- Drawing (everything comes from net.state)
-- ============================================================

function draw_score()
  local score = net.state.score
  if not score then
    return
  end

  for i = 1, score.left do
    fill_rect(COL_LEFT, 8 + (i - 1) * 6, 6, 4, 4)
  end
  for i = 1, score.right do
    fill_rect(COL_RIGHT, W - 8 - (i - 1) * 6 - 4, 6, 4, 4)
  end
end

function draw_game()
  local pads = net.state.pads
  local ball = net.state.ball

  if pads then
    fill_rect(COL_LEFT, 8, pads.left, PAD_W, PAD_H)
    fill_rect(COL_RIGHT, W - 8 - PAD_W, pads.right, PAD_W, PAD_H)
  end

  if ball and net.state.playing and not net.state.winner then
    fill_rect(COL_BALL, ball.x, ball.y, BALL_SIZE, BALL_SIZE)
  end

  draw_score()
end

function _draw()
  clear(COL_BG)

  if state == "playing" then
    draw_game()
  elseif state == "over" and net.state.winner then
    -- Fill the screen with the winner's color
    local col = net.state.winner == "left" and COL_LEFT or COL_RIGHT
    fill_rect(col, 0, 0, W, H)
  else
    -- Menu / waiting: a centered "net" between two idle paddles
    fill_rect(COL_LEFT, 8, (H - PAD_H) / 2, PAD_W, PAD_H)
    fill_rect(COL_RIGHT, W - 8 - PAD_W, (H - PAD_H) / 2, PAD_W, PAD_H)
    for y = 0, H - 8, 12 do
      fill_rect(5, W / 2 - 1, y, 2, 8)
    end
  end
end