Learning

Lua + LÖVE: Make a Game · Lesson 11 · 45 min

Save, debug, and ship

Persist a high score, diagnose failures systematically, package a .love file, and choose your next project.

Learn

Persistence, debugging, QA, and packaging.

Build

A saved high score and runnable archive.

Prove it

Test from a clean launch and share the game.

A game is finished when someone can launch it, understand it, play it, restart it, and return later without your editor open. This final lesson turns a working project into a small deliverable.

Save only the durable value

The current score belongs to one run. The high score belongs to the whole installation.

Add or confirm this state in game.lua:

local Game = {
  -- other state...
  highScore = 0,
}

Load it once in Game.load:

if love.filesystem.getInfo("highscore.txt", "file") then
  local contents = love.filesystem.read("highscore.txt")
  Game.highScore = tonumber(contents) or 0
end

Write only when a run beats it:

local function saveHighScore()
  if Game.score <= Game.highScore then
    return
  end

  Game.highScore = Game.score
  local success, message = love.filesystem.write(
    "highscore.txt",
    tostring(Game.highScore)
  )

  if not success then
    print("Could not save high score: " .. tostring(message))
  end
end

Call saveHighScore() when entering game over. Draw Game.highScore on the result overlay.

LÖVE writes to a per-game save directory, not beside your source. The identity in conf.lua keeps that location stable:

t.identity = "orbit-runner"

Print the exact location while debugging:

print(love.filesystem.getSaveDirectory())

Treat save data as untrusted input

The file may be missing, empty, or edited. getInfo handles absence, and tonumber(contents) or 0 gives invalid text a safe fallback.

Debug from evidence

When something fails, reduce uncertainty in a fixed order.

1. Read the first useful error

LÖVE’s error screen includes a message and stack trace. Start with the first line naming one of your files. Later errors are often consequences.

Common translations:

Error fragment Likely meaning
expected 'end' a block or function was not closed
attempt to index ... (a nil value) the table or field before the dot is missing
attempt to perform arithmetic ... nil a number was never initialized or a field name is wrong
module 'player' not found filename/path does not match require
bad argument #... a LÖVE function received the wrong type or argument order

Use the debugging field guide for a longer decision path.

2. Prove which branch runs

Add temporary, specific output:

print("meteor hit", index, "lives before", Game.lives)

Avoid a stream of vague print("here") messages. Include the event and values needed to test your belief.

3. Draw hidden state

Games have spatial and time-based state that text logs explain poorly. Draw collision circles, entity counts, state names, and timer values on-screen.

love.graphics.print("state: " .. Game.state, 20, 46)
love.graphics.print("meteors: " .. #Game.meteors, 20, 70)

4. Assert invariants

An assertion fails exactly where an impossible value first appears:

assert(Game.lives >= 0, "lives fell below zero")
assert(Game.player, "playing state requires a player")

Use assertions for programmer mistakes, not ordinary player actions.

5. Remove half the possibilities

If a large feature fails, temporarily bypass effects, audio, or one entity type. Determine which half contains the bug, then repeat. This is faster than rereading everything.

Test the player’s journey

Run this manual smoke test from a fresh launch:

Test the packaged archive too. Packaging errors can exist even when the source folder works.

Build a .love archive

A .love file is a ZIP archive with main.lua at its root. From inside the orbit-runner folder:

zip -9 -r ../orbit-runner.love .

Then run it:

love ../orbit-runner.love

The most common packaging mistake is placing the project folder itself inside the archive:

wrong: orbit-runner/main.lua
right: main.lua

Open the archive with a ZIP viewer if LÖVE reports that no game exists.

The finished course archive is available for comparison: download Orbit Runner.

For a platform-specific executable, follow LÖVE’s official game distribution guide. A .love file is the portable project payload; distribution steps bundle it with the engine for players who do not have LÖVE installed.

What you now know

You built one complete loop of game development:

values → rules → real-time update → drawing → input
       → entities → collision → states → feedback
       → modules → persistence → package

More importantly, you learned recurring patterns:

Choose the next game

Do not expand Orbit Runner forever. Build a second small game and transfer the patterns.

  1. Breakout: rectangle collision, velocity reflection, level grids.
  2. Asteroids: rotation, acceleration, screen wrapping, projectiles.
  3. Top-down dungeon room: sprite animation, tile maps, enemy steering.
  4. One-button rhythm game: beat timing, input windows, audio synchronization.

Keep the scope to one screen and one core verb. Finish it, package it, then make the next one slightly more ambitious.

Course complete

You are done when your packaged archive passes the smoke test from a clean launch and another person can start, move, understand the goal, lose, restart, and quit without instruction from you.