Skip to content

Playdate SDK + Pulp (Panic)

Versions at capture: SDK 3.1.1 (2026-07-22, actively shipping). Pulp last changed 2024-10-11 (maintained, quiet). Season Three announced April 2026 for late 2026. Batch: 11 (peer-product authoring platform, 2026-07-23; sibling to zork and strudel)

The KN-86 has a cart-authoring story of its own: Lisp source in a .kn86 container (ADR-0006), 54 device primitives exposed to cart Lisp (ADR-0005), a constrained cell tier the carts draw through (ADR-0027 / ADR-0036), and a kn86cart packager that bundles source. Panic has shipped and iterated the same shape of problem for four years on a device with a comparable envelope. This is the most directly comparable prior art we have for the developer-facing half of the product.

Prior coverage in this repo covers the company and skips the SDK. marketing-plan.md §Competitive Landscape names Playdate “the closest precedent” and does the positioning work; update-system.md studies their USB update transport (the fwup serial command). Neither looks at how people author for the thing. This file fills that gap.


Panic gives developers three downloads: the SDK (“Lua and C APIs, docs, as well as a Simulator for local development, with profiling and more”), Pulp (“a click-and-place game maker”), and Playdate Mirror (a desktop app that mirrors a connected device’s screen for capture and streaming). The SDK and Pulp are separate products with separate audiences, separate languages, and separate performance ceilings. They meet at exactly one file format: both emit a .pdx bundle, and the device cannot tell which tool built it.


[myProjectName]/
source/
main.lua
...other .lua files
images/
sounds/
support/
Project files

“Place all scripts and assets together in a single project directory. Your source directory must, at minimum, contain one Lua script called main.lua.”

One command builds it:

$ pdc MyGameSource MyGame.pdx

.pdx is a directory that Finder renders as a single-icon bundle. Metadata lives in a plain-text pdxinfo at the project root: name, author, bundleID (reverse DNS), version, buildNumber (monotonically increasing, drives updates), imagePath (launcher card, icon, launch image), launchSoundPath, and up to two contentWarning screens.

function playdate.update()
-- called right before every frame is drawn onscreen
end

The display runs 30 fps by default and 50 fps maximum (playdate.display.setRefreshRate(rate), 0 to 50). Everything else in the API is a thing you call from inside that function or a callback the system calls into.

The rest of the lifecycle is a fixed vocabulary of named callbacks you optionally define:

CallbackFires when
playdate.update()every frame
playdate.cranked(change, acceleratedChange)crank rotates
playdate.crankDocked() / crankUndocked()crank folded in / pulled out
playdate.AButtonDown/Up/Held(), BButtonDown/..., upButtonDown(), …per-button events
playdate.gameWillPause() / gameWillResume()system menu opened / closed
playdate.deviceWillSleep()before low-power sleep
playdate.deviceWillLock() / deviceDidUnlock()lock state
playdate.gameWillTerminate()exiting via the system menu

“All files imported from main.lua (and imported from files imported from main.lua, and so on) are compiled into a single pdz file, and import runs the code from the file only once.”

The whole game is one compilation unit. There is no module loader at runtime and no dynamic code loading in the usual Lua sense. Optional standard libraries are pulled in the same way, and you pay for only what you import:

import "CoreLibs/object"
import "CoreLibs/graphics"
import "CoreLibs/sprites"
import "CoreLibs/timer"

CoreLibs is the interesting design choice. The sprite system, the timer system, the animator/easing library, the grid view, the class/object system: none of it is in the runtime. It ships as Lua source you opt into. A game that does its own drawing imports none of it and carries none of its cost.

Everything hangs off one global table, playdate.

