A player walks towards a wall. The game needs to decide whether the next step is allowed. Reading the picture back from the screen might work for a tiny game, but what happens when an enemy stands over the wall, or the camera scrolls it out of view?
Keep the level as data. Each cell holds a tile ID. Drawing code chooses its appearance; movement and interaction code ask what that ID means. A wall can change colour without becoming walkable. A floor can stay in the map while a player passes over it.
Choose your machine
These are reusable routines with complete small callers, rather than a new game project. All supply checked tile reads/writes, property lookup and world-position conversion. The BASIC group also supplies a bounded text-region adapter; native graphical paths remain separate work.
| Machine | Implementation |
|---|---|
| Acorn Atom | BASIC routines and calling contract |
| Amstrad CPC | BASIC routines and calling contract |
| Atari 800XL | BASIC routines and calling contract |
| Commodore PET | BASIC routines and calling contract |
| Commodore VIC-20 | BASIC routines and calling contract |
| Dragon 32 | BASIC routines and calling contract |
| Mattel Aquarius | BASIC routines and calling contract |
| MSX1 | BASIC routines and calling contract |
| Oric Atmos | BASIC routines and calling contract |
| Spectravideo SVI-328 | BASIC routines and calling contract |
The native byte-map group uses the machine’s CPU directly. The diagnostic ROMs/cartridges isolate query code and RAM; they do not claim a stock-OS loading or display path.
| Machine | Implementation |
|---|---|
| BBC Micro | Native routines and calling contract |
| Acorn Electron | Native routines and calling contract |
| Commodore 64 | Native routines and calling contract |
| Atari 2600 | Native routines and calling contract |
| Atari 5200 | Native routines and calling contract |
| Atari 7800 | Native routines and calling contract |
| NES | Native routines and calling contract |
| ZX Spectrum | Native routines and calling contract |
| Jupiter Ace | Native routines and calling contract |
| ZX80 | Native routines and calling contract |
| ZX81 | Native routines and calling contract |
| ColecoVision | Native routines and calling contract |
| Sord M5 | Native routines and calling contract |
| Memotech MTX | Native routines and calling contract |
| Tatung Einstein | Native routines and calling contract |
| Master System | Native routines and calling contract |
| Game Gear | Native routines and calling contract |
| SG-1000 | Native routines and calling contract |
| Game Boy | Native routines and calling contract |
| Amiga | Native routines and calling contract |
Each machine has its own source and execution evidence. Matching syntax does not establish matching firmware or graphics behaviour. The coverage inventory names all 30 currently emulated systems, their implementation and verification level, and the unfinished display/loader paths.
Rows first, columns second
An eight-column map stores row zero’s eight cells, followed by row one’s eight cells, and so on. For zero-based column C and row R, the array index is R*W+C, where W is the map width. Column 3, row 2 of an eight-column map is entry 19.
The routines check coordinates before using that index. Without the check, column W can spill into the next row while still producing an apparently valid array subscript. A negative coordinate can fail differently again. They return a separate success value so an invalid position cannot quietly masquerade as floor tile zero.
A flat numeric array keeps the arrangement easy to inspect and edit. It is not the smallest or fastest representation on every machine. The map’s width is independent of the screen width, and a map query need not draw anything.
A tile ID is not its artwork
The sample uses four IDs: floor, wall, hazard and goal. A separate table gives each ID a property value. You can change a wall’s appearance without changing its behaviour, or give several differently drawn walls the same blocking property.
The provided property lookup returns a number. Your game chooses what that number means. Simple classes are often enough; combinations of bit flags are another option when a tile can block, hurt and conceal something at once. Terrain data, object occupancy and temporary effects can also remain separate rather than squeezing every combination into a new tile ID.
This lookup answers which tile occupies a cell. A box collision test answers whether two shapes overlap. For an actor spanning several cells, query the cells touched by its bounds; checking only its centre can miss a wall at its edge. Resolving motion and preventing fast objects from crossing walls are further techniques.
Convert positions in the right space
With eight world units per tile, positions 0–7 belong to column zero, 8–15 to column one. Divide the non-negative world position by the tile size and round down. The supplied conversion also performs the checked map lookup.
Use world coordinates for a world map. Subtracting the camera gives a screen position, which can identify a different cell entirely. The sample rejects negative world positions before division; maps with a signed origin or wrapping need an explicit policy. Rectangular cells need separate horizontal and vertical sizes.
Draw only the region you need
The text adapter accepts a source column/row and a width/height. It rejects a region that extends outside the map before printing any of it. A valid region prints at the current cursor; the caller owns screen placement.
That is useful for a room, a debug view or a small text game. Native graphical adapters must translate IDs into glyphs, bitmaps or hardware tile entries and handle the chosen machine’s layout, colours, attributes and write restrictions. A successful BASIC text map does not establish those paths.
Native region drawing now provides separately verified VDP adapters for nine machines.
Map changes and display changes are separate. Redraw the affected cells when scenery changes. When removing an actor, the map may also provide the scenery needed to restore its old area, provided transient objects and drawing order are handled separately.
Choose the next representation when you need it
Numeric arrays are comfortable to work with, but each element uses the interpreter’s numeric storage. Byte maps save space; packed IDs save more at the cost of extraction work. Row pointers can replace repeated multiplication. A ROM map with a small table of changed cells can avoid a full writable copy. Large worlds may decode one room or stream map columns into a cache.
Those choices deserve concrete routines and measured workloads. Start with the readable version, keep its query contract clear, and change the representation when the game gives you a reason. The scrolling family covers the separate job of moving the view.
BASIC uses the same logical questions
The BBC BASIC version and Commodore 64 version use zero-based arrays. Sinclair BASIC and MTX BASIC add one at the final array access. The map coordinates stay zero-based in all four. That is a useful distinction to keep when translating a routine: its logical interface need not inherit the interpreter’s array base.
More assembly routes beside BASIC
The Atom, PET, unexpanded VIC-20 and Oric now have assembly callers for the same checked 6502 include. Their existing BASIC entries remain separate. Each assembly caller has its own reset fixture and mapped RAM reservation; the VIC-20 cannot simply borrow the PET addresses. These qualify CPU queries and guarded writes, while native drawing and stock-ROM integration remain separate work.
Remaining assembly map implementations
The CPC and unexpanded Aquarius share the checked Z80 routine but reserve different RAM. MSX1 selects its RAM slot explicitly in the reset fixture; SVI-328 uses its own memory arrangement. Neither should borrow the other’s startup registers.
The Atari XL reuses the 6502 include. The Dragon 32 supplies a 6809 implementation with big-endian pointers and world positions. All 30 machines now have assembly CPU/RAM map-query implementations in the source inventory; each language still needs its own execution evidence. BASIC and FORTH are separate routes, and these memory diagnostics do not establish native drawing or firmware-facing loading.
FORTH keeps the byte map visible
The Jupiter Ace FORTH implementation uses CREATE/C, for the map, byte reads/writes and explicit stack effects for checked queries. It runs on the unexpanded Ace through the stock ROM. Its active descriptor, signed world conversion and dictionary/stack costs differ from the separately verified assembly routine; a shared machine name does not make their language evidence interchangeable.
Two early Sinclair BASIC dialects
The ZX80 BASIC port uses zero-based integer arrays; the ZX81 BASIC port uses one-based floating-point arrays with explicit INT conversion and fractional-index checks. Both expose zero-based logical cells, and both qualify the complete readable example on a 16 KiB configuration. Their real-ROM callers, customisation and fault runs are separate from the assembly evidence. Neither entry claims native region drawing.
A RAM setting is not a requirement
The Sinclair BASIC examples show why we measure the program we actually ask readers to run. Their unchanged readable callers run on 2 KiB (ZX80) and 4 KiB (ZX81); the earlier 16 KiB diagnostic setting was not a minimum. Separate stock 1 KiB paths now exercise all four operations. ZX80 keeps its API and generates the room more cheaply; ZX81 uses a character string and fixed descriptors. Both retain the readable alternative, with measured storage and explicit contract differences.