This document explains how a 1991 DOS game was studied and rebuilt as a highly faithful game for the Playdate handheld. It is written for readers who may be new to programming, reverse engineering, emulation, or game preservation.
The most important fact to understand is this:
“The Playdate game is not the original DOS program running inside an emulator, and it is not decompiled DOS code compiled for a new machine. It is a new, portable game engine written in C, guided by evidence from the original executable and driven by data converted from an owner-supplied copy of the DOS game.
That distinction explains both the technical approach and the quality of the result. The project preserved the parts that needed to remain exact—levels, masks, sprites, animation data, music commands, timing relationships, and many gameplay constants—while rewriting the platform-specific machinery for a modern one-bit handheld.
The finished port includes all 120 one-player levels in Fun, Tricky, Taxing, and Mayhem order; the complete skill set; mutable terrain; traps; progression and saved games; native Playdate menus and controls; exact 2× gameplay presentation; real-time AdLib synthesis; sound effects; and device-specific audio and storage optimizations.
1. The basic ideas
Before following the project, it helps to define a few terms.
Source code
Source code is the human-readable program written by a developer. C source might contain a line such as:
lemming->state = LP_STATE_FALL;A compiler translates source code into machine code: numeric instructions that a processor can execute.
Machine code and executables
Machine code is designed for a particular processor. The DOS version of Lemmings contains 16-bit Intel x86 machine code. Playdate uses a very different ARM processor. The DOS instructions therefore cannot simply be copied into a Playdate application.
An executable is a file containing machine code plus information needed to load and run it. The DOS VGA program used here is an MZ executable, a common DOS executable format.
Assembly and disassembly
Assembly language is a readable notation for machine instructions. A disassembler turns machine-code bytes into assembly instructions. It can show that a value is loaded, compared, or written, but it usually cannot recover the original variable names, comments, file organization, or programmer intent.
Decompilation
A decompiler tries to turn machine instructions into higher-level pseudocode resembling C. This can make control flow easier to understand, but the result is an interpretation—not the original source code.
Compilation throws information away. A function once named UpdateFallingLemming may appear as FUN_1234. A meaningful structure may look like unrelated array offsets. Several original statements may become one instruction sequence, or one source operation may expand into many instructions.
This is why decompilation is the start of an investigation, not a button that restores a project.
Reverse engineering
Reverse engineering is the larger process of learning how a system works from its observable artifacts. It may use:
- File-format inspection
- Disassembly and decompilation
- Controlled experiments in the original game
- Comparisons between versions
- Memory and register traces
- Timing measurements
- Tests of competing explanations
A port
A port adapts software to another platform. A faithful port preserves the important behavior and content while replacing assumptions that only made sense on the original machine.
Emulation
An emulator imitates another machine. For example, DOSBox emulates enough of a DOS-era PC to run the original executable. This project does not emulate a complete PC. It does, however, use a focused OPL2 emulator to recreate the AdLib sound chip. That is a small, deliberate form of component emulation inside an otherwise native port.
A clean implementation
The new engine was written as modern, portable C. The original executable was treated as private behavioral evidence rather than copied, translated, or linked into the port. Decompiled output stayed in a private workspace. Public code expresses independently written rules and interfaces.
This is sometimes described as a clean-room-style boundary. The important practical rule was simple: record facts about behavior and formats, then implement those facts cleanly in the new engine.
2. The project at a glance
The workflow has four main layers:
Owner-supplied DOS files
|
+--> checksums and preservation notes
|
+--> executable unpacking, disassembly, and private Ghidra analysis
| |
| +--> behavioral facts
|
+--> deterministic asset converter --> lemmings.lpd
|
New portable C engine + Playdate adapter ----------+--> Lemmings.pdx
|
ADLIB.DAT command image + DBOPL -------------------+--> real-time musicThe repository organizes those responsibilities deliberately:
| Area | Purpose |
|---|---|
preservation/ | Reference hashes, reproducible decompilation setup, and reverse-engineering notes |
tools/assetc/ | DOS level and graphics decoding, conversion, compression, and pack validation |
tools/audio/ | Independent AdLib research interpreters, traces, and reference rendering |
playdate/core/ | Platform-independent game, pack, progress, music, and AdLib runtime code |
playdate/src/main.c | Playdate input, menus, drawing, saves, sound effects, and application lifecycle |
playdate/tests/ | Native tests for game behavior, progression, and audio |
This separation was one of the largest contributors to quality. A display bug could be fixed without changing physics. An audio optimization could be tested without touching level parsing. The converter could prove that data was decoded correctly before the handheld ever loaded it.
3. Establishing a trustworthy reference
Reverse engineering is unreliable if the source material can silently change.
Old games often exist in many forms: original disks, regional variants, later releases, cracked copies, repacks, and archives with altered documentation. Two files with the same name can contain different bytes and therefore different code or data.
The project addressed this with SHA-256 hashes. A hash is like a highly sensitive digital fingerprint. Passing a file through SHA-256 produces a long value. Changing even one byte produces a different value with overwhelming probability.
The file preservation/reference.sha256 records the expected hashes of 71 preservation inputs. The verifier in preservation/verify_reference.py checks three things:
- Every expected file exists.
- No unexpected file has appeared in the reference set.
- Every file produces its recorded hash.
This creates an immutable baseline. When a test says that a table occurs at a particular offset, we know exactly which binary that offset refers to.
The reference inventory established several early facts:
- The VGA game program is a PKLITE-compressed, 16-bit DOS MZ executable.
- Gameplay levels are 1,600 by 160 source pixels.
- The original gameplay viewport is 320 by 160 pixels, with a 320 by 40 control panel.
- Level storage contains 80 unique layouts plus 80 variation records, assembled into 120 one-player levels.
- Graphics include five normal terrain/object sets and four 960 by 160 special backgrounds.
- Decompressed AdLib data forms a 22,125-byte Sound Images memory image with 21 music tracks.
- A 256 KiB, headerless Atari Lynx cartridge image was available as a secondary control reference, especially useful when considering handheld interaction and action-panel presentation.
The original files were never edited in place. Generated files went into ignored working directories. This protected both reproducibility and the legal boundary between new code and owner-supplied material.
4. Why the executable had to be unpacked first
The VGA executable was compressed with PKLITE. Executable compressors reduce disk size by storing a small unpacking routine followed by compressed program data. When launched under DOS, the stub reconstructs the real program in memory and transfers control to it.
If a decompiler analyzes the packed file directly, it mostly sees the unpacker—not the game.
The project automated unpacking in preservation/prepare_decomp.py:
- Read the DOS executable header.
- Extract the header size in 16-byte paragraphs.
- Locate PKLITE decoder metadata.
- Calculate where the compressed payload begins.
- Clone the open-source
depkliteutility at a pinned Git commit. - Compile that known revision locally.
- Decrypt and unpack the payload into a raw binary image.
- Reject the output unless its size is exactly 82,906 bytes.
Pinning the depklite revision matters. If a tool changes later, the project should not unknowingly generate different analysis input.
The expected-size check is also important. A program that merely exits without an error has not necessarily done the right thing. In preservation work, successful output should have properties that can be verified.
5. Producing assembly and pseudocode
The unpacked image was analyzed in two complementary ways.
A flat disassembly
ndisasm -b 16 produced a text listing of 16-bit x86 instructions. A flat disassembler is simple and predictable. It does not try to infer elaborate types or program structure, which makes it useful when checking exact offsets and instruction bytes.
A Ghidra project
Ghidra was run headlessly using the processor definition x86:LE:16:Real Mode:
x86selects the processor family.LEmeans little-endian byte order.16selects 16-bit instructions.Real Modematches the memory model used by DOS programs before protected-mode operating systems became common.
The unpacked file is raw program data, so it no longer has a normal executable header telling Ghidra where execution begins. SeedEntry.java solves this by disassembling address 0000:0000, creating an entry function, and marking it as an external entry point.
ExportFunctions.java then visits every function Ghidra discovers and asks its decompiler for C-like pseudocode. The exported pseudocode is useful for searching and annotation, but remains private.
That privacy boundary is intentional. The public engine contains newly expressed game rules, not pasted decompiler output.
What the tools could and could not tell us
The tools could reveal:
- Branches, loops, calls, and arithmetic
- Reads and writes at fixed offsets
- Tables indexed by level, object, or channel numbers
- Relationships between timers and state changes
- Calls into DOS or sound-driver interfaces
They could not automatically reveal:
- Original function and variable names
- Original comments
- Original source files
- Whether a strange operation was intentional, an optimization, or compiler output
- The complete meaning of every field
- The original programming language with certainty
The executable proves that the shipped program consists of 16-bit x86 machine code. It may contain patterns consistent with assembly or compiled higher-level code, but a stripped binary alone is not enough to responsibly claim an exact original source language.
Deep dive: from machine-code bytes to readable portable C
This is the step most often compressed into the phrase “we decompiled the game.” In reality, it is a chain of different transformations, and only the first few can be automated. The final portable C was not emitted by Ghidra and cleaned up until it compiled. It was written independently after the machine code had been turned into evidence about data formats and behavior.
“The DOS executable contains native machine code, not bytecode. A disassembler decodes those bytes into x86 instructions; a decompiler lifts the instructions into approximate pseudocode; a human then extracts a behavioral specification; and the new engine expresses that specification as portable C.
Machine code is not bytecode
People often use “bytecode” to mean any program represented as bytes. In software engineering, it normally has a narrower meaning: instructions for a virtual machine, such as Java bytecode or Microsoft’s Common Intermediate Language. Another program interprets or compiles that intermediate language.
DOS Lemmings is different. Its executable contains instructions intended to run directly on an Intel-compatible processor in 16-bit real mode. Those instructions are machine code. They are still stored as bytes, but the processor itself assigns meaning to them.
A short, invented x86 example—not bytes copied from Lemmings—looks like this:
Bytes Disassembled instruction Plain-language effect
B8 03 00 MOV AX, 0003h Put the number 3 in register AX
83 C0 02 ADD AX, 0002h Add 2 to AX
3D 05 00 CMP AX, 0005h Compare AX with 5
75 04 JNE somewhere_else Jump if the values differThe bytes do not contain the English descriptions, variable names, or a statement such as if (value != 5). Those are interpretations layered on top.
How x86 instructions are decoded
The x86 instruction set is variable-length. One instruction may occupy one byte while another occupies several. A decoder starts at a known address and reads:
- Optional prefix bytes that change operand size, segment selection, or repetition behavior.
- An opcode identifying the broad operation.
- Sometimes a ModR/M byte describing registers and memory addressing.
- Sometimes a displacement used in an address.
- Sometimes an immediate value stored directly in the instruction.
Multi-byte numbers are little-endian, meaning the least significant byte appears first in the file. The two bytes 34 12 therefore represent the 16-bit value 0x1234.
Correctly finding the first instruction is essential. Starting one byte late can produce a completely different sequence of apparently valid instructions. Because x86 encoding is dense, random data can also look like code. This is why the project seeded the known entry point and also retained a flat ndisasm listing for exact offset checks.
The code-versus-data problem
An executable is not one uninterrupted stream of instructions. It also contains constants, lookup tables, text, masks, addresses, and padding. The bytes themselves do not carry universal labels saying “code starts here” or “this is a 17-entry music table.”
Analysis begins at an entry point and follows direct evidence:
- A
CALLsuggests another function entry. - A conditional or unconditional jump creates another possible execution path.
- A return ends one path.
- A memory read may point to data.
- An indirect call or jump may require manual investigation because its destination is computed at runtime.
This process is called recursive traversal. A linear sweep, by contrast, tries to decode everything in order. Each catches things the other can miss. Ghidra combines automated analysis with information supplied by the researcher.
Tables are especially important in games. If code loads a level number, constrains it to a range, and uses it to index a nearby sequence of bytes, those bytes may be a selection table. The meaning comes from the access pattern, values, surrounding comparisons, and observed behavior—not from the bytes alone.
Real-mode segments and pointers
A modern flat address such as 0x12345678 can often be treated as one location in one large address space. A 16-bit DOS program commonly uses a segment and an offset. In real mode, the physical address is approximately:
physical address = segment × 16 + offset
1234:0050 → 0x12340 + 0x0050 → 0x12390Different segment:offset pairs can name the same physical byte. Code, data, and stack segments may also be changed independently. A decompiler has to track these assumptions, and the researcher must distinguish a near pointer from a far pointer when the evidence requires it.
The unpacked PKLITE result is a raw image without the original loader metadata Ghidra would normally consume from an MZ file. The project therefore selected the real-mode processor explicitly and seeded address 0000:0000. Addresses recorded in preservation notes are offsets in that pinned raw image, which avoids pretending they are universal addresses for every release.
Recovering functions and local values
At machine level, a function is a convention rather than a richly described object. A CALL places a return address somewhere, a routine manipulates registers and memory, and a RET returns. The binary may not state how many arguments exist or what type each one has.
Older x86 compilers often build a stack frame using the BP register. Positive offsets from BP may be arguments and negative offsets may be local storage. But optimized code can omit the conventional frame, reuse registers, merge variables, or keep values in global memory.
Ghidra performs several reasoning steps:
- Group instructions into basic blocks: straight-line sequences with one entry and one exit.
- Connect blocks into a control-flow graph showing possible branches and loops.
- Lift processor instructions into an internal, processor-neutral representation called p-code.
- Track where values are defined and later used.
- Infer parameters, local variables, return values, and likely types.
- Simplify the result into C-like expressions, loops, and conditionals.
A control-flow graph is the hidden structure behind readable pseudocode:
[read current state]
|
[state == falling?]
/ \
yes no
| |
[run fall behavior] [check other states]
\ /
[advance frame]Even after this analysis, names might remain FUN_1042, param_1, or DAT_7a20. Types may be wrong. A word interpreted as an unsigned integer might really be a signed coordinate, bit flags, a timer, or an offset.
How meaning is recovered
Readable analysis grows through annotation. The researcher follows cross-references, compares tables with known file records, watches how values change, and renames things only when evidence supports the name.
A typical investigation might proceed like this:
- Find code that reads a known level field or updates a visible counter.
- Follow callers and callees to identify the larger update phase.
- Record which offsets are read and written.
- Compare those accesses with controlled behavior in the original game.
- Try an interpretation such as “this byte is the falling-state timer.”
- Check whether every use of the byte is consistent with that interpretation.
- Record the conclusion, location, method, and confidence.
Dynamic observation can strengthen static analysis. For example, a table that looks like song indices becomes much more convincing when its values match music heard across normal levels, its length matches the wrap comparison, and a neighboring table matches the four special levels.
Why decompiled pseudocode is not portable C
C-like pseudocode may look compilable while still encoding assumptions that only hold in the original process. Common hazards include:
| Recovered pattern | Why copying it is risky | Portable treatment |
|---|---|---|
| Raw numeric addresses | They refer to one loaded DOS memory image. | Use named arrays, structures, indexes, and validated readers. |
| Segmented pointers | Playdate uses a flat ARM address space. | Convert source offsets explicitly while parsing; use normal pointers only inside owned buffers. |
| Unaligned word reads | x86 permits accesses that another CPU may reject or handle differently. | Read bytes and assemble little-endian integers with helper functions. |
| Implicit 16-bit overflow | A modern C int is normally wider, and signed overflow is not portable. | Use uint8_t, uint16_t, int16_t, or wider deliberate arithmetic. |
| Register reuse | One register may represent several unrelated source variables over time. | Create separate, meaningfully named C values. |
| Magic offsets | The number describes a layout, not intent. | Give it a field name or isolate it in a format decoder. |
| DOS interrupts and hardware writes | Those services do not exist on Playdate. | Replace them with a narrow platform interface or semantic event. |
| Busy waits or hardware timing | CPU speed and scheduling are completely different. | Advance a fixed 17 Hz simulation from elapsed time. |
There is another subtle risk: a mechanically translated operation can preserve an implementation accident while losing the rule it once supported. The three-pixel falling counter seed is a concrete example. It belonged with a three-pixel movement step. Retaining the seed after changing movement speed made the port less faithful.
The specification bridge
The project therefore inserted a conceptual bridge between decompilation and implementation:
PRIVATE EVIDENCE PUBLIC IMPLEMENTATION
---------------- ---------------------
x86 instructions named C functions
Ghidra pseudocode facts explicit structs and enums
raw offsets -------> fixed-width integer types
tables and traces validated data readers
observed DOS behavior deterministic tests
No original routine is linked or executed.The “facts” in the middle are statements such as:
- The simulation advances at 17 ticks per second.
- This field is a signed source-pixel coordinate.
- This mask removes these terrain pixels during a mining frame.
- The normal music selector wraps after 17 entries.
- A bomber counts down for five simulation seconds.
- Nuke arms one lemming slot per tick.
These statements are much easier to review and test than a large body of anonymous pseudocode.
Designing the new C model
The portable engine turns recovered concepts into explicit types. A lemming has a position, direction, state, animation frame, permanent abilities, countdown, and fall distance. A level has authored metadata, objects, collision, steel, and mutable visual planes. Skills and states are enums rather than unexplained numeric values.
The resulting API uses verbs that describe game operations:
void lp_game_init(LPGame* game, LPLevelAssets* level);
void lp_game_tick(LPGame* game);
int lp_game_can_assign(const LPGame* game, unsigned index, LPSkill skill);
int lp_game_assign(LPGame* game, unsigned index, LPSkill skill);
void lp_game_nuke(LPGame* game);These declarations are from the new engine’s public interface. They are not claimed as recovered original function names.
Several design choices make this C portable:
- Fixed-width integers state exactly how much data a field holds.
- Explicit endian readers decode DOS values without depending on the host processor.
- Bounded arrays make limits such as 100 lemmings and 32 objects visible.
- No platform calls in the game core allows the same simulation to run in tests, a desktop host, Simulator, and ARM device builds.
- Semantic sound events let gameplay request “splat” or “exit” without depending on Playdate’s audio API.
- Source-pixel coordinates and fixed ticks keep rules stable when display or frame timing changes.
- Versioned pack readers isolate old binary layouts from runtime structures.
What was translated and what was redesigned
Not every part of the project took the same route:
| Material | Technique | Reason |
|---|---|---|
| Levels, objects, palettes, sprite frames, and masks | Decode and deterministically convert original data. | The authored bytes are the best source of visual and level fidelity. |
| Gameplay behavior | Infer rules, then independently implement a C state machine. | DOS machine code cannot run natively on ARM, and clean rules are testable. |
| Music commands | Implement the Sound Images interpreter and feed its register writes to DBOPL. | This preserves original musical control flow without emulating an entire PC. |
| DOS display and input code | Replace with a Playdate adapter. | VGA, mouse, keyboard, D-pad, crank, and a one-bit LCD have different interfaces. |
| Menus and level browser | Redesign natively while preserving content and progression. | The high-resolution Playdate screen and system menu can provide better handheld usability. |
| Timing-sensitive behavior | Preserve logical rates and relationships, then validate on real hardware. | Wall-clock loops and CPU-speed assumptions would not transfer safely. |
Proving that the C means the same thing
Readable names do not prove correctness. Each recovered rule needs an observable consequence.
The project used multiple levels of equivalence:
- Structural equivalence: decoded sections, record counts, frame dimensions, and table hashes match known facts.
- Behavioral equivalence: skills, release timing, fatal falls, traps, exits, and nuke transitions produce the expected state changes.
- Trace equivalence: independently written audio interpreters emit identical register records for the same command stream.
- Output equivalence: selected synthesized PCM output matches a pinned golden hash.
- Experiential comparison: actual levels are played in Simulator and on hardware to find camera, cadence, selection, readability, and audio-budget problems.
The last step feeds the first four. When playtesting finds a mismatch, the project returns to the evidence, identifies the missing rule, and adds a regression test. That is how anonymous machine instructions ultimately became readable, maintainable C without pretending the decompiler had reconstructed the original source.
6. Turning decompiler output into reliable knowledge
The project used an evidence-driven loop:
Observe something
|
Form a precise hypothesis
|
Find supporting data or executable behavior
|
Implement the smallest clean rule
|
Write a test that would fail if the rule regressed
|
Compare again with the original and with real gameplayThis is much safer than treating decompiler pseudocode as authoritative source code.
For each important finding, useful notes identify the exact reference hash, offset, observation method, and confidence. An address without a pinned binary is ambiguous; a guess without a test is fragile.
Example: the music rotation
The level files do not simply contain a song number for every normal level. Analysis of the unpacked VGA executable found a 17-byte selection table at offset 0x4877, indexed by code near 0x4858. A comparison at 0x484C wraps the sequence after 17 entries.
After converting the original one-based driver indices to zero-based pack indices, the normal sequence is:
Lemming 1, Lemming 2, Lemming 3, Mountain, Ten Lemmings, Can-Can,
Tim 1, Tim 2, Tim 3, Tim 4, Doggie, Tim 5, Tim 6, Tim 7,
Tim 8, Tim 9, Tim 10A neighboring table at 0x4888 selects one of four special tracks for levels using special graphics: Awesome, Menace, Beast II, and Beast I.
This explains why a plausible “track per level” implementation could sound good yet route the wrong music. The port maps the original success-order rotation onto canonical level order so choosing levels directly remains deterministic while preserving the intended sequence and special overrides.
Example: falling distance
At one point, a miner who cut through terrain could fall down a shaft and die even though later lemmings survived the same fall.
The cause was not the level. The action-to-fall transition initialized the first lemming’s fall counter with three pixels that it had not actually travelled. That behavior could work when paired with the DOS game’s three-pixel first falling step, but the Playdate port had tuned falling to a smaller step. The counter and physical position no longer agreed.
The fix was conceptual: fall distance must count actual source pixels travelled, including the landing step. A new fall begins at zero. Tests now pin both sides of the boundary: a 62-pixel drop survives and a 63-pixel drop splats.
This is a good example of faithful porting. Copying a number without preserving the context around it was less accurate than preserving the underlying rule.
Example: the first release
The first lemming initially waited according to the level’s release rate. On very slow levels this produced a delay of more than six seconds. Investigation showed that entrance setup and the interval between later lemmings are separate concepts.
The engine now uses a fixed two-second first-release delay. Later release intervals still follow the level’s authored release rate. The clock remains at its initial value during “Let’s Go!” and the hatch opening, then starts with the first falling lemming and music.
Example: exact explosions
Bombing is not represented by one generic circle. The original executable contains 51 frames of 80 signed particle positions. The converter extracts that table from the unpacked executable and verifies it against an expected SHA-256 hash. The runtime combines it with the original terrain-removal mask, five-second countdown, “Oh no” transition, burst frame, sound event, and delayed removal.
7. Decoding the DOS data archives
Much of Lemmings is data-driven. Levels, terrain, objects, palettes, sprites, masks, and music commands live in DAT files rather than being drawn or authored again for this port.
The backwards LZ container
The DOS DAT files use a compressed section format decoded by tools/assetc/crunch.py. It is a backwards LZ-style scheme.
LZ compression saves space by replacing repeated data with references to bytes already decoded. “Backwards” here means that the bitstream and output are processed in a direction that differs from the straightforward left-to-right examples commonly shown in tutorials.
The decoder does more than produce bytes. It checks:
- Section checksums
- Declared compressed and decompressed sizes
- Bitstream exhaustion
- Back-reference validity
- Exact output length
Malformed data raises a format error instead of being accepted silently. Defensive parsing matters because one incorrect byte can shift every later field and create errors that look like gameplay bugs.
Level records
tools/assetc/levels.py parses each binary level record into named concepts:
- Release rate
- Number of lemmings
- Rescue requirement
- Time limit
- Counts for all eight skills
- Starting camera position
- Normal graphics set or special graphics ID
- Object placements
- Terrain-piece placements
- Steel regions
- Level name
The game stores 80 unique level layouts. ODDTABLE.DAT supplies 80 variation records that reuse layouts with changed parameters. The converter joins these into the canonical 120-level sequence and checks expected names. This prevents a technically valid but incorrectly ordered pack.
VGA planar graphics
Modern images often store complete RGB color values one pixel after another. DOS VGA assets commonly use indexed color and planar storage.
An indexed pixel stores a small number that points into a palette. The palette supplies the red, green, and blue values. Planar storage separates bits of those indices into different bit planes. Recovering an image therefore requires combining corresponding bits from several areas.
The converter reconstructs:
- Five standard terrain and object styles: dirt, fire, marble, pillar, and crystal
- Four special background maps
- Lemming animation frames
- Object animation frames
- Transparency masks
- Palettes
- Terrain-edit masks used by destructive skills
Masks deserve special attention. A transparency mask says which pixels belong to a sprite. A terrain-edit mask says which pixels a basher, miner, digger, builder, or explosion changes. Keeping exact masks is one reason interactions align with the original artwork instead of merely looking similar.
8. Converting data instead of decoding it on Playdate
The Playdate should spend its limited CPU and memory running the game, not repeatedly understanding several 1991 file formats. The desktop converter transforms the verified DOS files into one purpose-built file named lemmings.lpd.
The format begins with the magic value LPD1 and currently uses version 6. “Magic” is a short identifying byte sequence that lets a loader reject the wrong kind of file.
The pack contains:
- A source-data digest
- All 120 canonical level records
- Compressed terrain visual planes
- A collision plane and steel plane
- Object placements and metadata
- A deduplicated lemming/object sprite atlas
- Original action-panel artwork and digits
- Exact terrain-edit masks
- The explosion-particle table
- The decompressed AdLib command image
Determinism
The converter is deterministic: the same verified input produces the same output bytes. Determinism makes bugs reproducible and permits exact hashes in validation records.
PackBits compression
Level bitplanes contain long repeated regions, making a simple run-length format effective. The project uses deterministic PackBits-style encoding:
- Literal records copy a sequence of different bytes.
- Repeated records store a byte once plus a repeat count.
Tests cover literal and repeated-run boundaries, malformed input, truncated input, trailing data, and byte-for-byte round trips.
Streaming within a fixed budget
Each 1,600 by 160 one-bit plane contains 32,000 bytes. The native loader decompresses through a bounded 1 KiB streaming reader into fixed gameplay buffers. It does not need to load an entire compressed source file and several temporary copies at once.
Deduplicating sprites
Identical encoded animation frames share atlas storage. If two logical frames contain the same mask and pixels, only one copy is stored. Together with compressed level planes and excluding Simulator-only binaries from device packages, this helped keep the private device build around 2.84 MB—roughly 90 percent of the project’s 3 MiB target.
9. Rebuilding the game rules in portable C
The core engine in playdate/core/lp_game.c does not know about Playdate buttons, screen drawing, files, or menus. It owns the simulation.
The platform-independent model includes:
- Up to 100 lemmings
- Position and direction
- Current action state and animation frame
- Permanent climber and floater abilities
- Bomber countdown
- Fall distance
- Release timing and rate
- Mutable terrain and steel
- Active traps and exits
- Skill inventory
- Alive, released, rescued, and lost counts
- Clock, nuke sequence, win, and loss state
- A bounded queue of semantic sound events
Fixed-tick simulation
The game advances at 17 logic ticks per second, matching the DOS-compatible rate identified for the original game.
A fixed tick means game rules advance in equal logical steps even if screen drawing fluctuates slightly. The Playdate adapter accumulates real elapsed time and runs complete game ticks when enough time has accumulated.
Real time arrives
|
Accumulate elapsed seconds
|
While at least 1/17 second is available:
update one complete game tick
|
Draw the current stateThis makes physics reproducible and testable. A level behaves the same whether a frame took slightly longer to draw.
State machines
Each lemming is a small state machine. A state machine is a system that is in one named condition at a time and follows defined rules to move to another.
For example:
WALK --> FALL --> WALK
| |
| +--> FLOAT --> WALK
|
+--> BASH --> WALK or FALLThe actual engine supports walking, falling, climbing, floating, blocking, building, bashing, mining, digging, bomber countdown, “Oh no,” explosion, splatting, drowning, burning, and exiting.
Every tick processes the current state, checks terrain and objects, mutates terrain when required, advances animation, and transitions only according to explicit rules.
Terrain as both pixels and rules
The visible landscape is also collision data. Skills modify the solid plane using original masks. Those edits are propagated to all terrain visual planes, so switching dither modes cannot restore pixels already dug away.
Steel is stored separately. Destructive skills consult it before removing terrain. One-way terrain and blocker interactions are similarly game rules, not just artwork.
Selection fidelity
Selecting a moving eight-pixel character with a D-pad cursor is difficult if the hit area is only one visible pixel. The engine exposes the original-sized inclusive targeting footprint and whether the active skill can be assigned to that lemming.
The Playdate draws a two-pixel-thick dark bounding box only when assignment is valid. It blends with existing dark pixels instead of inverting them, avoiding the shimmering appearance caused by an alternating black/white box.
The basher case received special attention because a too-short eligible window made valid solutions impossible. Eligibility is evaluated from game state and nearby terrain, not merely from one animation frame.
Bombing and nuking
An individual bomber receives a five-real-second fuse. The countdown changes into the original “Oh no” pose, then the burst and particle animation remove terrain and eventually remove the lemming.
Nuke follows DOS slot order, arming one eligible lemming per tick rather than assigning every fuse simultaneously. Each lemming still receives the complete five-second countdown.
10. Adapting a wide color game to a small one-bit display
The DOS gameplay world is 1,600 pixels wide and 160 pixels tall. Its usual viewport is 320 by 160. Playdate’s display is 400 by 240 and can show only black or white pixels.
These constraints create competing goals:
- Show enough of the level to plan.
- Make tiny lemmings readable.
- Preserve original pixel shapes.
- Reserve space for useful status information.
- Avoid visual noise on a one-bit screen.
Exact 2× gameplay
Normal gameplay uses a 200 by 104 source-pixel crop below a fixed 32-pixel status panel. It is enlarged exactly 2×, filling 400 by 208 Playdate pixels.
Each original pixel becomes a uniform 2 by 2 block:
Original pixel: Playdate output:
# ##
##This is integer scaling. There is no interpolation, smoothing, fractional resizing, or position-dependent resampling. Sprite edges therefore retain the same geometry as the source art.
Earlier experiments showed why this matters. Arbitrary scaling can make one source pixel become one output pixel in one column and two in another. Thin arms, legs, tools, and animation offsets then appear inconsistent.
What gets dithered
Dithering simulates shades using patterns of black and white pixels. The converter calculates luminance from the VGA palette and creates four terrain choices:
- Solid
- Dispersed Bayer 2×2
- Clustered 2×2
- Bayer 4×4
The menu labels shorten these to Solid, Bayer 2, Cluster 2, and Bayer 4 so they fit the handheld interface.
The pattern is anchored to world coordinates during conversion. If it were recomputed relative to the screen, scrolling would shift the pattern and make stationary ground shimmer.
Terrain, special backgrounds, entrances, and exits receive matching visual planes. They are large environmental forms where tonal separation helps readability.
Lemmings are handled differently. Their exact source transparency masks are retained, and every opaque source pixel is drawn black. This preserves pale hands and feet that would disappear under a simple brightness threshold. Animated objects use a stable luminance threshold rather than changing dither phases. Action-menu sprite art is clear and undithered.
This selective policy is a central quality decision: do not apply one conversion rule indiscriminately to every asset.
Camera behavior
The cursor moves through source-pixel coordinates. Pushing it toward a screen edge scrolls the camera. Horizontal movement stops at visible terrain/object bounds plus a 24-source-pixel margin, preventing infinite scrolling into empty space.
The original start-scroll value was authored for a 320-pixel viewport. Reusing it in the narrower 200-source-pixel detail view left the entrance partly or fully off screen in 13 levels. The port instead centers on the entrance hatch, clamped to the valid world area. All 120 levels now open with the hatch fully visible; 108 can place it exactly in the center.
Native menus around a faithful game view
The DOS menu resolution did not need to constrain every Playdate screen. Title, credits, difficulty selection, level cards, results, and system menus use the full native 400 by 240 display.
The gameplay itself remains the exact 2× source view. This gives fidelity where source pixels matter and usability where Playdate conventions matter.
The level browser exposes all four original ratings, terrain previews, rescue requirements, time, skill counts, completion status, and saved progression. The action panel pauses gameplay and provides release rate, all eight skills, pause, and nuke in a 6 by 2 layout. Original DOS action art is used where suitable, with the Lynx version informing handheld interaction and supplying selected control art.
The crank is adapted contextually:
- During gameplay it fast-forwards simulation, capped at 4× total speed.
- In the paused action panel it adjusts release rate.
- In the level browser it moves between levels.
This is a blend of original content, Lynx-inspired directness, and native Playdate interaction.
11. Recreating AdLib music instead of storing recordings
The music system is one of the port’s most technically distinctive features.
What AdLib music is
An AdLib card contains an OPL2 FM-synthesis chip. FM synthesis creates tones mathematically by combining oscillators. Games control it by writing values to hardware registers that set instruments, pitches, volumes, envelopes, and key-on/key-off events.
The game’s ADLIB.DAT is therefore not a folder of WAV recordings. It contains a compressed 22,125-byte Sound Images memory image with command streams and tables.
The project preserves the commands and synthesizes them in real time.
ADLIB.DAT commands
|
Sound Images command interpreter
|
OPL2 register writes
|
DOSBox DBOPL chip model
|
PCM samples heard by PlaydateThis avoids the storage cost and loop seams of long pre-rendered recordings. More importantly, it preserves the original control flow, including voices that loop independently.
Independent research oracles
Audio routing initially sounded recognizable but incorrect. To avoid tuning by ear alone, the project created independent implementations:
- A Python Sound Images command interpreter
- A JavaScript reference player used as an oracle
- A clean C interpreter for the Playdate runtime
- A thin C-to-C++ wrapper around the pinned DOSBox DBOPL core
An oracle is an independent implementation or source of truth used to judge another implementation.
All 21 music streams and 18 AdLib effect streams were compared for 10,000 timer steps. The clean interpreter’s register stream matched the independent player byte for byte. The complete Fun 1 command cycle contains 4,609 steps and was also matched byte for byte.
A one-second native render is pinned to an exact FNV-1a hash. That test catches errors that command-level comparisons alone might miss in synthesis or sample scheduling.
Finding exact loop points
A loop should not be guessed by trimming silence or listening for a beat. Some tracks have introductions, and different OPL voices can loop on different schedules.
The research interpreter records the complete command-interpreter state. It searches for the first state that repeats exactly. The repeated state defines the true command-cycle boundary.
For Fun 1, the cycle is approximately 59.04 seconds. Another track, Lemming 2, has a much longer combined voice cycle. Using exact state repetition eliminates audible gaps and incorrect early restarts.
Avoiding harshness and clipping
The approved path uses DBOPL’s raw 32-bit output and applies controlled scaling instead of inheriting a clipping mixer. Validation renders found zero clipped synthesis samples across all 39 command streams.
Two gentle fixed-point low-pass stages soften the final signal. This reduces harsh high-frequency energy without replacing the underlying OPL character.
12. Why device audio was harder than Simulator audio
The Simulator runs on a Mac with far more processing power than the Playdate. Audio that is perfect in the Simulator can run slowly, detune, distort, or stutter on the physical handheld if synthesis cannot finish before the audio hardware needs more samples.
Gameplay remaining smooth while music slowed was an important diagnostic clue: the simulation was healthy, but the audio production path was missing deadlines.
Reference rate and device rate
The reference synthesis rate is 49,716 Hz. On device, running the full DBOPL workload at that rate alongside gameplay was too expensive.
The optimized device path synthesizes at 22,050 Hz, safely above its final 8.5 kHz passband, then uses deterministic 2× linear interpolation for Playdate’s 44.1 kHz output callback.
A fractional scheduler preserves the approved 49,716 Hz command timing rather than rounding every tick independently. The complete Fun 1 cycle differs by only two output frames over about 59.04 seconds.
This is a controlled tradeoff: reduce the expensive chip-emulation rate while preserving musical event timing and filtering the output appropriately.
Render ahead, do not synthesize in the callback
Audio callbacks have hard deadlines. They should not allocate memory, read files, or perform unpredictable amounts of work.
The final architecture uses a ring buffer:
Main game loop SDK audio callback
-------------- ------------------
Run DBOPL ahead Copy ready samples
Fill ring buffer -------------> Advance read index
Aim for 6,144 frames Zero-fill only on underrunThe ring holds 8,192 mono frames. The main update loop synthesizes toward a 6,144-frame high-water mark, approximately 139 milliseconds of queued audio. The SDK audio thread only copies already-published samples.
Atomic producer and consumer indices prevent the two contexts from seeing half-updated state. Track changes flush safely. If the producer ever falls behind, the callback inserts zeroes and increments an underrun counter instead of reading invalid memory.
This is why “put it on a dedicated thread” was not the complete answer. The useful design principle was to separate expensive production from deadline-sensitive consumption. The SDK callback already executes in the audio context; making that context do more work would not help.
Sound effects
Fifteen short effects are imported as IMA ADPCM. ADPCM is compressed audio suited to small clips. They are preloaded and played from a four-voice pool, eliminating trigger-time file reads.
Most action effects retain the DOS AdLib character. The iconic spoken lines—“Let’s Go!”, “Oh no!”, and “Yippee!”—use original Amiga captures selected for their recognizable voices. Per-effect gains compensate for different recording levels so effects do not overpower the live music.
Gameplay emits semantic events such as assignment, failure, splat, drowning, burning, exit, “Oh no,” and explosion. The portable core does not know which audio API will play them; the Playdate layer drains the bounded event queue and chooses the corresponding sound.
13. Saving progress and presenting a complete game
Faithfulness is not limited to physics. A preservation port also needs a coherent path through the game.
The LPS1 save format stores completion bits and the last-played rating. A bit is a single binary value, making it compact to record whether each of 120 levels is complete.
Within each rating, the player may select completed levels and the first incomplete level. All four difficulty ratings are available, matching the original ability to move between Fun, Tricky, Taxing, and Mayhem rather than forcing one uninterrupted sequence.
The title screen preserves the complete logo and credits DMA Design for the original game. A detailed DOS credits screen is available separately. Completing the final Mayhem level displays the original salute to master Lemmings players.
The system menu is contextual:
- Dither selection appears on the title screen.
- Choose Level and Reset Level appear during a level.
- Continue and Nuke are grouped under the game control.
Contextual menus keep irrelevant options from cluttering the screen.
14. Testing the preservation claims
“It looks right to me” is useful feedback, but it is not enough for a preservation project. The port uses several kinds of tests.
Unit tests
Unit tests examine a small rule in isolation. Examples include:
- A PackBits stream round-trips exactly.
- A malformed archive is rejected.
- A lemming can receive a climber skill only when eligible.
- A 62-pixel fall survives and a 63-pixel fall does not.
- Progression unlocks the first incomplete level.
Integration tests
Integration tests verify that components work together. The asset tests build and reopen an entire pack. The host runner loads all 120 levels through the same bounded path used on Playdate.
Smoke tests
A smoke test checks that a broad operation completes without obvious failure. Every level runs for 300 ticks while release and alive-count invariants are checked.
Golden and oracle tests
A golden test compares output against an approved exact result. Audio register traces and PCM hashes are examples. These tests are extremely good at catching tiny timing or routing changes.
What is covered
The validation suite includes:
- All 71 preservation input hashes
- Known compressed-section shapes and failure cases
- Canonical 120-level ordering
- Pack structure and version
- Bounded level decompression
- Original sprite opacity masks
- Black lemming silhouettes
- Sprite atlas deduplication
- Terrain and portal dither planes
- Terrain edits propagated through every visual mode
- Sprite and object animation loading
- Spawning, movement, and release timing
- All eight skills
- Steel preservation and terrain destruction
- Selection bounds and assignment eligibility
- Traps and exits
- Sound-event transitions
- Five-second bombers and slot-ordered nuke
- Win and loss conditions
- Save progression
- All 39 AdLib streams over 10,000 timer steps
- Exact music command loops
- PCM golden output and clipping checks
- Ring-buffer producer/consumer behavior and underruns
- Host, Simulator, and ARM compilation with warnings treated as errors
Why real device testing still mattered
Automated tests proved data and logic, but only physical hardware exposed the real-time audio budget. Human play also found usability and fidelity problems that were difficult to predict from isolated tests.
The best workflow used both:
Automated checks catch known rules and regressions.
Human comparison discovers missing rules and perceptual problems.
New discoveries become automated checks whenever possible.15. Iteration was part of the reverse engineering
The Git history shows that fidelity emerged through repeated, focused corrections rather than one final rewrite.
Selected milestones include:
| Commit | Improvement |
|---|---|
63ca58b | Initial preservation port, converter, portable engine, tests, and Playdate adapter |
032e903 | Pixel-perfect detail rendering |
e4fdab7 | Selectable dithering and exact music loops |
2813e2a | Native audio and complete Playdate presentation |
4957a9b | Complete action panel and level flow |
d42e736 | Corrected clock and HUD counters |
55e246f | Redesigned release-rate control |
776c8e3 | Corrected fall-distance accounting |
0917287 | Fixed the first-lemming release delay |
2dca6cb | Tuned falling to 2.2 source pixels per tick on average |
18c4409 | Centered opening views on entrance hatches |
Several lessons recur in these changes.
Perceptual correctness is measurable
“Falling feels absurdly fast” begins as subjective feedback. It becomes engineering when speed is expressed using named constants, animation cadence is tied to travelled distance, and fatal-distance tests remain valid when those constants change.
Platform changes expose hidden assumptions
The DOS start-scroll value was not wrong. It assumed a 320-pixel viewport. The Playdate’s exact 2× crop is only 200 source pixels wide. Centering on the hatch preserved the intent under a new viewport.
Exact data is better than imitation
Using original masks, poses, particle coordinates, music command streams, level records, and palettes eliminated entire categories of approximation.
Selective adaptation is better than dogma
The port preserves gameplay pixels exactly but uses native-resolution menus. It preserves OPL commands but lowers the chip-emulation rate on hardware. It keeps original progression while adding a Playdate-friendly level browser. Fidelity is preservation of the experience and rules, not blind preservation of every old limitation.
16. Why the result is so faithful
The port’s quality comes from the combination of several choices.
- The inputs are pinned. Every claim refers to a known version of the game.
- Original data is converted, not manually recreated. Levels, placements, masks, sprites, palettes, and command streams retain their authored structure.
- The executable is treated as evidence. Important tables and timing behavior are recovered from the actual program rather than memory or folklore.
- Rules are written in source-pixel units and fixed ticks. Rendering changes do not silently change physics.
- Pixel scaling is exact. Tiny sprite geometry is never interpolated.
- Dithering is selective and world-anchored. Ground gains tone without making moving art shimmer.
- Music is synthesized from original commands. There are no arbitrary recording lengths or PCM loop seams.
- Independent audio implementations cross-check one another. A bug is less likely to agree across separately written interpreters.
- The core is portable and testable away from Playdate. Game logic can be exercised quickly on a desktop.
- Human observations become regression tests. Bugs that mattered in real levels were turned into permanent boundaries.
- The hardware is respected. Audio buffering, fixed memory, compression, and native UI address the real device rather than assuming Simulator behavior.
- Claims remain honest. The project calls DOS-exactness a validation target where frame-perfect golden replay comparison is not yet complete.
The final point matters. A high-quality preservation project documents both what has been proven and what remains a target. Confidence comes from evidence, not from using the word “perfect.”
17. Reproducing the research and build workflow
The public repository deliberately excludes original game data, imported recordings, launcher art, generated packs, and private decompiler output. A reader must supply a legally obtained compatible DOS copy and required private assets.
Development environment
On a Nix-capable system:
nix develop
make playdate-sdkThe pinned environment supplies Clang, Python with Pillow, FFmpeg, Node, ZIP tools, and the ARM compiler. The SDK target fetches the pinned Playdate SDK into an ignored build directory.
For reverse engineering tools:
nix develop .#decompileThis adds Ghidra, a Java Development Kit, and ndisasm.
Verify the original files
make verify-referenceRun this first. If it fails, do not assume later offsets or output are valid. Determine whether a file is missing, extra, or from another release.
Build converted assets
make assetsThis verifies the reference set, decodes the original files, builds the canonical level sequence, composes and dithers graphics, constructs the sprite atlas, extracts exact tables, and writes the versioned LPD1 pack under generated/.
Run automated tests
make testThis runs Python conversion/audio tests and native core tests. Warnings are treated as errors so suspicious C or C++ code does not quietly enter a build.
Run all levels on the desktop
make hostThe host program loads and smoke-simulates all 120 levels. This is much faster than manually opening every level on the handheld.
Rebuild the private decompilation workspace
make decompileThis verifies the source executable, unpacks it reproducibly, writes a flat 16-bit disassembly, creates a Ghidra project, and exports private pseudocode beneath preservation/private/.
Render independent audio references
make audio REFERENCE_PLAYER=/path/to/local/lemmings.jsThe local reference player is intentionally not part of the public repository. The command interpreter and DBOPL oracle produce traces and renders used to validate routing, timing, loops, and synthesis.
Import sound effects
make effects EFFECTS_ARCHIVE=/path/to/LemmingsVersionsNLSounds.zipThe importer verifies the accepted archive by SHA-256, chooses the intended DOS effects and Amiga speech, converts them to IMA ADPCM, and stages the 15 runtime clips.
Build for Simulator
make simulator PLAYDATE_SDK_PATH=/path/to/PlaydateSDK
make playdate-runThe first command compiles and stages the Simulator package. The second builds and launches it.
Build for a physical Playdate
make playdate PLAYDATE_SDK_PATH=/path/to/PlaydateSDKDevice builds require a complete Arm GNU Toolchain with newlib and C++ support. If it is not first on the shell’s path, provide ARM_TOOLCHAIN_PATH explicitly.
Package a release
make release PLAYDATE_SDK_PATH=/path/to/PlaydateSDKThe release process advances the semantic patch version and Playdate build number together, writes the PDX, and creates an upload-ready ZIP. Test-unlock builds contain a marker that the release packager refuses, preventing an internal all-level build from being published accidentally.
18. Common problems and what they teach
Ghidra shows nonsense
Confirm that the executable was unpacked successfully, that its raw size is exactly 82,906 bytes, that the processor is 16-bit x86 real mode, and that address zero was seeded as an entry point. Wrong load assumptions can make valid machine code look meaningless.
Converted graphics have the wrong colors
Indexed pixels only make sense with the correct palette. Decode the palette from the matching graphics set, preserve masks separately, and test known frames. Do not infer transparency from a color value when an explicit source mask exists.
Sprites look uneven
Check for fractional scaling, smoothing, or screen-relative dithering. Exact 2× nearest-neighbor expansion should create four identical output pixels for every source pixel.
Music resembles the original but notes or instruments are wrong
Compare the complete OPL register stream against an independent oracle. Listening alone cannot easily distinguish a bad instrument-table offset, wrong channel routing, incorrect timing, or incorrect track mapping.
Music loops with a gap
Find a repeated full interpreter state, not merely a visually similar waveform point. Independent voices may make the true cycle much longer than the melody suggests.
Simulator audio is good but device audio is slow or distorted
Measure underruns and reduce work performed in the audio callback. Render ahead, preallocate memory, preload effects, use a ring buffer, and consider a lower synthesis rate with deterministic conversion.
A single lemming behaves differently after using a skill
Inspect transition bookkeeping. State changes may initialize counters or positions differently from a lemming that enters the same state naturally. Test the exact level scenario and both sides of any distance boundary.
19. Preservation and licensing boundaries
This project’s portable engine, Playdate adapter, converter, and documentation are newly written code. They are licensed under the project’s MIT terms.
DBOPL is derived from DOSBox and is licensed under GPL-2.0-or-later. Because DBOPL is linked into the built game, distributed binaries must meet the applicable GPL source and build-instruction obligations described in LICENSING.md.
Original game data, art, levels, music data, recordings, names, and other third-party material are not relicensed by the project. The current public repository therefore excludes:
reference/- Generated packs and builds
- Private Ghidra output
- Imported sound recordings
- Original or derived launcher artwork
This document describes the method but does not distribute those materials and is not legal advice. Anyone repeating the work should use a lawfully obtained copy and understand the rules applying in their jurisdiction and distribution context.
20. Lessons for another preservation port
For someone beginning a similar project, the most transferable checklist is:
- Identify one exact reference version and hash every input.
- Never modify the reference files.
- Separate private evidence from publishable implementation.
- Automate unpacking and tool versions so analysis can be reproduced.
- Learn the data formats before redrawing or approximating assets.
- Keep game rules independent of rendering and platform APIs.
- Express timing in fixed logical ticks and distances in source units.
- Use exact masks and tables when they exist.
- Create at least one independent oracle for difficult subsystems.
- Turn every confirmed bug into a regression test.
- Test on real hardware early, especially audio and memory behavior.
- Adapt controls and menus intentionally while preserving game semantics.
- Record what is proven, what is inferred, and what still needs comparison.
- Keep builds deterministic and validate their hashes and sizes.
- Treat human playtesting as evidence generation, not as a substitute for tests.
Conclusion
This Playdate port became extremely faithful because it did not rely on a single technique.
Decompilation helped expose hidden executable behavior. File-format work recovered authored data. A deterministic converter made that data practical on the handheld. A clean fixed-tick C engine expressed the rules clearly. Exact integer rendering protected the sprite art. Selective world-anchored dithering solved the one-bit display problem. Real-time OPL2 synthesis preserved the musical programs. Independent oracles and regression tests made subtle claims measurable. Finally, repeated Simulator and device play turned perceptual problems into concrete engineering corrections.
The project is therefore best understood not as “running decompiled Lemmings on Playdate,” but as a layered preservation effort:
“Preserve the evidence, recover the rules, retain the authored data, reimplement the system cleanly, adapt only where the new hardware requires it, and verify every important claim.
That combination is what allowed a game designed for a 1991 DOS PC to feel at home on a tiny 2020s handheld without losing the character, timing, challenge, artwork, or sound that made the original recognizable.
Repository reference map
The following files are useful companions to this document:
| File or directory | What to study |
|---|---|
README.md | Current build requirements, controls, and public/private boundaries |
docs/PORT.md | Architecture and current fidelity policy |
docs/VALIDATION.md | Exact automated and artifact validation record |
preservation/README.md | Preservation workflow and inventory findings |
preservation/prepare_decomp.py | Reproducible PKLITE unpacking and Ghidra setup |
preservation/ghidra_scripts/ | Entry-point seeding and private pseudocode export |
preservation/music.md | Executable evidence for music selection |
tools/assetc/ | Level, graphics, sprite, and pack conversion |
tools/audio/ | Independent Sound Images interpretation and reference rendering |
playdate/core/lp_game.c | Portable fixed-tick gameplay simulation |
playdate/core/lp_adlib.c | Runtime command interpreter, scheduling, filtering, and ring buffer |
playdate/src/main.c | Playdate-specific drawing, input, menus, saves, and sound playback |
playdate/tests/ | Gameplay, progression, DBOPL, and callback-audio tests |
CHANGELOG.md | Release-level account of fidelity improvements |
LICENSING.md | MIT, DBOPL GPL, font, and original-material boundaries |