ModuleCovers
playdate.graphicsdrawing primitives, images, image tables, fonts, text, sprites, tilemaps, stencils, draw modes
playdate.soundsynth, sampleplayer, fileplayer, sequence/track, channels, effects
playdate.timer / playdate.frameTimerwall-clock and frame-count timers with easing
playdate.uigridview, crankIndicator (the system-standard “turn the crank” prompt)
playdate.geometrypoint, vector2D, rect, polygon, affineTransform
playdate.file / playdate.datastoreraw file IO / one-call table serialization
playdate.displayrefresh rate, inverted, scale, offset, flush
playdate.json, playdate.string, playdate.mathhelpers
playdate.networkHTTP/TCP (newer addition)

The canonical example, verbatim from the docs

Section titled “The canonical example, verbatim from the docs”
import "CoreLibs/object"
import "CoreLibs/graphics"
import "CoreLibs/sprites"
import "CoreLibs/timer"
local gfx <const> = playdate.graphics
local playerSprite = nil
function myGameSetUp()
local playerImage = gfx.image.new("Images/playerImage")
assert( playerImage )
playerSprite = gfx.sprite.new( playerImage )
playerSprite:moveTo( 200, 120 )
playerSprite:add()
local backgroundImage = gfx.image.new( "Images/background" )
assert( backgroundImage )
gfx.sprite.setBackgroundDrawingCallback(
function( x, y, width, height )
backgroundImage:draw( 0, 0 )
end
)
end
myGameSetUp()
function playdate.update()
if playdate.buttonIsPressed( playdate.kButtonUp ) then
playerSprite:moveBy( 0, -2 )
end
if playdate.buttonIsPressed( playdate.kButtonRight ) then
playerSprite:moveBy( 2, 0 )
end
if playdate.buttonIsPressed( playdate.kButtonDown ) then
playerSprite:moveBy( 0, 2 )
end
if playdate.buttonIsPressed( playdate.kButtonLeft ) then
playerSprite:moveBy( -2, 0 )
end
gfx.sprite.update()
playdate.timer.updateTimers()
end

Twelve lines of setup, then a loop that polls input, moves a sprite, and calls two “tick the world” functions. gfx.sprite.update() is the whole render pass: it clears dirty rects, calls each sprite’s update() and draw(), and composites. The background is a callback, so it only redraws where something moved.

sprite:setCollideRect(x, y, width, height)
sprite:moveWithCollisions(goalX, goalY) -- returns actualX, actualY, collisions, length
sprite:collisionResponse(other) -- return "slide" | "bounce" | "overlap" | "freeze"
sprite:overlappingSprites()
sprite:alphaCollision(otherSprite) -- per-pixel refinement

Broad phase is a rect sweep; you refine with alphaCollision only where it matters. collisionResponse is a method you override to name a behavior string, so the collision policy lives on the sprite itself.

playdate.sound.synth.new([instrument]) -- waveform synth, playNote(note, length, release)
playdate.sound.sampleplayer.new(path) -- fully loaded into RAM, low latency
playdate.sound.fileplayer.new(path) -- streamed from flash, for music
playdate.sound.sequence.new() -- MIDI-ish tracks: addTrack, track:setNotes(...)
channel:addSource(source) -- effects/routing

Three tiers, chosen by how the audio is stored: sample players hold the whole clip in RAM, file players stream, and the synth generates. That split is forced by 16 MB of RAM.

playdate.datastore.write(table, [filename], [prettyPrint])
playdate.datastore.read([filename])
playdate.datastore.writeImage(image, path) / readImage(path)

One call, a Lua table, done. Raw playdate.file is there for anyone who needs it, and almost nobody does.

local menu = playdate.getSystemMenu()
menu:addMenuItem(title, callback)
menu:addCheckmarkMenuItem(title, initialValue, callback)
menu:addOptionsMenuItem(title, optionsArray, initialValue, callback)
playdate.setMenuImage(image, xOffset)

A game gets three slots in the system pause menu and a decorative image beside them. It does not draw the menu, own the pause interaction, or decide what the menu looks like. The firmware owns that surface completely, exactly the way Row 0 and Row 74 are ours.

