goldbox.areas

The areas of Pool of Radiance, and the names to put on a map.

This module is the single source of truth for the area table. It replaces the hand-written dictionary that used to live in automap/state.py, and the thirty-row table in docs/118-debug-mode.md is the same data written out.

The key is the area id, not the map file. An area is a script – ECL<id>, with the id in hex, so area 21 is ECL15 – and the game’s own transition opcode NEWECL takes exactly that id. The map is a consequence of the script, not the other way round, and the relation is not a bijection:

  • thirty scripts, twenty-nine maps. ECL0C does not exist at all, so id 12 is absent from the table;

  • three areas have no map. ECL08 (Phlan City Hall), ECL0B (the training hall) and ECL13 (Cave of Diogenes) issue no LOADFILES and never put a GEO on the screen. ECL1E was believed a fourth until #260 (Area 30 is recorded as having no map, and ECL1E loads GEO12): it carries LOADFILES 18, 2, 255 at $9A54, four statements after it writes the party’s square, and file 18 is GEO12, Podol Plaza’s own map;

  • three areas carry two maps each. ECL10, ECL18 and ECL1D each load two `GEO`s from the one script;

  • three areas are outdoors. Areas 25-27 load a SQRDATA as well as a GEO; LOADFILES picks the file type from $49E6, not from the operand.

So Area.geos is a tuple, which is empty for the three mapless areas and holds two entries for the three doubled ones, and areas_for_geo returns a tuple as well – there is nothing in the format that stops two scripts naming one map, and ECL07 already loads GEO03 on its way into area 5. ECL1E is a second example: its GEO12 is also area 18’s, Podol Plaza’s.

Names come from docs/88-map-files.md and two write-ups since lost, work/reports/world-map.md and work/reports/quest-flags.md; arrival squares were harvested from the departing scripts’ SAVE <n>, mapX and the arriving scripts’ entry 4. Fourteen areas have no known arrival square and say so with arrival = None. FastTraveling into all fifteen that had none at the time and watching where the party ends up P20, whose write-up work/reports/p20-arrivals.md is lost: it found area 21’s square in ECL15’s own bytecode, and landing_square at the foot of this module is the fallback that measurement put in place of “the first square with a passable edge”.

A name is a title, so it is title-cased, leading article included: “The Slums”, not “the Slums”. The table used to mix the two – proper names in capitals, descriptions in lower case – and the seam showed in the dropdown, which is a list of titles and nothing else. Short function words inside a name stay down (“Valjevo Castle, a Floor”, “Temple of Bane”).

## Titles

GEO15 exists in both Pool of Radiance and Curse of the Azure Bonds and means a different place in each (docs/120-curse-testing.md), so a table keyed by map file alone cannot help lying. GEO_NAMES is therefore keyed by game title first, and area_name degrades an unrecognised title – or an unrecognised map – to "area 21", read straight off the file’s own hex digits, rather than to a confident wrong answer.

AREAS_SILVER_BLADES is the second table, built for #20 (Build an area table for Silver Blades) and read off that title’s own six sides by tools/areatable.py. It shares the Area shape and nothing else: sparse ids, no names, disk sides 1-6, and five areas whose map is not their own id. Its own comment carries what does not carry over, and every row is PROBABLE.

AREAS_CURSE is the third, measured for #192 (Convert a Curse of the Azure Bonds DOS save into a C64 one, which the importer refuses today) step 0b by the same tool. It carries no name and no arrival square – a conversion needs neither – and every row is UNKNOWN, because unlike Silver Blades no Curse area has been entered by a driven fast travel yet, only walked by the bytecode.

Enumerating maps by count or assuming a GEO00 is wrong for every Gold Box title after this one: Curse’s ids are sparse and chapter-grouped, and Silver Blades, Champions and Death Knights start at $10 or $20 (write-up lost, work/reports/goldbox-inventory.md; the per-title base addresses are asserted in tests/test_curse.py::test_the_addresses_are_the_ones_measured). Scan a directory; never a range.

Module Attributes

POOL_OF_RADIANCE

Game titles this module knows about.

MISSING_ID

ECL0C is not on any of the nine disks, so there is no area 12.

FACINGS

Facing, as the game stores it in $C04D.

AREAS

The table.

AREAS_SILVER_BLADES

