Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Playdate game architecture patterns — push-down state machine, screen lifecycle hooks (onEnter, onExit, onPause, onResume), models/systems vs screens/ui module separation, and the 30 FPS update budget. Use when designing, structuring, or reviewing the architecture of a Playdate game.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 33% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 19% | 0% |
| case-08 | ✗→✓ | ▲ Improved | -2% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 24% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 18% | 0% |
The recommended pattern for Playdate games is a push-down automaton managing a stack of screens. This enables nested state — pausing to show a menu without destroying the game state beneath.
lua-- src/core/StateMachine.lua StateMachine = {} StateMachine.__index = StateMachine function StateMachine.new() return setmetatable({ stack = {} }, StateMachine) end -- Push a new state; pause the current one function StateMachine:push(state) local current = self.stack[#self.stack] if current and current.onPause then current:onPause() end table.insert(self.stack, state) if state.onEnter then state:onEnter() end end -- Pop current state; resume the one below function StateMachine:pop() local current = table.remove(self.stack) if current and current.onExit then current:onExit() end local next = self.stack[#self.stack] if next and next.onResume then next:onResume() end end -- Replace current state (no stack growth) function StateMachine:swap(state) local current = table.remove(self.stack) if current and current.onExit then current:onExit() end table.insert(self.stack, state) if state.onEnter then state:onEnter() end end function StateMachine:update() local s = self.stack[#self.stack] if s and s.update then s:update() end end function StateMachine:draw() local s = self.stack[#self.stack] if s and s.draw then s:draw() end end
| Hook | Called when | |------|-------------| | onEnter() | Screen pushed onto the stack | | onExit() | Screen popped off the stack | | onPause() | Another screen pushed on top | | onResume() | Top screen popped; this screen is now active | | update() | Every frame (30 FPS), delegated by the state machine | | draw() | Every frame (or on demand) |
lua-- src/screens/TitleScreen.lua TitleScreen = {} TitleScreen.__index = TitleScreen function TitleScreen.new() return setmetatable({}, TitleScreen) end function TitleScreen:onEnter() -- initialize input handlers, UI state end function TitleScreen:onExit() -- cleanup end function TitleScreen:update() if playdate.buttonJustPressed(playdate.kButtonA) then Game:push(BattleScreen.new()) end end
| Layer | May call playdate.*? | Testable on desktop Lua? | |-------|----------------------|-------------------------| | models/ | No | Yes | | systems/ | No | Yes | | screens/ | Yes | No | | ui/ | Yes | No | | persistence/ | Yes | No |
Never call playdate.* from models or systems. This rule enables unit tests to run on desktop Lua without the simulator.
src/
main.lua Entry point: init Game (StateMachine), import all modules, push first screen
core/
StateMachine.lua Push-down automaton
screens/ Title, Battle, Result — Playdate API consumers, own the game loop per state
models/ Pure data + logic (Actor, Battle) — no Playdate API
systems/ Stateless logic operating on models — no Playdate API
ui/ Rendering helpers — Playdate API consumers
data/ Lua-native content tables (enemies, items, encounters)
persistence/ SaveManager — wraps playdate.datastore
tests/
*_test.lua Desktop-runnable unit tests (models + systems only)playdate.update() is called 30 times per second. Each call must complete within ~33ms.
playdate.getMemoryStats() and playdate.getStats()playdate.graphics.sprite.update() batches all active sprite updates — call once per frameluaimport "CoreLibs/sprites" import "CoreLibs/graphics" import "CoreLibs/timer" import "core/StateMachine" import "screens/TitleScreen" -- import all other modules... Game = StateMachine.new() Game:push(TitleScreen.new()) function playdate.update() Game:update() playdate.graphics.sprite.update() playdate.timer.updateTimers() end
Game content lives as plain Lua tables in src/data/, not JSON or YAML:
lua-- src/data/enemies.lua ENEMY_DEFS = { goblin_thug = { name = "Goblin Thug", maxHP = 30, speed = 4, attack = 8 }, goblin_rogue = { name = "Goblin Rogue", maxHP = 20, speed = 8, attack = 10 }, }
This avoids a JSON parsing step and keeps content editable without a pipeline.
Other measured skills in the registry, with their headline benchmark lift.