Panic’s own framing:

“Lua is a great language for writing Playdate games. Its easy to use, and enables speedy development. Lua’s main drawback is performance, including sporadic hits due to garbage collection.”

“If your Playdate game requires maximum performance, C is the best choice.”

Both APIs exist side by side, and a project can mix them: “Parts of your game, or the entire game if desired, can be written in C using the Playdate C API.” Profiling ships in the box (playdate.drawFPS(x, y), playdate.getFPS(), plus a sampler in the Simulator).

Hardware envelope: 400×240 1-bit memory LCD, 30 fps default / 50 fps max, 16 MB RAM, 4 GB flash. Panic’s own sizing guidance: “A typical Playdate game might be in the 20-40MB range,” growing past 100 MB when there is a lot of audio, and “The biggest culprit in blowing up game size is audio.”


“Pulp is a click-and-place game maker for Playdate. If you’ve never made a game before, or you’re looking to try a fun, quirky sandbox for prototyping, Pulp can scale from goofing around to building a full Playdate game.”

It runs in the browser (desktop WebKit, so Safari or Chrome), installs nothing, and was “inspired by Bitsy, a little editor for little games or worlds.” Neven Mrgan proposed it inside Panic and Shaun Inman built it, with the blessing of Bitsy’s author, Adam Le Doux, and no fork of his code.

KeyModeWhat you do there
1Gametitle, author, version, build number, intro text, background color, song looping
2Fontfull- or half-width font, edit characters and the UI tiles
3Roomdraw tiles, animate them, place exits, lay out the world
4Songfive voices, up to 32 bars, piano roll
5Soundone voice, four bars
6ScriptPulpScript for the game and for individual tiles

Art, animation, level design, font design, music, sound, and code, in one browser tab with no build step.

The world model, and why the numbers matter

Section titled “The world model, and why the numbers matter”

Pulp halves the machine on purpose. The Playdate’s 400×240 becomes 200×120, everything is an 8×8 tile, and a room is therefore always exactly 25×15 tiles. One screen is one room; there is no camera and no scrolling. (The PulpScript defaults confirm the grid arithmetic: config.followCenterX is 12 and followCenterY is 7, the center cell of a 25×15 field.)

“Pulp is small on purpose.”

Four tile types, and the type decides the behavior:

TypeSolid?Behavior
Playern/a”The character or object the player controls; there’s only one of these in the game.” The only type that may contain transparent pixels. Extra player tiles exist for directional animation swaps.
Spritealways soliddoors, buttons, NPCs. Blocks movement, responds to interact.
Itemnever solidcollectibles. Vanish on pickup and auto-increment a plural-named variable (an item named dot bumps dots) unless you write your own collect.
Worldeitherfloors, walls, scenery.

Exits connect a tile to a room, or connect a whole room edge to the opposite edge of another room. An exit can sit under a solid World tile and stay inert until a Sprite covering it is removed, which is how you build a locked door without writing code.

An event language. There are no function definitions and no arguments: you write handlers for named events on the game or on a tile, and you raise your own events by name.

on eventName do
// code
end

Built-in events: load, start, enter, exit, finish, loop, change, select, dismiss, invalid, update, bump, confirm, cancel, crank, dock, undock, draw, interact, collect, any.

Raise your own with call "eventName" (this tile) or emit "eventName" (every tile that implements it). Address another tile and run a block in its context with tell x,y to ... end or tell "tileName" to ... end, which is a message send with a screen coordinate for an address.

Variables are global, default to 0, and need no declaration. Math is ++, --, +=, -=, *=, /=. Comparisons are ==, !=, >, <, >=, <=. Control flow is if/elseif/else/end, while ... do ... end, and done to bail early. Comments are //.

Deliberately absent: “PulpScript is limited in some ways — for instance, it doesn’t currently support arbitrary math expressions, just simple operations between two values.” No arrays, no local scope, no user functions with parameters. The stated philosophy is “brute force creativity” inside the constraints.