Secret of the Silver Blades, read off its own six sides by tools/areatable.py (#20 (Build an area table for Silver Blades)).

AREAS_CURSE

twenty-five scripts, on six sides (CURSE_A.D64-CURSE_F.D64), ECL64/ECL65 excluded the same way Silver Blades excludes its two -- they carry no LOADFILES and are not places.

TABLES

Game title -> that title's areas.

STARTS

Game title -> where a party that has not set out begins.

GEO_NAMES

Game title -> map file -> the name to show.

Functions

area(id)

The area with this id in Pool of Radiance, or None -- id 12 has no script.

area_in(id, title)

The area with this id in this title, or None.

area_name(geo[, title])

A name for a map that is never a lie about another game.

areas_for(title)

Every area of this title that anybody has written down.

areas_for_geo(geo)

Every area that loads this map.

areas_for_title(title)

Every area a fast travel may offer, which is nothing for four of six.

components(geo)

The map's squares grouped by what can walk to what.

geo_name(geo[, title])

The place this map is, in this game -- or None if we cannot say.

geo_number(geo)

GEO15 -> 21.

geos_for_area(id)

The maps this area loads: two, one, or none.

geos_in(title)

Every map any area of this title loads.

iter_areas()

landing_square(geo)

Where to drop a party on a map that has no arrival square of its own.

start_area(title)

The row start_of names, so a caller gets the map and the disk too.

start_of(title)

Where a party of this title that has not set out begins.

Classes

Area

One ECL script, and whatever map or maps it loads.

Arrival

The square the game itself puts the party on when it enters an area.

Start

Where a party that has not begun adventuring goes when it does.

goldbox.areas.AREAS: tuple[goldbox.areas.Area, ...] = (Area(id=0, name='New Phlan', disk=3, geos=('GEO00',), sqrdata=None, arrival=Arrival(x=15, y=1, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=1, name='Buccaneer Base', disk=6, geos=('GEO01',), sqrdata=None, arrival=Arrival(x=8, y=0, facing=2), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=2, name='Cadorna Textile House', disk=4, geos=('GEO02',), sqrdata=None, arrival=Arrival(x=0, y=4, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=3, name='Valjevo Castle, a Floor', disk=5, geos=('GEO03',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.PROBABLE: 'PROBABLE'>, geo_names={}, dynamic_geo=True, fasttravelable=True, side_name='POOL{}'), Area(id=4, name='Valjevo Castle, a Floor', disk=5, geos=('GEO04',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.PROBABLE: 'PROBABLE'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=5, name='Valjevo Castle, a Floor', disk=5, geos=('GEO05',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.PROBABLE: 'PROBABLE'>, geo_names={}, dynamic_geo=True, fasttravelable=True, side_name='POOL{}'), Area(id=6, name='Valjevo Castle, a Floor', disk=5, geos=('GEO06',), sqrdata=None, arrival=Arrival(x=4, y=15, facing=0), overland=None, confidence=<Confidence.PROBABLE: 'PROBABLE'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=7, name='Valjevo Castle, the Pool', disk=5, geos=('GEO07',), sqrdata=None, arrival=Arrival(x=5, y=7, facing=None), overland=None, confidence=<Confidence.PROBABLE: 'PROBABLE'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=8, name='Phlan City Hall', disk=3, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=9, name='Stojanow Gate', disk=2, geos=('GEO09',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=10, name='Valhingen Graveyard', disk=4, geos=('GEO0A',), sqrdata=None, arrival=Arrival(x=0, y=4, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=11, name='The Training Hall', disk=3, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=13, name='The Kobold Caves', disk=8, geos=('GEO0D',), sqrdata=None, arrival=Arrival(x=6, y=15, facing=0), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=14, name='Kovel Mansion', disk=3, geos=('GEO0E',), sqrdata=None, arrival=Arrival(x=4, y=0, facing=2), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=15, name="Mendor's Library", disk=2, geos=('GEO0F',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=16, name='The Lizardman Keep', disk=8, geos=('GEO10', 'GEO1E'), sqrdata=None, arrival=Arrival(x=8, y=14, facing=0), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=17, name='The Nomad Camp', disk=7, geos=('GEO11',), sqrdata=None, arrival=Arrival(x=1, y=14, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=18, name='Podol Plaza', disk=1, geos=('GEO12',), sqrdata=None, arrival=Arrival(x=0, y=4, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=19, name='Cave of Diogenes', disk=6, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=20, name='The Slums', disk=2, geos=('GEO14',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=21, name='Sokol Keep', disk=4, geos=('GEO15',), sqrdata=None, arrival=Arrival(x=8, y=14, facing=0), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=22, name="Yarash's Pyramid", disk=7, geos=('GEO16',), sqrdata=None, arrival=Arrival(x=15, y=7, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=23, name="Yarash's Pyramid, Lower", disk=7, geos=('GEO17',), sqrdata=None, arrival=Arrival(x=15, y=0, facing=2), overland=None, confidence=<Confidence.PROBABLE: 'PROBABLE'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=24, name='Temple of Bane', disk=1, geos=('GEO18', 'GEO1F'), sqrdata=None, arrival=Arrival(x=15, y=4, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=25, name='Wilderness, West Window', disk=6, geos=('GEO19',), sqrdata='SQRDATA04', arrival=None, overland=(14, 29), confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=26, name='Wilderness, Middle Window', disk=7, geos=('GEO1A',), sqrdata='SQRDATA05', arrival=None, overland=(7, 29), confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=27, name='Wilderness, East Window', disk=8, geos=('GEO1B',), sqrdata='SQRDATA06', arrival=None, overland=(9, 29), confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=28, name='Zhentil Keep Outpost', disk=6, geos=('GEO1C',), sqrdata=None, arrival=Arrival(x=7, y=0, facing=2), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=29, name="Kuto's Well", disk=8, geos=('GEO1D', 'GEO20'), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names=mappingproxy({'GEO20': "Kuto's Well Catacombs"}), dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=30, name=None, disk=1, geos=('GEO12',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=False, side_name='POOL{}'))

The table. Ordered by id, with 12 absent because ECL0C is absent.

The five POOL5 floors are a castle because ECL07 writes ledger flag 20 and prints the party leaving one; which floor is which is not known, which is why they are PROBABLE and why four of them share a name.

goldbox.areas.AREAS_CURSE: tuple[goldbox.areas.Area, ...] = (Area(id=1, name=None, disk=2, geos=('GEO01',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=2, name=None, disk=2, geos=('GEO01',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=3, name=None, disk=2, geos=('GEO03',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=4, name=None, disk=2, geos=('GEO04',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=16, name=None, disk=3, geos=('GEO10',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=17, name=None, disk=3, geos=('GEO11',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=18, name=None, disk=3, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=21, name=None, disk=3, geos=('GEO15',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=30, name=None, disk=1, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=32, name=None, disk=4, geos=('GEO20',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=33, name=None, disk=4, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=34, name=None, disk=4, geos=('GEO21',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=35, name=None, disk=4, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=37, name=None, disk=4, geos=('GEO25',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=48, name=None, disk=5, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=49, name=None, disk=5, geos=('GEO32',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=50, name=None, disk=5, geos=('GEO32',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=51, name=None, disk=5, geos=('GEO33',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=53, name=None, disk=5, geos=('GEO35',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=64, name=None, disk=6, geos=('GEO40',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=66, name=None, disk=6, geos=('GEO42',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=67, name=None, disk=6, geos=('GEO43',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=69, name=None, disk=6, geos=('GEO45',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=80, name=None, disk=1, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=81, name=None, disk=1, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'))

twenty-five scripts, on six sides (CURSE_A.D64-CURSE_F.D64), ECL64/ECL65 excluded the same way Silver Blades excludes its two – they carry no LOADFILES and are not places. Measured for #192 (Convert a Curse of the Azure Bonds DOS save into a C64 one, which the importer refuses today) step 0b by tools/areatable.py curse-of-the-azure-bonds, which walks every script’s control flow from its five entry GOTO`s and reads the `LOADFILES operands, corroborated by the loader’s own disk byte $7F12 agreeing with the target script’s side on all 23 static NEWECL`s it precedes, and re-checked directly off the disks by `tools/cursedisk.py --check-areas on 2026-09-05 with 0 disagreements against this table.

confidence is UNKNOWN for every row, unlike Silver Blades’ table: no Curse area has been entered by a driven fast travel, only walked by the bytecode. The geos column is the strongest part of it regardless – ECL02 loads GEO01, ECL22 loads GEO21 and ECL31/ECL32 both load GEO32, so it cannot be derived from the id the way a straight numbering could be.

There is no area 0, and the table is not missing it. A DOS Curse save whose area word is 0 is one the player made from the party-formation menu before pressing BEGIN ADVENTURING, and 0 is the initialiser’s value rather than a place: no ECL00 is on any of the six sides and no ECL container holds a block 0, and the same goes for GEO00. Adding a row would move goldbox.dos’s refusal from area_in to _resident_geo rather than remove it, and a C64 save naming area 0 sends the loader after GEO00 – measured, $B7/$BB at the disk prompt – which it can never find. #301 (A DOS Curse save standing in area 0 is refused by the import, because no row of the area table names area 0) and docs/185-a-party-that-has-not-set-out.md have the measurements.

Type:

Curse of the Azure Bonds

goldbox.areas.AREAS_SILVER_BLADES: tuple[goldbox.areas.Area, ...] = (Area(id=4, name=None, disk=1, geos=('GEO10',), sqrdata=None, arrival=Arrival(x=10, y=8, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=16, name=None, disk=1, geos=('GEO10',), sqrdata=None, arrival=Arrival(x=15, y=8, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=17, name=None, disk=1, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=32, name=None, disk=2, geos=('GEO20',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=33, name=None, disk=2, geos=('GEO21',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=34, name=None, disk=2, geos=('GEO22',), sqrdata=None, arrival=Arrival(x=14, y=14, facing=0), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=48, name=None, disk=3, geos=('GEO31', 'GEO30'), sqrdata=None, arrival=Arrival(x=3, y=3, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=49, name=None, disk=3, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=True, fasttravelable=True, side_name='SILVER-{}'), Area(id=50, name=None, disk=3, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=True, fasttravelable=True, side_name='SILVER-{}'), Area(id=51, name=None, disk=3, geos=('GEO31',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=52, name=None, disk=3, geos=('GEO32',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=64, name=None, disk=4, geos=('GEO40',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=65, name=None, disk=4, geos=('GEO41',), sqrdata=None, arrival=Arrival(x=13, y=9, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=66, name=None, disk=4, geos=('GEO42',), sqrdata=None, arrival=Arrival(x=12, y=13, facing=0), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=68, name=None, disk=4, geos=('GEO44',), sqrdata=None, arrival=Arrival(x=7, y=15, facing=0), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=80, name=None, disk=5, geos=('GEO50',), sqrdata=None, arrival=Arrival(x=1, y=11, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=81, name=None, disk=5, geos=('GEO51',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=82, name=None, disk=5, geos=('GEO52',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=96, name=None, disk=6, geos=('GEO60',), sqrdata=None, arrival=Arrival(x=15, y=0, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=97, name=None, disk=6, geos=('GEO61',), sqrdata=None, arrival=Arrival(x=15, y=0, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=98, name=None, disk=6, geos=('GEO62',), sqrdata=None, arrival=Arrival(x=0, y=15, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=99, name=None, disk=6, geos=('GEO62',), sqrdata=None, arrival=Arrival(x=0, y=0, facing=2), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'))

Secret of the Silver Blades, read off its own six sides by tools/areatable.py (#20 (Build an area table for Silver Blades)). Twenty-two scripts, seventeen maps, ECL64 and ECL65 excluded because their first four bytes do not decode as the GOTO an area script opens with – they are on every side and are the machine, not a place.

Every row is CONFIRMED, and each one the same way: a party has been put in that area on a running machine and the landing measured. Twenty-one were entered by a trip through automap.actions.FastTravel – 22 hops across nine driven sessions – and $11 is where a loaded party starts, so it was read where it stood. #20 (Build an area table for Silver Blades), work/issue20/land1-land9.

The map column is 21 of 21 exact, an unmasked 1024-byte compare of $0400 against the copy on the player’s own disk, no fingerprint and no filename. That includes all five rows where an area’s map is not its own id – $04 and $10 both loaded GEO10, $30 and $33 both GEO31, $34 GEO32, $63 GEO62 – and $11, where a sweep of all of RAM found none of the seventeen maps anywhere, which is what a row with no geos claims.

The side column was confirmed by the game asking for it. This loader letters its sides: INSERT SIDE A through INSERT SIDE F for sides 1 to 6. Six letters, six sides, each drawn when a party was sent to an area this table puts on that side, each answered by attaching it.

`confidence` grades the id, the side and the map, and not `arrival`. Twelve arrivals into a row that carries a square have been measured, with (1, 1, 2) – a square no row carries – written into $C04B first, so that a table square read back afterwards could only be the arriving script’s own doing. Five reproduce the static reading, six land somewhere else, and one writes no square at all:

area

came from

this table

where the party actually landed

$30

$11

3,3 E

3,3 E

$41

$60

13,9 E

13,9 E

$44

$60

7,15 N

7,15 N

$50

$11

1,11 E

1,11 E

$62

$11

0,15 E

0,15 E

$10

$11

15,8 W

3,3 S

$42

$11

12,13 N

0,7 W

$60

$50

15,0 W

0,7 E

$60

$42

15,0 W

15,9 W

$61

$63

15,0 W

0,15 E

$04

$52

10,8 E

9,8 W

$63

$62

0,0 S

nothing written

Four rows with no arrival at all placed the party anyway, from the one came-from each was entered with: $20 at 10,14 W from $21, $21 at 15,9 N from $61, $40 at 15,7 W from $44 and $34 at 4,0 S from $11. They are recorded here rather than in the rows, because one measurement from one came-from is exactly the evidence $60 below shows to be insufficient. Four more wrote nothing and left the deliberately wrong square standing: $31, $32, $33 and $51.

$60 twice, from two different areas, landing in two different places, is the proof rather than an argument: an arrival square here can depend on `$4BF2`, and a fast travel hands the arriving script a came-from it may have no branch for. ECL34 and ECL51 were already known to branch that way when the table was built; ECL10, ECL42, ECL60 and ECL61 do it too and tools/areatable.py recorded the constant on the branch it happened to walk. ECL63 writes no square at all on this path: the deliberately wrong square survived it untouched.

None of that makes the field harmful – the arriving script wins wherever it writes one – but it is not a promise about where a fast-travelled party ends up. The disk column is the strongest part of the table: the side is where the file sits in the directory, 29 of 29 static SAVE <n>, [$7F12] before a NEWECL name that same side with 0 disagreeing, and the loader has now asked for six of them out loud.

Four things do not carry over from Pool of Radiance, and each is a trap for anything that reads this table expecting the older shape.

  • The ids are sparse and blocked by side. $04, $10-$11, $20-$22, $30-$34, $40-$42, $44, $50-$52, $60-$63. The high nibble is the side for twenty-one of the twenty-two, and ECL04 is the exception: it sits on side 1. Never enumerate by range.

  • An area’s map is not its own id in the `$3x` block. ECL30 loads GEO31, ECL33 loads GEO31 as well, ECL34 loads GEO32, ECL04 loads GEO10 and ECL63 loads GEO62. Pool of Radiance’s rule – every script’s LOADFILES first operand is its own id – is false here in five of the twenty-two.

  • The arriving script places the party, not the departing one. All twelve squares here come from the area’s own entry 4; exactly one NEWECL in the whole title writes a square before it (ECL44 sends the party to area $40 at 7,15 N), and that one contradicts ECL40’s own entry 4, so area $40 gets no square. Pool of Radiance harvested most of its sixteen the other way round.

  • A square is often computed rather than constant. ECL21 reads its through GETTABLE indexed by a variable, ECL34 and ECL51 branch on the came-from area $4BF2, and ECL34 adds 3 to whatever $C04B already holds. Nine rows have no arrival for that reason, and writing one for them would be inventing it.

class goldbox.areas.Area[source]

Bases: object

One ECL script, and whatever map or maps it loads.

__init__(id, name, disk, geos=(), sqrdata=None, arrival=None, overland=None, confidence=Confidence.UNKNOWN, geo_names=<factory>, dynamic_geo=False, fasttravelable=True, side_name='POOL{}')
Parameters:
Return type:

None

arrival: goldbox.areas.Arrival | None = None
confidence: goldbox.layout.Confidence = 'UNKNOWN'

What this grades is the `name`, and only the name. Every PROBABLE row is one whose name is a guess – the five Valjevo Castle floors and lower Yarash’s Pyramid – and the one UNKNOWN is area 30, which has no name because it is not a place: it is the attract-mode demo. A row’s other facts are graded where they are stated, in the comment beside it, which is why area 30 reads UNKNOWN here while its ECL, its disk, its GEO and its refusal to be fasttraveled are all CONFIRMED.

Said here because docs/118-debug-mode.md’s own table carries the same column and had graded area 30 CONFIRMED, against this file’s UNKNOWN, since before either said what the column was for. Settled 2026-09-07 at Donald’s asking: the two now agree, and both say what they are grading.

disk: int

Which disk side carries the script – 1-8 in Pool of Radiance, 1-6 in Secret of the Silver Blades. This is what a fasttravel writes to the loader’s disk byte ($6E12 in Pool of Radiance, $7F12 in Silver Blades) and what the loader will prompt for.

dynamic_geo: bool = False

do not pick a square off geos[0].** Two different mechanisms set it, one per title.

Pool of Radiance areas 3 and 5 issue no static LOADFILES at all and choose their map at run time with GETTABLE ..., mapDir; their geos entry is the doc’s inference from the id, not something the bytecode says. Silver Blades areas $31 and $32 issue none either, and their map is loaded by ECL30, the script that sends the party to them – geos is empty there rather than a guess.

And the inference is wrong for both. FastTraveled into, area 3 loaded GEO05 and area 5 loaded GEO04$6E15 and the bytes at $0400 agreeing (write-up lost, work/reports/p20-arrivals.md). So a square chosen off geos[0] is a square off a map the game was never going to show, and a caller with no arrival square should write none for these two and let the arriving script place the party, which is what it does.

Type:

**geos may be incomplete

property ecl: str
fasttravelable: bool = True

Whether a debug fasttravel may enter this area at all. False for ECL1E alone: it is the attract-mode demo, and fasttraveling there ends the session. $C04B-$C04D read 254, 127, 16, no GEO is resident, no status line and no command bar appear, and the program counter never returns to DUNGEON’s key-wait loop, so no later fasttravel can be started (write-up lost, work/reports/p20-arrivals.md).

property geo: str | None

The one map, where there is exactly one. None for none and for two.

Callers that cannot cope with an area having two maps should be reading geos and deciding what to do, not silently taking the first.

geo_names: Mapping[str, str]

Names for individual maps of a two-map area, where the second map is a place in its own right – area 29’s GEO20 is the catacombs under Kuto’s Well, not Kuto’s Well.

geos: tuple[str, ...] = ()

The GEO files the script statically loads, in the order it loads them. Empty for the three mapless Pool of Radiance areas, and for the two Silver Blades areas whose map is loaded by the script that sends the party to them.

property has_map: bool
id: int
property label: str

New Phlan - GEO00, POOL3.

Type:

One line for a dropdown

name: str | None

None for ECL1E, which has no NEWECL pointing at it and no name anybody has been able to attach to it, though it does load GEO12 (#260 (Area 30 is recorded as having no map, and ECL1E loads GEO12)).

name_for(geo)[source]

This area’s name for one of its maps.

Parameters:

geo (str)

Return type:

str | None

property outdoors: bool
overland: tuple[int, int] | None = None

Where a fast travel puts the party on the travel grid, window-local (x, y), for areas 25-27 only – written to $49C3/$49C4. arrival is the GEO square in $C04B, and stays None for these three: $C04B is not GDRIVE00’s square outdoors, so a caller must never write one there for a window. newecl_writes in automap/actions.py writes this field instead (#178 (Fast Travel to the wilderness leaves the party on whatever overland square it last stood on)).

`confidence` below does not grade this field. It is one grade for the row and it was written for the area itself, so a row could read CONFIRMED while its overland was still inferred. All three are now CONFIRMED in the running game – a party arrived on each square and walked off it – but the two gradings remain separate things, and the per-square evidence is in the comment beside the rows. Anything that ever shows a confidence to a user must not read this field’s trustworthiness off confidence; raised in the code review of #178 on 2026-09-02, when nothing displayed it yet and the trap was one dropdown column away.

side_name: str = 'POOL{}'

How this title names a disk side, for label. Pool of Radiance’s sides are POOL1-POOL8 and Silver Blades’ are SILVER-1-SILVER-6, and a dropdown that said POOL3 under a Silver Blades session would be naming a disk the player does not own.

sqrdata: str | None = None

The overland square-data file, for areas 25-27 only.

class goldbox.areas.Arrival[source]

Bases: object

The square the game itself puts the party on when it enters an area.

facing is None where the departing script sets the square but not the direction – area 7 is the one such case.

__init__(x, y, facing=None)
Parameters:
Return type:

None

facing: int | None = None
property facing_letter: str
x: int
y: int
class goldbox.areas.Confidence[source]

Bases: enum.Enum

How much we trust a field definition.

CONFIRMED = 'CONFIRMED'
GUESS = 'GUESS'
PROBABLE = 'PROBABLE'
UNKNOWN = 'UNKNOWN'
goldbox.areas.GEO_NAMES: Mapping[str, Mapping[str, str]] = mappingproxy({'Pool of Radiance': mappingproxy({'GEO00': 'New Phlan', 'GEO01': 'Buccaneer Base', 'GEO02': 'Cadorna Textile House', 'GEO03': 'Valjevo Castle, a Floor', 'GEO04': 'Valjevo Castle, a Floor', 'GEO05': 'Valjevo Castle, a Floor', 'GEO06': 'Valjevo Castle, a Floor', 'GEO07': 'Valjevo Castle, the Pool', 'GEO09': 'Stojanow Gate', 'GEO0A': 'Valhingen Graveyard', 'GEO0D': 'The Kobold Caves', 'GEO0E': 'Kovel Mansion', 'GEO0F': "Mendor's Library", 'GEO10': 'The Lizardman Keep', 'GEO1E': 'The Lizardman Keep', 'GEO11': 'The Nomad Camp', 'GEO12': 'Podol Plaza', 'GEO14': 'The Slums', 'GEO15': 'Sokol Keep', 'GEO16': "Yarash's Pyramid", 'GEO17': "Yarash's Pyramid, Lower", 'GEO18': 'Temple of Bane', 'GEO1F': 'Temple of Bane', 'GEO19': 'Wilderness, West Window', 'GEO1A': 'Wilderness, Middle Window', 'GEO1B': 'Wilderness, East Window', 'GEO1C': 'Zhentil Keep Outpost', 'GEO1D': "Kuto's Well", 'GEO20': "Kuto's Well Catacombs"}), 'Curse of the Azure Bonds': mappingproxy({}), 'Secret of the Silver Blades': mappingproxy({})})

Game title -> map file -> the name to show. Curse and Silver Blades are present and empty: their maps are not named anywhere yet – Silver Blades’ twenty-two areas are decoded down to the map and the disk side and not one of them has a name – and an empty table degrades to "area 21" where a missing title would degrade to the same thing. Listing them is the difference between “we know we do not know” and “we never looked”.

goldbox.areas.MISSING_ID = 12

ECL0C is not on any of the nine disks, so there is no area 12. Kept as a named constant because “the ids are 0-30 with one hole” is a fact about the game that a caller iterating a range would otherwise have to rediscover.

goldbox.areas.POOL_OF_RADIANCE = 'Pool of Radiance'

Game titles this module knows about. Plain strings on purpose, and the seam with goldbox/games.py: these are Game.title, so a caller holding a descriptor writes areas.area_name(geo, game.title) and neither module imports the other. tests/test_areas.py pins the two spellings together.

goldbox.areas.TABLES: Mapping[str, tuple[goldbox.areas.Area, ...]] = mappingproxy({'Pool of Radiance': (Area(id=0, name='New Phlan', disk=3, geos=('GEO00',), sqrdata=None, arrival=Arrival(x=15, y=1, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=1, name='Buccaneer Base', disk=6, geos=('GEO01',), sqrdata=None, arrival=Arrival(x=8, y=0, facing=2), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=2, name='Cadorna Textile House', disk=4, geos=('GEO02',), sqrdata=None, arrival=Arrival(x=0, y=4, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=3, name='Valjevo Castle, a Floor', disk=5, geos=('GEO03',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.PROBABLE: 'PROBABLE'>, geo_names={}, dynamic_geo=True, fasttravelable=True, side_name='POOL{}'), Area(id=4, name='Valjevo Castle, a Floor', disk=5, geos=('GEO04',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.PROBABLE: 'PROBABLE'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=5, name='Valjevo Castle, a Floor', disk=5, geos=('GEO05',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.PROBABLE: 'PROBABLE'>, geo_names={}, dynamic_geo=True, fasttravelable=True, side_name='POOL{}'), Area(id=6, name='Valjevo Castle, a Floor', disk=5, geos=('GEO06',), sqrdata=None, arrival=Arrival(x=4, y=15, facing=0), overland=None, confidence=<Confidence.PROBABLE: 'PROBABLE'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=7, name='Valjevo Castle, the Pool', disk=5, geos=('GEO07',), sqrdata=None, arrival=Arrival(x=5, y=7, facing=None), overland=None, confidence=<Confidence.PROBABLE: 'PROBABLE'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=8, name='Phlan City Hall', disk=3, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=9, name='Stojanow Gate', disk=2, geos=('GEO09',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=10, name='Valhingen Graveyard', disk=4, geos=('GEO0A',), sqrdata=None, arrival=Arrival(x=0, y=4, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=11, name='The Training Hall', disk=3, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=13, name='The Kobold Caves', disk=8, geos=('GEO0D',), sqrdata=None, arrival=Arrival(x=6, y=15, facing=0), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=14, name='Kovel Mansion', disk=3, geos=('GEO0E',), sqrdata=None, arrival=Arrival(x=4, y=0, facing=2), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=15, name="Mendor's Library", disk=2, geos=('GEO0F',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=16, name='The Lizardman Keep', disk=8, geos=('GEO10', 'GEO1E'), sqrdata=None, arrival=Arrival(x=8, y=14, facing=0), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=17, name='The Nomad Camp', disk=7, geos=('GEO11',), sqrdata=None, arrival=Arrival(x=1, y=14, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=18, name='Podol Plaza', disk=1, geos=('GEO12',), sqrdata=None, arrival=Arrival(x=0, y=4, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=19, name='Cave of Diogenes', disk=6, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=20, name='The Slums', disk=2, geos=('GEO14',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=21, name='Sokol Keep', disk=4, geos=('GEO15',), sqrdata=None, arrival=Arrival(x=8, y=14, facing=0), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=22, name="Yarash's Pyramid", disk=7, geos=('GEO16',), sqrdata=None, arrival=Arrival(x=15, y=7, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=23, name="Yarash's Pyramid, Lower", disk=7, geos=('GEO17',), sqrdata=None, arrival=Arrival(x=15, y=0, facing=2), overland=None, confidence=<Confidence.PROBABLE: 'PROBABLE'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=24, name='Temple of Bane', disk=1, geos=('GEO18', 'GEO1F'), sqrdata=None, arrival=Arrival(x=15, y=4, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=25, name='Wilderness, West Window', disk=6, geos=('GEO19',), sqrdata='SQRDATA04', arrival=None, overland=(14, 29), confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=26, name='Wilderness, Middle Window', disk=7, geos=('GEO1A',), sqrdata='SQRDATA05', arrival=None, overland=(7, 29), confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=27, name='Wilderness, East Window', disk=8, geos=('GEO1B',), sqrdata='SQRDATA06', arrival=None, overland=(9, 29), confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=28, name='Zhentil Keep Outpost', disk=6, geos=('GEO1C',), sqrdata=None, arrival=Arrival(x=7, y=0, facing=2), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=29, name="Kuto's Well", disk=8, geos=('GEO1D', 'GEO20'), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names=mappingproxy({'GEO20': "Kuto's Well Catacombs"}), dynamic_geo=False, fasttravelable=True, side_name='POOL{}'), Area(id=30, name=None, disk=1, geos=('GEO12',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=False, side_name='POOL{}')), 'Curse of the Azure Bonds': (Area(id=1, name=None, disk=2, geos=('GEO01',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=2, name=None, disk=2, geos=('GEO01',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=3, name=None, disk=2, geos=('GEO03',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=4, name=None, disk=2, geos=('GEO04',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=16, name=None, disk=3, geos=('GEO10',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=17, name=None, disk=3, geos=('GEO11',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=18, name=None, disk=3, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=21, name=None, disk=3, geos=('GEO15',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=30, name=None, disk=1, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=32, name=None, disk=4, geos=('GEO20',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=33, name=None, disk=4, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=34, name=None, disk=4, geos=('GEO21',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=35, name=None, disk=4, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=37, name=None, disk=4, geos=('GEO25',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=48, name=None, disk=5, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=49, name=None, disk=5, geos=('GEO32',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=50, name=None, disk=5, geos=('GEO32',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=51, name=None, disk=5, geos=('GEO33',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=53, name=None, disk=5, geos=('GEO35',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=64, name=None, disk=6, geos=('GEO40',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=66, name=None, disk=6, geos=('GEO42',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=67, name=None, disk=6, geos=('GEO43',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=69, name=None, disk=6, geos=('GEO45',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=80, name=None, disk=1, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}'), Area(id=81, name=None, disk=1, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.UNKNOWN: 'UNKNOWN'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='CURSE_{}')), 'Secret of the Silver Blades': (Area(id=4, name=None, disk=1, geos=('GEO10',), sqrdata=None, arrival=Arrival(x=10, y=8, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=16, name=None, disk=1, geos=('GEO10',), sqrdata=None, arrival=Arrival(x=15, y=8, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=17, name=None, disk=1, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=32, name=None, disk=2, geos=('GEO20',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=33, name=None, disk=2, geos=('GEO21',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=34, name=None, disk=2, geos=('GEO22',), sqrdata=None, arrival=Arrival(x=14, y=14, facing=0), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=48, name=None, disk=3, geos=('GEO31', 'GEO30'), sqrdata=None, arrival=Arrival(x=3, y=3, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=49, name=None, disk=3, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=True, fasttravelable=True, side_name='SILVER-{}'), Area(id=50, name=None, disk=3, geos=(), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=True, fasttravelable=True, side_name='SILVER-{}'), Area(id=51, name=None, disk=3, geos=('GEO31',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=52, name=None, disk=3, geos=('GEO32',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=64, name=None, disk=4, geos=('GEO40',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=65, name=None, disk=4, geos=('GEO41',), sqrdata=None, arrival=Arrival(x=13, y=9, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=66, name=None, disk=4, geos=('GEO42',), sqrdata=None, arrival=Arrival(x=12, y=13, facing=0), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=68, name=None, disk=4, geos=('GEO44',), sqrdata=None, arrival=Arrival(x=7, y=15, facing=0), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=80, name=None, disk=5, geos=('GEO50',), sqrdata=None, arrival=Arrival(x=1, y=11, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=81, name=None, disk=5, geos=('GEO51',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=82, name=None, disk=5, geos=('GEO52',), sqrdata=None, arrival=None, overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=96, name=None, disk=6, geos=('GEO60',), sqrdata=None, arrival=Arrival(x=15, y=0, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=97, name=None, disk=6, geos=('GEO61',), sqrdata=None, arrival=Arrival(x=15, y=0, facing=3), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=98, name=None, disk=6, geos=('GEO62',), sqrdata=None, arrival=Arrival(x=0, y=15, facing=1), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'), Area(id=99, name=None, disk=6, geos=('GEO62',), sqrdata=None, arrival=Arrival(x=0, y=0, facing=2), overland=None, confidence=<Confidence.CONFIRMED: 'CONFIRMED'>, geo_names={}, dynamic_geo=False, fasttravelable=True, side_name='SILVER-{}'))})

Game title -> that title’s areas.

goldbox.areas.area(id)[source]

The area with this id in Pool of Radiance, or None – id 12 has no script.

Parameters:

id (int)

Return type:

goldbox.areas.Area | None

goldbox.areas.area_in(id, title)[source]

The area with this id in this title, or None.

area above is Pool of Radiance’s, and an id means a different place in each game: $21 is Sokol Keep there and a Silver Blades area on side 2 here. A caller holding a title must ask this one.

Parameters:
Return type:

goldbox.areas.Area | None

goldbox.areas.area_name(geo, title=POOL_OF_RADIANCE)[source]

A name for a map that is never a lie about another game.

GEO15 is Sokol Keep in Pool of Radiance and somewhere else entirely in Curse of the Azure Bonds. With no title, or a title with no table, this falls back to the file’s own number – "area 21" – which says exactly as much as we know.

Parameters:
  • geo (str)

  • title (str | None)

Return type:

str

goldbox.areas.areas_for(title)[source]

Every area of this title that anybody has written down.

This is the knowledge, not the permission. It answers with Silver Blades’ twenty-two rows; areas_for_title below is the one a fast travel asks, and it still refuses them. Use this for labelling a map, for a report, or for anything that only reads.

Parameters:

title (str | None)

Return type:

tuple[goldbox.areas.Area, …]

goldbox.areas.areas_for_geo(geo)[source]

Every area that loads this map. Usually one; never assume it.

Parameters:

geo (str)

Return type:

tuple[goldbox.areas.Area, …]

goldbox.areas.areas_for_title(title)[source]

Every area a fast travel may offer, which is nothing for four of six.

`AREAS` is Pool of Radiance’s and only Pool of Radiance’s. Every row carries a POOL disk number and an ECL id, and neither means anything in another title – Curse’s disks are not numbered like Pool of Radiance’s and its ECL ids have never been decoded (docs/138-multiple-games.md §§2, 6). A caller that is about to write one of those numbers into a running machine must ask this rather than reading AREAS, and must offer nothing when it comes back empty. Falling back to Pool of Radiance’s list is the one answer that corrupts.

Silver Blades is offered now, and it was not before. Two things had to be true and both are. #15 (Fast Travel for more than one Gold Box title) moved the addresses off Pool of Radiance’s, so automap/actions.py writes $7F12, $7F1B, $4BF2, the wipe at $4C00, the sixth write at $4BFB and the tail $210C when the title is this one. And a party has been fast-travelled into fourteen of these twenty-two areas on a running machine, each landing checked by an exact 1024-byte compare of $0400 against the map on the player’s own disk, 15 hops and 15 matches (#20 (Build an area table for Silver Blades)). Offering rows nobody had driven was what the PROBABLE grade on the whole table was warning about, and that is what stopped being true.

Curse is offered too, now that its table exists. #192 (Convert a Curse of the Azure Bonds DOS save into a C64 one, which the importer refuses today) step 0b built AREAS_CURSE off the bytecode, and automap/fasttravel.py’s addresses for it were already CONFIRMED by four driven warps (#19) before this table existed – so both of automap.actions.area_rows’s gates are open. Unlike Silver Blades, no individual Curse row has been landed on by a driven fast travel: every row’s confidence is UNKNOWN, and a caller reading that field before trusting a row – which nothing here does yet – would need to know that. areas_for above is the accessor for everything that only reads, and answers for every title that has a table at all.

Parameters:

title (str | None)

Return type:

tuple[goldbox.areas.Area, …]

goldbox.areas.components(geo)[source]

The map’s squares grouped by what can walk to what.

A GEO is not one room. GEO19 breaks into 42 pieces, GEO0D into 21, and a square picked without regard to which piece it is in can be a place the party cannot walk out of.

Return type:

list[set[tuple[int, int]]]

goldbox.areas.geo_name(geo, title=POOL_OF_RADIANCE)[source]

The place this map is, in this game – or None if we cannot say.

Parameters:
  • geo (str)

  • title (str | None)

Return type:

str | None

goldbox.areas.geo_number(geo)[source]

GEO15 -> 21. The digits are hex, which is why GEO0A exists.

Parameters:

geo (str)

Return type:

int | None

goldbox.areas.geos_for_area(id)[source]

The maps this area loads: two, one, or none.

Parameters:

id (int)

Return type:

tuple[str, …]

goldbox.areas.geos_in(title)[source]

Every map any area of this title loads.

The set a GEO number read out of a saved game is checked against, so that a number nothing could have loaded is refused rather than written into a converted save. It is the union of the rows and not a range: GEO0C is in no row (areas_for_geo("GEO0C") is empty) and 12 is inside any range anybody would write.

The set can be short of what a title’s disks hold, because two Pool of Radiance rows are dynamic_geo – their script chooses its map at run time and their geos entry is the doc’s inference from the id, which is known to be wrong for both. If this ever refuses a save the game itself wrote, the row is what is incomplete.

Parameters:

title (str | None)

Return type:

frozenset[str]

goldbox.areas.landing_square(geo)[source]

Where to drop a party on a map that has no arrival square of its own.

What `FastTravel` uses, in place of the old rule – the first square with any passable edge at all, which therefore took (0, 0) on every one of the twenty-nine maps (write-up lost, work/reports/p20-arrivals.md; the pocket sizes are asserted in tests/test_p20.py’s POCKETS). That was legal in the narrow sense on most maps and wrong on four: (0, 0) is in a pocket of 32 squares in GEO05, 30 in GEO19, 16 in GEO1A and 48 in GEO1B, cut off from the rest of the map, and a party fasttraveled there can walk but cannot get out.

So: the largest connected component, and within it a square off the outer ring where one exists, facing a passable edge. Returns None for a map with no passable edge anywhere, which is also what an area with no map gets.

Two kinds of area the caller must not call this for at all, because the answer would be meaningless rather than merely imperfect: the three overland areas, where the position is $49C3/$49C4, not a GEO square – Area.overland carries it and FastTravel writes it directly (#178 (Fast Travel to the wilderness leaves the party on whatever overland square it last stood on); this used to say every arriving script writes [$4A18]/[$4A19], “the world-map cell” – those bytes are scratch, and the only writers copy an indoor square into $C04B on the way into a window’s own cave, corrected while fixing #178); and the two `dynamic_geo` areas, which load a map geos does not name.

Return type:

tuple[int, int, int] | None