A walkable Mandalay Bay that mounts the terminal’s own screens
Published
July 25, 2026
Abstract
The pixel RPG is the third playable surface of The Mandalay Bay: a Phaser 3 top-down overworld of twenty-eight authored rooms, laid over the exact same chip wallet, save slot, and rule set as the Python CLI and the web terminal. This page is generated from the live world data in docs/rpg/js/data/ — every table below is read from the JSON the game itself loads at boot.
Keywords
Phaser, pixel RPG, overworld, quest design, Mandalay Bay
Build a guest — archetype, skin tone, hair, outfit — arrive on Las Vegas Blvd, and walk in through the gold doors. From there the resort is a place rather than a menu tree: you cross the casino floor to reach the sports book, ride the tower up to a room that charges you nightly, and get stopped by staff who spotted you from across the carpet.
The reference feel is Pokémon — a top-down overworld, a START menu that holds your whole life, line-of-sight challengers, a collection dex, and secrets that pay in flavor rather than chips.
Tiles, props, and guest sprites are drawn at boot by TextureFactory.js (no vendored assets/tiles/ loader on the live path). Characters are 32×44 tuxedo-style pixel grids with a three-frame stride; the wardrobe recolors skin, hair, and outfit.
Code
pd.DataFrame( [ {"Content": "Rooms", "Count": len(MAPS)}, {"Content": "NPCs", "Count": sum(len(v) for v in NPCS.values())}, {"Content": "Doors", "Count": sum(len(m.get("doors", [])) for m in MAPS.values())}, {"Content": "Dialogue nodes", "Count": len(DIALOGUES)}, {"Content": "Quests", "Count": len(QUESTS)}, {"Content": "Easter eggs", "Count": len(EGGS)}, {"Content": "Zone triggers", "Count": len(TRIGGERS)}, ])
Table 1. World size at render time, read from the authored JSON
Content
Count
0
Rooms
33
1
NPCs
78
2
Doors
76
3
Dialogue nodes
316
4
Quests
10
5
Easter eggs
22
6
Zone triggers
28
2 The delegation rule
The RPG does not reimplement game logic or game screens. Every casino, hotel, pool, and shopping flow lives once in docs/js/ as a buildXRenderers(ctx) factory. The web terminal spreads those factories into its renderer table; the RPG’s TerminalHostOverlay builds the same context and mounts them inside an encounter panel. A feature shipped to the terminal shows up in the RPG with no RPG-side work.
Code
flowchart LR LOGIC["docs/js/ — rules, wallet, world clock"] --> UI["docs/js/ui/ — buildXRenderers(ctx)"] UI --> APP["app.js — web terminal"] UI --> HOST["TerminalHostOverlay — RPG"] DATA["docs/rpg/js/data/*.json"] --> OW["OverworldScene"] OW -->|encounter id| HOST OW -->|Esc / X| MENU["MenuOverlay — START"] MENU --> HOST
flowchart LR
LOGIC["docs/js/ — rules, wallet, world clock"] --> UI["docs/js/ui/ — buildXRenderers(ctx)"]
UI --> APP["app.js — web terminal"]
UI --> HOST["TerminalHostOverlay — RPG"]
DATA["docs/rpg/js/data/*.json"] --> OW["OverworldScene"]
OW -->|encounter id| HOST
OW -->|Esc / X| MENU["MenuOverlay — START"]
MENU --> HOST
The four exceptions are the screens that read better in-world: blackjack, hold’em, roulette, and the House of Blues rhythm minigame. Those are the battle screens, and they still take their bet through the shared stake-tier picker.
3 The property
Rooms are authored as declarative JSON — a base fill, ground rectangles, decor, a deterministic scatter rule for greenery, and explicit clears that keep doorways open — then compiled into tile layers by MapLoader.compileMap() at boot.
summary = world.groupby("Wing", observed=True).agg( Rooms=("Room", "count"), NPCs=("NPCs", "sum"))fig, ax = plt.subplots(figsize=(8.4, 4.0))x =range(len(summary))ax.bar([i -0.2for i in x], summary["Rooms"], width=0.4, label="Rooms", color="#C5050C", edgecolor="#7a0307")ax.bar([i +0.2for i in x], summary["NPCs"], width=0.4, label="NPCs", color="#f0b429", edgecolor="#a8781a")ax.set_xticks(list(x))ax.set_xticklabels(summary.index, rotation=20, ha="right")ax.set_ylabel("Count")ax.spines["top"].set_visible(False)ax.spines["right"].set_visible(False)ax.legend(frameon=False)fig.tight_layout()plt.show()
Figure 1. Where the world’s rooms and residents sit
Code
world
Table 3. Every room, its residents, and its exits
Wing
Room
Map id
NPCs
Exits
Spawn
0
Arrival
Registration Lobby
registration_lobby
4
6
(15, 26)
1
Arrival
Valet & Parking
valet_garage
3
2
(26, 15)
2
Casino
Casino Floor North
main_resort
9
6
(15, 26)
3
Casino
Casino Floor South
casino_floor_south
4
5
(15, 3)
4
Casino
Foundation Room
foundation_room
3
1
(15, 26)
5
Casino
High Limit Salon
high_limit_salon
3
2
(15, 26)
6
Casino
Race & Sports Book
race_sports_book
2
1
(26, 15)
7
Retail
Convention Center
convention_center
2
2
(15, 26)
8
Retail
Sky Bridge
sky_bridge
1
3
(15, 26)
9
Retail
The Shoppes at Mandalay Place
mandalay_place
3
2
(15, 26)
10
Bars
Betty's Bar
betty_bar
2
1
(15, 26)
11
Bars
Skyfall Lounge
skyfall_lounge
1
1
(15, 26)
12
Hotel
Bathhouse Spa
spa
1
1
(15, 3)
13
Hotel
Delano Wing
delano_wing
1
3
(26, 15)
14
Hotel
Gentleman's Club — Velvet Ledger
gentlemans_club
3
1
(15, 26)
15
Hotel
Guest Floor Corridor
guest_corridor
4
4
(15, 26)
16
Hotel
Tower Elevator Lobby
hotel_tower
2
6
(15, 26)
17
Hotel
Your Room
guest_room
2
1
(15, 26)
18
Pool
Cabanas & Hot Tubs
cabana_row
2
1
(3, 15)
19
Pool
Mandalay Beach
mandalay_beach
2
4
(15, 3)
20
Pool
Moonlight Rave Stage
rave_stage
1
1
(15, 26)
21
Pool
Moorea Beach Club
beach_club
1
2
(15, 3)
22
Attractions
HOB Green Room
hob_green_room
1
1
(26, 15)
23
Attractions
House of Blues
house_of_blues
2
2
(15, 26)
24
Attractions
Shark Reef Exhibit Hall
shark_reef
2
1
(26, 15)
25
Attractions
Shark Reef Tunnel
reef_tunnel
1
2
(26, 15)
26
Attractions
ULTRA Arena Concourse
ultra_arena
2
2
(15, 26)
27
Back of house
Back of House
staff_corridor
2
1
(15, 26)
28
NaN
Excalibur Courtyard
excalibur_courtyard
2
1
(26, 15)
29
NaN
Las Vegas Blvd — Excalibur
strip_excalibur
2
2
(15, 3)
30
NaN
Las Vegas Blvd — Luxor
strip_luxor
3
3
(15, 3)
31
NaN
Las Vegas Blvd — Mandalay
strip_sidewalk
3
3
(15, 22)
32
NaN
Luxor Atrium
luxor_atrium
2
2
(26, 15)
Two doors are gated at the rope: the High Limit Salon checks chips and stake tier through docs/js/venues.js, and the Foundation Room wants Noir standing. Your own room door stops working while the folio is unpaid.
Code
gates = []for mid, m in MAPS.items():for d in m.get("doors", []): conditions = []if d.get("venueGate"): conditions.append(f"venue gate: {d['venueGate']}")if d.get("requiresChips"): conditions.append(f"{d['requiresChips']:,} chips")if d.get("requiresFlag"): conditions.append(f"flag: {d['requiresFlag']}")if d.get("requiresRoomKey"): conditions.append("a working room key")if conditions: gates.append( {"From": MAPS[mid].get("label", mid),"To": MAPS.get(d["to"], {}).get("label", d["to"]),"Requires": ", ".join(conditions), } )pd.DataFrame(gates)
Table 4. Gated doors and what they ask for
From
To
Requires
0
Registration Lobby
Your Room
a working room key
1
Casino Floor North
High Limit Salon
venue gate: high_limit_salon
2
Casino Floor North
Back of House
flag: hint_north_wall
3
High Limit Salon
Foundation Room
venue gate: foundation_room
4
Tower Elevator Lobby
Your Room
a working room key
5
Tower Elevator Lobby
Gentleman's Club — Velvet Ledger
venue gate: gentlemans_club
6
Guest Floor Corridor
Your Room
a working room key
7
House of Blues
HOB Green Room
flag: hob_backstage
4 Trainers
An NPC with a sight cone notices you walking into it, crosses the room, says its piece, and drops straight into its encounter. Each one challenges you once.
Staff also keep schedules. NPCs carrying a schedule entry move with the world clock’s day phase rather than standing in one spot forever:
Code
scheduled = [ {"Who": npc["name"],"Where": MAPS[mid].get("label", mid),"Phases": ", ".join(sorted(npc["schedule"])), }for mid, roster in NPCS.items()for npc in rosterif npc.get("schedule")]pd.DataFrame(scheduled).sort_values("Where").reset_index(drop=True)
Table 6. NPCs who move with the clock
Who
Where
Phases
0
Barkeep Betty
Betty's Bar
dawn
1
Spinster Sal
Casino Floor North
dawn
2
Security Sam
Casino Floor North
dawn, dusk, late, midday
3
Cocktail Cora
Casino Floor South
dawn
4
Housekeeper Hana
Guest Floor Corridor
late
5
Cab Line Carl
Las Vegas Blvd — Mandalay
late
6
Lifeguard Lou
Mandalay Beach
late
7
Bookie Blake
Race & Sports Book
late
8
Tourist Tina
Registration Lobby
dawn, dusk, late
5 Quests
Quest progress is derived, never incremented. QuestManager.syncDerived() reads reef photos, bar orders, dex counts, purchases, unlocked vignettes, egg count, and resort completion straight from shared session state, so a quest can never disagree with the system that produced it. Work you did before accepting a quest still counts — the derived value is banked and applied when you take the job.
The START menu carries three collections, all counting toward resort completion.
Collection
Entries
Filled by
Dex — Shark Reef
5 species
Photographing the reef
Dex — Slot floor
14 machines
Playing each machine once
Dex — Staff met
Every dealer and resort NPC
Talking to people
Bag
Quest items, mall purchases, minibar tabs
Playing
Secrets
12 easter eggs
Poking at things
Easter eggs are cosmetic only — an egg never pays chips. That is a hard design rule, and the world-data checker enforces that every egg flag is actually reachable from something in the world.
Code
pd.DataFrame( [{"Secret": e["label"], "Hint": e["hint"]} for e in EGGS.values()])
Table 8. The secrets board — hints only, no reveals
Secret
Hint
0
Cherry on top
Somewhere a machine still pays in fruit.
1
Retro palette
Up up down down… you know the rest.
2
Back of house
A wall that isn't quite a wall.
3
The talking plant
Stand still near the lobby greenery.
4
Statue etiquette
Everyone touches the golden statue. Not everyo...
5
Lock of the week
Ask the bookie what he actually likes.
6
The floor that isn't
Elevators skip a number for a reason.
7
555-0199
Dial the number nobody answers.
8
Do not tap the glass
The sign says do not. The sign is a suggestion.
9
The old bell
Some jackpots still ring analog.
10
Delano ghost floor
The quiet tower keeps quieter guests.
11
Green room list
Backstage has a guest list. You are not on it.
12
Velvet guest list
Ask the floor hostess who still owes on Row F.
13
Off-menu pour
The bottle captain keeps a pour that isn't pri...
14
Velvet back hall
Make it rain, then ask security about the serv...
15
Monsoon receipt
Make it monsoon more than once.
16
The ledger itself
Tip enough that the room starts keeping score.
17
Louis toast
Order the bottle that requires a spine.
18
Bar tab mosaic
Keep ordering until the tab looks like art.
19
Perfect cascade
Stop Tip Cascade dead-center in the green.
20
Triple Ace
Nail Bottle Memory when Blair stacks Ace three...
21
Felt ace
Win Dante's Felt Flip on an ace.
7 Time and money
docs/js/world-cycle.js is the single clock for all three surfaces. Two real hours make one resort day, split into four phases. In the overworld that clock tints the screen, walks NPCs to their scheduled positions, announces the day’s rotating reservation requirement, posts daily resort charges to your wallet, and — if the folio goes unpaid — evicts you and stops the room door from opening until you settle at the desk or win it back on the floor.
8 Controls
Input
Action
WASD / arrows
Walk
Tap / click a tile
Walk there — the resort is playable on a phone
Shift
Run — faster once a host comps the golf cart at Platinum
E / Enter / Space
Talk to whoever you are facing, advance dialogue
Esc / X
START menu
T
Trainer Card and wardrobe
P
MGM Rewards phone
↑↑↓↓←→←→BA
Retro palette
On a phone the canvas fills the screen and a thumb pad appears in the bottom corners — a d-pad, B to run, A to talk, ☰ for the START menu — and it hides itself whenever a conversation or a panel takes over. You can also ignore the pad completely: tap a tile to walk there, tap a person to walk over and talk, and tap anywhere to advance dialogue.
Gold walkways are the wayfinding: they connect the entrance, the pits, the aisles, and every door. Dark trim marks where one floor type ends, and floating signs name the zone you are standing in.
9 Saves
The RPG writes into the same slot as the terminal and the CLI. SAVE_VERSION is 8; rpg carries position, archetype, flags, quests, inventory, dex, eggs, map visits, options, and reputation. A v7 save migrates forward without renaming a key and keeps the map it was saved on. On the Python side, mandalay_bay/saves.py carries the web-only keys through a CLI load/save round trip untouched, so playing in the terminal never erases pixel progress.
10 Extending it
Full detail lives in the Pixel RPG GDD; the short version:
To add…
Do this
A room
Add a record to scripts/_author_maps.py, regenerate, wire doors both ways
An NPC
Add to the NPCS table plus a *_greet node in dialogues.json
A quest
Add to quests.json, derive its progress in QuestManager.syncDerived()
A casino screen
Build it in docs/js/ui/ and route it from HostedEncounters.js — never twice
---title: "The Pixel RPG"subtitle: "A walkable Mandalay Bay that mounts the terminal's own screens"date: last-modifiedabstract: | The pixel RPG is the third playable surface of **The Mandalay Bay**: a Phaser 3 top-down overworld of twenty-eight authored rooms, laid over the exact same chip wallet, save slot, and rule set as the Python CLI and the web terminal. This page is generated from the live world data in `docs/rpg/js/data/` — every table below is read from the JSON the game itself loads at boot.keywords: - Phaser - pixel RPG - overworld - quest design - Mandalay Bayexecute: # Every table here is read from js/data/, which changes without this file # changing. Freezing would publish last week's world. freeze: false---::: {.github-access}<a class="github-btn" href="https://exios66.github.io/degen-llms/rpg/"><i class="bi bi-controller"></i> Launch the RPG</a><a class="github-btn" href="https://github.com/Exios66/degen-llms/tree/main/docs/rpg"><i class="bi bi-github"></i> Source</a><a class="github-btn" href="docs/rpg/GDD.md"><i class="bi bi-journal-code"></i> Design document</a>:::```{python}#| label: setup#| include: falsefrom __future__ import annotationsimport jsonfrom pathlib import Pathimport matplotlib.pyplot as pltimport pandas as pdROOT = Path.cwd()DATA = ROOT /"docs"/"rpg"/"js"/"data"def load(rel: str):return json.loads((DATA / rel).read_text())MAP_IDS = load("maps/index.json")["maps"]MAPS = {mid: load(f"maps/{mid}.json") for mid in MAP_IDS}NPCS = load("npcs.json")DIALOGUES = load("dialogues.json")QUESTS = load("quests.json")EGGS = load("easter_eggs.json")TRIGGERS = load("triggers.json")WING_ORDER = ["Arrival", "Casino", "Retail", "Bars", "Hotel", "Pool", "Attractions", "Back of house",]```# What it isBuild a guest — archetype, skin tone, hair, outfit — arrive on Las Vegas Blvd,and walk in through the gold doors.From there the resort is a place rather than a menu tree: you cross the casinofloor to reach the sports book, ride the tower up to a room that charges younightly, and get stopped by staff who spotted you from across the carpet.The reference feel is Pokémon — a top-down overworld, a START menu that holdsyour whole life, line-of-sight challengers, a collection dex, and secrets thatpay in flavor rather than chips.Tiles, props, and guest sprites are drawn at boot by **`TextureFactory.js`**(no vendored `assets/tiles/` loader on the live path). Characters are 32×44tuxedo-style pixel grids with a three-frame stride; the wardrobe recolors skin,hair, and outfit.```{python}#| label: tbl-scale#| tbl-cap: "World size at render time, read from the authored JSON"pd.DataFrame( [ {"Content": "Rooms", "Count": len(MAPS)}, {"Content": "NPCs", "Count": sum(len(v) for v in NPCS.values())}, {"Content": "Doors", "Count": sum(len(m.get("doors", [])) for m in MAPS.values())}, {"Content": "Dialogue nodes", "Count": len(DIALOGUES)}, {"Content": "Quests", "Count": len(QUESTS)}, {"Content": "Easter eggs", "Count": len(EGGS)}, {"Content": "Zone triggers", "Count": len(TRIGGERS)}, ])```# The delegation ruleThe RPG **does not reimplement game logic or game screens.** Every casino,hotel, pool, and shopping flow lives once in `docs/js/` as a`buildXRenderers(ctx)` factory. The web terminal spreads those factories intoits renderer table; the RPG's `TerminalHostOverlay` builds the same context andmounts them inside an encounter panel. A feature shipped to the terminal showsup in the RPG with no RPG-side work.```{mermaid}flowchart LR LOGIC["docs/js/ — rules, wallet, world clock"] --> UI["docs/js/ui/ — buildXRenderers(ctx)"] UI --> APP["app.js — web terminal"] UI --> HOST["TerminalHostOverlay — RPG"] DATA["docs/rpg/js/data/*.json"] --> OW["OverworldScene"] OW -->|encounter id| HOST OW -->|Esc / X| MENU["MenuOverlay — START"] MENU --> HOST```The four exceptions are the screens that read better in-world: blackjack,hold'em, roulette, and the House of Blues rhythm minigame. Those are the battlescreens, and they still take their bet through the shared stake-tier picker.# The propertyRooms are authored as declarative JSON — a base fill, ground rectangles, decor,a deterministic scatter rule for greenery, and explicit clears that keepdoorways open — then compiled into tile layers by `MapLoader.compileMap()` atboot.```{python}#| label: tbl-wings#| tbl-cap: "Rooms, residents, and exits per wing"rows = []for mid, m in MAPS.items(): rows.append( {"Wing": m.get("wing", "Unsorted"),"Room": m.get("label", mid),"Map id": mid,"NPCs": len(NPCS.get(mid, [])),"Exits": len(m.get("doors", [])),"Spawn": f"({m['spawn']['x']}, {m['spawn']['y']})", } )world = pd.DataFrame(rows)world["Wing"] = pd.Categorical(world["Wing"], categories=WING_ORDER, ordered=True)world = world.sort_values(["Wing", "Room"]).reset_index(drop=True)world.groupby("Wing", observed=True).agg( Rooms=("Room", "count"), NPCs=("NPCs", "sum"), Exits=("Exits", "sum")).reset_index()``````{python}#| label: fig-wing-mix#| fig-cap: "Where the world's rooms and residents sit"summary = world.groupby("Wing", observed=True).agg( Rooms=("Room", "count"), NPCs=("NPCs", "sum"))fig, ax = plt.subplots(figsize=(8.4, 4.0))x =range(len(summary))ax.bar([i -0.2for i in x], summary["Rooms"], width=0.4, label="Rooms", color="#C5050C", edgecolor="#7a0307")ax.bar([i +0.2for i in x], summary["NPCs"], width=0.4, label="NPCs", color="#f0b429", edgecolor="#a8781a")ax.set_xticks(list(x))ax.set_xticklabels(summary.index, rotation=20, ha="right")ax.set_ylabel("Count")ax.spines["top"].set_visible(False)ax.spines["right"].set_visible(False)ax.legend(frameon=False)fig.tight_layout()plt.show()``````{python}#| label: tbl-rooms#| tbl-cap: "Every room, its residents, and its exits"world```Two doors are gated at the rope: the High Limit Salon checks chips and staketier through `docs/js/venues.js`, and the Foundation Room wants Noir standing.Your own room door stops working while the folio is unpaid.```{python}#| label: tbl-gated#| tbl-cap: "Gated doors and what they ask for"gates = []for mid, m in MAPS.items():for d in m.get("doors", []): conditions = []if d.get("venueGate"): conditions.append(f"venue gate: {d['venueGate']}")if d.get("requiresChips"): conditions.append(f"{d['requiresChips']:,} chips")if d.get("requiresFlag"): conditions.append(f"flag: {d['requiresFlag']}")if d.get("requiresRoomKey"): conditions.append("a working room key")if conditions: gates.append( {"From": MAPS[mid].get("label", mid),"To": MAPS.get(d["to"], {}).get("label", d["to"]),"Requires": ", ".join(conditions), } )pd.DataFrame(gates)```# TrainersAn NPC with a `sight` cone notices you walking into it, crosses the room, saysits piece, and drops straight into its encounter. Each one challenges you once.```{python}#| label: tbl-trainers#| tbl-cap: "Line-of-sight challengers"trainers = [ {"Who": npc["name"],"Where": MAPS[mid].get("label", mid),"Watches": npc["sight"].get("dir", npc.get("direction", "down")),"Range": npc["sight"].get("range", 4),"Encounter": npc.get("encounter", "conversation"), }for mid, roster in NPCS.items()for npc in rosterif npc.get("sight")]pd.DataFrame(trainers).sort_values("Where").reset_index(drop=True)```Staff also keep schedules. NPCs carrying a `schedule` entry move with the worldclock's day phase rather than standing in one spot forever:```{python}#| label: tbl-schedules#| tbl-cap: "NPCs who move with the clock"scheduled = [ {"Who": npc["name"],"Where": MAPS[mid].get("label", mid),"Phases": ", ".join(sorted(npc["schedule"])), }for mid, roster in NPCS.items()for npc in rosterif npc.get("schedule")]pd.DataFrame(scheduled).sort_values("Where").reset_index(drop=True)```# QuestsQuest progress is *derived*, never incremented. `QuestManager.syncDerived()`reads reef photos, bar orders, dex counts, purchases, unlocked vignettes, eggcount, and resort completion straight from shared session state, so a questcan never disagree with the system that produced it. Work you did beforeaccepting a quest still counts — the derived value is banked and applied whenyou take the job.```{python}#| label: tbl-quests#| tbl-cap: "The quest board from `js/data/quests.json`"pd.DataFrame( [ {"Quest": q["label"],"Given by": q.get("giverName", q.get("giver", "—")),"Category": q.get("category", "—"),"Goal": q.get("target", 1),"Reward": q.get("reward", "—"), }for q in QUESTS.values() ])```# CollectionsThe START menu carries three collections, all counting toward resortcompletion.| Collection | Entries | Filled by ||------------|---------|-----------|| **Dex — Shark Reef** | 5 species | Photographing the reef || **Dex — Slot floor** | 14 machines | Playing each machine once || **Dex — Staff met** | Every dealer and resort NPC | Talking to people || **Bag** | Quest items, mall purchases, minibar tabs | Playing || **Secrets** | 12 easter eggs | Poking at things |Easter eggs are **cosmetic only** — an egg never pays chips. That is a harddesign rule, and the world-data checker enforces that every egg flag isactually reachable from something in the world.```{python}#| label: tbl-eggs#| tbl-cap: "The secrets board — hints only, no reveals"pd.DataFrame( [{"Secret": e["label"], "Hint": e["hint"]} for e in EGGS.values()])```# Time and money`docs/js/world-cycle.js` is the single clock for all three surfaces. Two realhours make one resort day, split into four phases. In the overworld that clocktints the screen, walks NPCs to their scheduled positions, announces the day'srotating reservation requirement, posts daily resort charges to your wallet,and — if the folio goes unpaid — evicts you and stops the room door fromopening until you settle at the desk or win it back on the floor.# Controls| Input | Action ||-------|--------|| WASD / arrows | Walk || Tap / click a tile | Walk there — the resort is playable on a phone || Shift | Run — faster once a host comps the golf cart at Platinum || E / Enter / Space | Talk to whoever you are facing, advance dialogue || Esc / X | START menu || T | Trainer Card and wardrobe || P | MGM Rewards phone || ↑↑↓↓←→←→BA | Retro palette |On a phone the canvas fills the screen and a thumb pad appears in the bottomcorners — a d-pad, **B** to run, **A** to talk, **☰** for the START menu — andit hides itself whenever a conversation or a panel takes over. You can alsoignore the pad completely: tap a tile to walk there, tap a person to walk overand talk, and tap anywhere to advance dialogue.Gold walkways are the wayfinding: they connect the entrance, the pits, theaisles, and every door. Dark trim marks where one floor type ends, and floatingsigns name the zone you are standing in.# SavesThe RPG writes into the same slot as the terminal and the CLI. `SAVE_VERSION`is **8**; `rpg` carries position, archetype, flags, quests, inventory, dex,eggs, map visits, options, and reputation. A v7 save migrates forward withoutrenaming a key and keeps the map it was saved on. On the Python side,`mandalay_bay/saves.py` carries the web-only keys through a CLI load/save roundtrip untouched, so playing in the terminal never erases pixel progress.# Extending itFull detail lives in the [Pixel RPG GDD](docs/rpg/GDD.md); the short version:| To add… | Do this ||---------|---------|| A room | Add a record to `scripts/_author_maps.py`, regenerate, wire doors both ways || An NPC | Add to the `NPCS` table plus a `*_greet` node in `dialogues.json`|| A quest | Add to `quests.json`, derive its progress in `QuestManager.syncDerived()`|| A casino screen | Build it in `docs/js/ui/` and route it from `HostedEncounters.js` — never twice |Then run the checks:```bashnode scripts/smoke-test-rpg.mjs # world-data referential integritypython3 scripts/smoke-test-web.py # browser walk + end-to-end journeypython3-m pytest # Python rules```