CategoryCommands
Drawdraw tileId at x,y · hide · window at x,y,w,h · label "text" at x,y,len,lines · fill "white"|"black" at x,y,w,h · crop to x,y,w,h
Talksay "message" · say "msg" then ... end · ask "question" then option "opt" then ... end end · menu at x,y,w,h then option ... end · fin "message"
Worldgoto x,y · goto x,y in "roomName" · swap tileId · play tileId · frame n · tell x,y to ... end · mimic "tileName"
Audiosound "name" · once "songName" · loop "songName" · stop · bpm n
Flowwait duration then ... end · shake duration · ignore / listen (input off/on) · act
Persiststore "var" · restore "var" · toss "var" (each also works with no argument for everything)
Querysolid x,y · type x,y · id x,y · name x,y · invert
Mathrandom max · random min,max · floor · ceil · round · sine · cosine · tangent · radians · degrees
Debuglog "message" · dump

Three read-only tables carry all the context a handler needs:

  • event: dx,dy (movement, -1/0/1), tx,ty (target tile), x,y (this instance), px,py (player), aa,ra (crank absolute and relative angle), ax,ay,az + orientation (accelerometer), option (menu choice), game, room, player, tile, frame.
  • config: tunables with sane defaults: autoAct (1), inputRepeat (1), inputRepeatDelay (0.4), inputRepeatBetween (0.2), follow (0), followCenterX (12), followCenterY (7), followOverflowTile (“black”), allowDismissRootMenu (0), sayAdvanceDelay (0.2), textSpeed (20), textSkip (1).
  • datetime: year, month, day, weekday, hour, hour12, minute, second, ampm, timestamp.

Strings interpolate variables and pad them inline: "count is {count}", "{6,0:score}" (left-pad to 6 with zeros), "{8, :value}" (right-pad to 8 with spaces), "{embed:tileName}" (draw a tile mid-sentence), \n newline, \f page break.

on interact do
health += 1
health++
health--
if health > maxHealth then
health = maxHealth
end
if health <= 0 then
health = 0
fin "You died..."
end
end
on collect do
teleportX = event.x
teleportY = event.y
teleportX += 5
goto teleportX,teleportY
sound "teleport"
end

That is the whole flavor. Named event, flat statements, global variables, one verb per line.

The single most useful technical fact in this file:

“The resulting game package contains the Pulp engine, plus the JSON file describing the game yourself — rather than being bytecode, the game is a big table of all the objects you’ve created, including the script. The engine steps through that as you play.”

Pulp ships an interpreter plus a data file. The .pdx carries the Pulp engine and the game as a table of objects (tiles, rooms, songs, scripts) that the engine walks. PulpScript stays data that the engine steps through, with no compile to Lua or to bytecode anywhere in the path. The Pulp compiler’s job is packing and trimming, and its 2024 changelog entries are about exactly that: “inlines single command blocks to save runtime memory,” “spreads initial data load and processing over first two frames.”

The cost is the ceiling. The community wiki puts it plainly: “The performance ceiling is quite low,” and Pulp “won’t suit well to anything ‘real-time’ such as a scrolling shooter.” Tile-based and turn-based is where it works.

Pulp games are real commercial games on the real store

Section titled “Pulp games are real commercial games on the real store”

Checked directly, because “the beginner tool’s output is second-class” is the obvious assumption and it is wrong here. A Pulp game and an SDK game are the same artifact to the platform: same .pdx, same sideload path, same itch.io listing, same Catalog submission form.

The proof is a Catalog collection called “Pulp Hits” carrying 25 paid titles at capture, with the pitch “Did you know you can make your own game in Pulp right now? Using a little code, or no code!” Among them: Pixel Ghost’s four-game Life’s Too Short series, EYELAND (Ron Lent), The Fall of Elena Temple (GrimTalin), HANA: Spacetime Fantasy (KINGOFSHIBUYA), Castle Helios (Patrick Witmer), and Hidey Spot by Neven Mrgan, the Panic designer who proposed Pulp in the first place. Pulpergeist (Crandaddy, published 2023-09-26) sells for $2 and its store page says outright: “PULPERGEIST is a classic point-and-click adventure game made for Playdate with Pulp.”

Two things follow. Catalog’s stated bar (finished, polished, tested on hardware, “one of the best in your genre”) is applied to Pulp submissions on the same terms, and Panic merchandises the tool through the store by grouping its output into a browsable collection that doubles as an ad for the editor. The genre spread is the honest caveat: point-and-click adventures, puzzles, mazes, word games. No marquee action title came out of Pulp, and the interpreter ceiling is why.

Three of them, and only one is Panic’s.

  1. The .pdx is yours. A Pulp game downloads as an ordinary bundle and sideloads or sells like any other. “When you download your game as a pdx, it’s just like any other game on the platform except you built it with Pulp instead of with Lua or C.” The project JSON rides along inside the bundle and can be deleted if you would rather not ship your source.
  2. Pulp Audio Runtime (Panic’s). A small Lua library plus two JSON files (pulp-songs.json, pulp-sounds.json) that you drop into an SDK project so a Lua/C game can play music and sound effects authored in Pulp’s editors. import 'pulp-audio', call pulp.audio.init() once, pulp.audio.update() each frame, then pulp.audio.playSound(name) / playSong(name, playOnce, onComplete) / stopSong() / setBpm(n). The authoring tool’s most useful piece was unbundled and made available to the pro track.
  3. pulp-to-lua / “Pulp Mill” (community, Nick B). Converts a Pulp .json project into a Lua SDK project, claiming a performance gain of “an order of magnitude at least,” declaring the ~150 most-used variables as Lua locals, and letting you inject raw Lua through // [LUA] comments. Panic never built the official Pulp-to-SDK graduation path; the community did. There is also Piplup, an incomplete decompiler that recovers .json from a Pulp .pdx.

Catalog (Panic’s on-device store, launched March 2023) is curated by hand: “reviewed and handpicked by real Playdate team members,” 1 to 3 weeks to hear back, resubmission allowed. Terms: “You keep 75% of sales revenue. Minus any Stripe credit card fees (typically 2.9% + .30¢).” Price between $1 and $100. Submission is a zipped .pdx plus screenshots and a description. Bar: finished, polished, tested on real hardware, “one of the best in your genre,” nothing hateful, and no generative AI for art, audio, music, text, or dialog. Panic handles payments, fraud, returns, hosting; the developer handles support and updates. Non-exclusive, so itch.io and direct sales continue in parallel.

Pulp games go through the same form on the same terms, and Panic groups the accepted ones into a “Pulp Hits” collection (25 paid titles at capture). See Pulp games are real commercial games on the real store.

Sideloading is first-class: over USB or by uploading to the account website, which pushes the game to the device. Itch.io carries the long tail of jam games and experiments.

SDK License 1.0 is free and royalty-free with commercial use allowed and no revenue share on games. The restrictions to note: no redistributing the SDK, no using it “to develop applications for other platforms or to develop another SDK, without express written permission from Panic,” no disassembly, and no using the word “Playdate” in your application’s name.

Panic ships a compiler, a simulator, a mirror app, and docs. Everything else came from outside: Noble Engine (scenes, transitions, save slots, animated sprite states), Roomy-Playdate (scene management), playdate-luacats (LuaCATS type annotations for editor autocomplete), VS Code templates for all three platforms, PDFontTool (TTF/OTF to Playdate font), PlaydateLDtkImporter and Tiled support (level editors), DrawDate (browser 1-bit sprite editor), a GitHub Action that installs the SDK in CI, and unofficial bindings for Rust (Crankstart, playdate-rs), Nim, and Java.


1. Our stack already matches theirs, structurally

Section titled “1. Our stack already matches theirs, structurally”
PlaydateKN-86
pdc source out.pdxkn86cart build carts/carts/snake.lsp
.pdx bundle (code + assets + metadata).kn86 container (ADR-0006)
pdxinfo (name, author, bundleID, version, buildNumber)cart manifest (cartridge-format.md)
playdate.* global table, ~one namespaceNoshAPI v1, 54 primitives (ADR-0005)
playdate.update() once per frameevent-driven redraw with a 20 fps animation cap
CoreLibs, opt-in Lua librariesthe ui/ Fe component kit
3 system-menu slots, firmware owns the restRow 0 + Row 74 are firmware-owned (screen-design-rules.md)
playdate.datastore.write(table)per-cart save data + Universal Deck State
Lua for carts, C for the runtime and the fast pathFe-Lisp for carts, C for libnosh (ADR-0001, ADR-0004)

The convergence is the point. Two teams solving “third parties author for a constrained 1-bit handheld” landed on the same six pieces. Where we differ, the difference should be a decision we can defend, and there are two: our language is a Lisp with a tree-walking interpreter (theirs is a bytecode VM), and our packager bundles source with no compile step (theirs compiles).

2. CoreLibs is the argument for keeping ui/ out of the runtime

Section titled “2. CoreLibs is the argument for keeping ui/ out of the runtime”

Panic put the sprite system, the timers, the easing curves, and the grid view in importable Lua source and kept them out of the firmware. A game that draws its own screens imports none of it and pays nothing. That is precisely the ui/ arrangement we already have (a Fe component kit referenced by path, loaded per context), and Playdate is the four-year proof that the arrangement holds up. It also names the risk we already hit: the router’s System context loads every program library into one arena cumulatively, so the “opt in and pay for what you use” property has to be real at load time and not just at authoring time.

3. Pulp is the strongest available model for a KN-86 authoring tool

Section titled “3. Pulp is the strongest available model for a KN-86 authoring tool”

If we ever want people who do not write Lisp to make cartridges, Pulp is the reference implementation, and its architecture is one we could adopt nearly unchanged:

  • A browser editor that emits data, plus a small on-device interpreter that walks the data. We already have the interpreter half. A cart that is “a big table of objects, including the script” is a Fe data structure, and stepping through it is a Fe loop. No new runtime is needed for a Pulp-analogue on the KN-86.
  • Make the tool a smaller machine than the device. Pulp halves the resolution, fixes the tile size at 8×8, fixes the room at exactly one screen, and allows one font and one player. Our version of that arithmetic: the cart area is Rows 1 through 73 of a 128×75 grid, so a “room” could be a fixed block of cells with a single glyph set and no scroll. The constraint is what makes the tool teachable in an afternoon.
  • Four tile types where the type carries the behavior (solid/not, collectible/not, one player) is a remarkably small ontology that still covers adventure, puzzle, and dungeon games. Compare our cell-handler contract, which is more general and correspondingly harder to explain.
  • The beginner tool’s output sells in the real store. Panic did not build a sandbox and wall it off. Pulp games clear the same Catalog bar, sit on the same shelves, and get a curated collection that advertises the editor back to buyers. Any KN-86 authoring tool should target the real .kn86 format and the real distribution path from day one, or it is a demo.
  • The event vocabulary is 20 names. enter/exit/update/bump/interact/collect/confirm/cancel/draw plus call/emit for user events and tell x,y to ... end to run a block in another tile’s context. tell is a message send addressed by grid coordinate, which lands naturally in Fe and is a good idea on its own.

4. The Pulp Audio Runtime is the precedent for the tracker workbench

Section titled “4. The Pulp Audio Runtime is the precedent for the tracker workbench”

Panic took the one part of the beginner tool that professionals also wanted (the music and sound editors), and shipped it as a library plus two JSON files that any SDK game can import: three calls to wire up, four to use. The KN-86 tracker workbench already produces .knm chip-tunes with a kn86psg audition binary. This is the shipped precedent for how that becomes a cart-facing feature: export the song data, drop it in the cart, import a tiny Fe player, call it once per frame. Same for the bark lab. The pattern generalizes: an authoring tool earns its keep when its output can leave the tool.

5. Things they did that we should copy outright

Section titled “5. Things they did that we should copy outright”
  • buildNumber as a monotonic integer separate from the user-facing version string. Updates key off the integer. Cheap, and we do not have it.
  • Profiling in the box. drawFPS(x, y) is one call and it is in the shipped API, not a debug build. Our on-glass timing has a known problem (the clock quantizes coarser than a frame), so an FPS counter that averages over a window is the honest version of the same tool.
  • The .pdx is a plain directory. Inspectable, diffable, no tooling required to look inside a game. Our .kn86 should be as transparent.
  • Sizing guidance in the docs. “20-40MB typical, audio is what blows it up” tells an author what normal looks like before they overshoot. We should publish the equivalent number for .kn86 once the launch carts settle.
  • Sample player versus file player as separate types. The choice of “load it all into RAM” versus “stream it” is exposed in the type name, so the author makes the memory decision consciously. With a 32 KB Fe arena and a 4 KB deck-state region, we have far less room to be casual about this than they do.
  • Their scripting language has no functions. PulpScript has events and global variables. That is a defensible choice for a beginner tool and a bad one for the KN-86, where the whole product thesis is that the operator writes Lisp. Our Pulp-analogue, if it exists, should emit Lisp that a curious author can then open and edit, which is the graduation path Panic left to the community.
  • Pulp’s ceiling is real and was reached. “The performance ceiling is quite low.” An interpreter walking a data table on a 180 MHz part cannot do real-time action, and the fix arrived from outside as a transpiler. If we build a data-driven cart tool, plan the escape route to Lisp source on day one.
  • The SDK license forbids using their SDK to build for other platforms or to build another SDK. We take the design and none of the code. Same discipline as the AGPL note on strudel.
  • Catalog’s 75/25 split with hand curation is the closest thing to a benchmark for any KN-86 cart store. Their bar is “finished, polished, tested on hardware, one of the best in your genre,” and they explicitly ban generative AI in submitted art, audio, and text.

  • Currency at capture (2026-07-23). SDK 3.1.1 shipped 2026-07-22. SDK 3.0.0 (2025-10-01) was a launcher and library overhaul plus CMake build support and a unified Simulator across macOS, Windows, and Linux. SDK 2.0.0 was June 2023. Season Three was announced April 2026 for late 2026. Pulp’s last changelog entry is 2024-10-11, which reads as maintenance; no deprecation notice appears anywhere on Panic’s site.
  • Pulp’s tile grid is derived. Panic’s docs never state 25×15. It follows from the half-resolution 200×120 canvas with 8×8 tiles, and the config.followCenterX/followCenterY defaults of 12 and 7 confirm it.
  • The runtime architecture quote comes from journalism. The “engine plus a big table of objects” description comes from Game Developer’s piece on Pulp, sourced from Panic. Panic’s own docs never describe the runtime. Treat it as high-confidence but unofficial; the Pulp changelog’s memory-and-load-time entries corroborate it.
  • Cross-link zork: the other authoring-architecture entry in this corpus. Zork is the historical case (a Lisp-family authoring language over a portable VM); Playdate is the contemporary shipping one.
  • Cross-link strudel: the authoring-language sibling. Strudel answers “what should the language be,” Playdate answers “what should the platform around the language be.”
  • Cross-link marketing-plan.md §Competitive Landscape #1, the positioning half of the same competitor.
  • Cross-link update-system.md: their USB transport, already studied for ours.