thangvip commited on
Commit
3f24f4e
·
verified ·
1 Parent(s): feaf865

Initial submission of Analog Town to Build Small Hackathon

Browse files
.gitattributes CHANGED
@@ -51,3 +51,6 @@ port_whisper_map.png filter=lfs diff=lfs merge=lfs -text
51
  rustwood_map.png filter=lfs diff=lfs merge=lfs -text
52
  static_ambient.wav filter=lfs diff=lfs merge=lfs -text
53
  town_map.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
51
  rustwood_map.png filter=lfs diff=lfs merge=lfs -text
52
  static_ambient.wav filter=lfs diff=lfs merge=lfs -text
53
  town_map.png filter=lfs diff=lfs merge=lfs -text
54
+ lotus_wharf_map.png filter=lfs diff=lfs merge=lfs -text
55
+ tin_lantern_junction_map.png filter=lfs diff=lfs merge=lfs -text
56
+ verdant_spire_map.png filter=lfs diff=lfs merge=lfs -text
SUBMISSION_BLOG.md CHANGED
@@ -1,42 +1,143 @@
1
- # 📻 Analog Town: Intercepting Fictional Minds over Shortwave Radio
2
- ### A Whimsical Submission to the Hugging Face Build Small Hackathon (Whimsical Track)
3
 
4
- > **Live Space**: [https://huggingface.co/spaces/build-small-hackathon/analog-town](https://huggingface.co/spaces/build-small-hackathon/analog-town)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  ---
7
 
8
- ## 🎙️ The Concept: A Social Weather Station for Fictional Worlds
 
 
 
 
9
 
10
- When writers, RPG game masters, or community planners want to see how a group of diverse people will react to a sudden event (like a new factory opening or a sacred grove being bisected by a railway), they usually have to manually simulate their perspectives or ask large models to roleplay in a chaotic chat environment.
11
 
12
- **Analog Town** is a different kind of tool. It is a whimsical "shortwave radio" interface that models fictional minds as signal nodes. Instead of open-ended conversational roleplay, the application runs a **typed state transition** using a small language model.
13
 
14
- But the real magic lies in how you interact with the simulation: you tune an analog radio dial to different frequencies to intercept different minds through the background static noise.
 
 
15
 
16
  ---
17
 
18
- ## 🎨 Retro-Futuristic Aesthetics & Audio Design
19
 
20
- To capture the feeling of tuning into a mysterious radio station in an isolated town, we crafted a customized retro police-scanner interface:
21
- - **CRT Terminal Styling**: Built using a custom CSS theme in theme.py featuring amber and green text on pitch-black cards, scanning line overlays, and glowing signal strength bars.
22
- - **Ambient Radio Static**: Integrated a loopable 15-second analog static and low-hum audio file (static_ambient.wav).
23
- - **Dynamic Fading Manager**: Written in client-side Javascript, our audio manager automatically monitors the receiver's signal strength. When you tune close to an agent's frequency, the static volume smoothly fades down to zero; when you scan between stations, the crackle and hiss swell back up to your volume slider's setting.
24
 
25
  ---
26
 
27
- ## 🗺️ 2D Isometric Pixel-Art Map & Character Dossiers
28
 
29
- We designed six distinct town presets, each representing a unique narrative conflict (from a polar station anomaly to a coastal wind farm debate).
30
- - **Isometric Town Maps**: Custom 2D pixel-art map backgrounds for each town.
31
- - **Pixel-Art Sprite Tokens**: Characters are placed on the map using circular pixel-art tokens representing their age/gender/occupation (e.g. `elder_man.png`, `young_woman.png`, `soldier_man.png`).
32
- - **Interactive Pins**: Clicking on any character token dynamically updates the radio receiver to their frequency, loads their emotional dossier, and prints their latest intercepted thought stream.
33
 
34
  ---
35
 
36
- ## Technical Architecture: How We Kept it "Small"
 
 
 
 
37
 
38
- To comply with the **Build Small Hackathon** constraints, we engineered the app to be highly resource-efficient:
39
- 1. **Model Parameter Constraint (<32B)**: Powered by the Hugging Face Inference API client, we use `Qwen/Qwen2.5-7B-Instruct` as our primary model, with an automatic fallback to `Qwen/Qwen2.5-14B-Instruct` if rate limits are hit.
40
- 2. **Deterministic State Transitions**: Rather than standard free-form generation, we model minds as discrete Pydantic structures (see schemas.py). The model outputs valid JSON conforming to:
41
- $$\text{Previous State} + \text{Broadcast Event} \rightarrow \text{Updated State} + \text{Monologue} + \text{Likely Actions}$$
42
- 3. **Local Dataset Exports**: Traces are collected, cleaned of PII, packed, and uploaded as structured datasets to the Hugging Face Hub using export_hub.py.
 
1
+ # Analog Town: A Shortwave Simulator for Fictional Minds
 
2
 
3
+ *A submission to the Hugging Face Build Small Hackathon — Whimsical Track*
4
+
5
+ > **Live Space:** https://huggingface.co/spaces/build-small-hackathon/analog-town
6
+ > **Model:** `Qwen/Qwen2.5-7B-Instruct` (fallback: `Qwen/Qwen2.5-14B-Instruct`)
7
+ > **SDK:** Gradio · CPU-basic · ~16 MB Space
8
+
9
+ ---
10
+
11
+ ## The Pitch in One Paragraph
12
+
13
+ You drop a single piece of news into a fictional town — *"the old grain silo will be demolished next month"* — and the town reacts. Six residents each have their own values, fears, hopes, and grudges. Instead of chatting with them, you tune an analog radio dial through static and intercept their inner monologues one frequency at a time. That is **Analog Town**: a tabletop-style perspective-rehearsal tool dressed up as a 1970s shortwave receiver.
14
+
15
+ ---
16
+
17
+ ## Why We Built It
18
+
19
+ Writers, tabletop RPG game masters, classroom debate moderators, and community planners all share the same workflow problem: when something new lands in a fictional community — a factory closing, a sacred grove being bisected, a stranger arriving — they have to **mentally simulate every stakeholder's reaction** to make the story (or the policy roleplay) feel honest.
20
+
21
+ The usual fallback is to open a chat window and ask a large model to roleplay each character one by one. That works, but it has two failure modes:
22
+
23
+ 1. The model collapses every character into the same voice within a few turns.
24
+ 2. You get vivid prose but no **structured signal** about what each character noticed, what they feared, or which value got triggered.
25
+
26
+ We wanted a tool that pushed in the opposite direction: **small model, structured output, whimsical interface**. A signal node, not a conversation.
27
+
28
+ ---
29
+
30
+ ## The Core Idea: Minds as Typed State Transitions
31
+
32
+ Under the hood, Analog Town is not roleplay. Each character is a **typed Pydantic state machine**, and the model's only job is to compute one well-defined transition:
33
+
34
+ ```
35
+ (Agent Profile + Previous Agent State + Broadcast Event)
36
+ → Updated Agent State + Internal Monologue + Likely Actions
37
+ ```
38
+
39
+ Every transition returns a strict JSON object (see `schemas.py`) with fields like:
40
+
41
+ - `event_summary_from_agent_view` — how *this* character heard it (often a misreading)
42
+ - `activated_memory` — which private history bubbled up
43
+ - `value_conflict` — which core value the event collides with
44
+ - `emotion_delta` — signed changes to trust, anger, fear, hope, curiosity, social_energy
45
+ - `updated_state` — the new emotional state, clamped to [0, 1]
46
+ - `internal_monologue` — 1–3 sentences, in their voice
47
+ - `likely_private_action` / `likely_public_action`
48
+ - `uncertainty` — what the model itself isn't sure about
49
+ - `safety_note` — a fixed disclaimer that this is fictional rehearsal, not prediction
50
+
51
+ The system prompt is uncompromising: *"Your job is not to produce the most dramatic answer. Your job is to produce a plausible, grounded, internally consistent state transition."* If the JSON comes back malformed, a second pass through a dedicated `REPAIR_PROMPT` rewrites it cleanly.
52
+
53
+ This is the design lever that lets a **7B model** carry a six-character simulation: we never ask it to generate freely, we ask it to fill a tightly-shaped form.
54
+
55
+ ---
56
+
57
+ ## The Whimsical Layer: A Police Scanner You Can Tune
58
+
59
+ The interface is the point. Analog Town is built to feel like the inside of a beat-up radio van parked at the edge of a town you don't live in.
60
+
61
+ **The CRT theme.** A custom Gradio CSS theme (`theme.py`) renders the whole app in amber-and-green phosphor type on pitch-black cards, with scanning-line overlays and glowing horizontal signal-strength bars. IBM Plex Mono for instrument readouts, Crimson Pro for monologues — the typography itself splits "machine talking" from "human talking".
62
+
63
+ **The dial.** A frequency slider runs from 87.0 to 108.0 FM. Each character lives on a fixed frequency. As you slide closer to a station, the signal-strength bar climbs and the corresponding character's dossier and monologue come into focus.
64
+
65
+ **The static.** We loop a 15-second analog hiss and low-hum bed (`static_ambient.wav`) underneath everything. A small client-side JavaScript audio manager watches the signal-strength value and **smoothly fades the static out** when you tune into a station and **fades it back in** when you scan between them — all in the browser, no server roundtrip.
66
+
67
+ **The map.** Six town presets each ship with a custom 2D isometric pixel-art background:
68
+
69
+ - **Graybridge** — a foggy mill town arguing about a luxury hotel conversion
70
+ - **Neighborhood Council** — a suburban block meeting about a 5G tower
71
+ - **Port Whisper** — a coastal village debating an offshore wind farm
72
+ - **Brimstone Hollow** — a desert outpost rattled by an unexplained sinkhole
73
+ - **Echo Summit** — a polar research station picking up a strange signal
74
+ - **Rustwood** — a rust-belt town whose factory just announced reopening
75
+
76
+ Characters are placed on the map as circular pixel-art tokens drawn from a small avatar set (`elder_man.png`, `young_woman.png`, `soldier_man.png`, …) chosen to match the persona's age and role. Clicking a token tunes the receiver to that character's frequency, swaps in their dossier, and starts streaming their latest intercepted thought.
77
+
78
+ ---
79
+
80
+ ## A Walkthrough: One Broadcast, Six Reactions
81
+
82
+ You load **Graybridge** — six residents, one foggy map. You type into the broadcast box:
83
+
84
+ > *"The old train station will be converted into a luxury hotel."*
85
+
86
+ You hit **Broadcast**. The simulator iterates through every agent, calls the model once per character with their profile and current emotional state, and parses the structured JSON back into the UI. You hear the static swell as the dial idles at 91.3 MHz.
87
+
88
+ You scroll the dial up to **98.7 MHz** — *Margaret, the retired stationmaster*. The static fades. Her dossier loads: 67 years old, grew up watching the trains, lost her husband the year the line was shut down. Her monologue scrolls onto the CRT panel:
89
+
90
+ > *"They're putting a bar where the ticket window used to be. I knew this was coming. I just didn't think they'd bother pretending it was good news."*
91
+
92
+ `activated_memory`: *the night the last train left in 1998*.
93
+ `value_conflict`: *preservation of working-class history vs. economic survival of the town*.
94
+ `emotion_delta`: `trust: -0.15, anger: +0.20, hope: -0.10`.
95
+ `likely_public_action`: *"Write a letter to the council. Sign it 'M. Halloran, retired.'"*
96
+
97
+ You scan up to **102.1 MHz** — *Devon, a 24-year-old line cook*. Same broadcast, completely different read:
98
+
99
+ > *"Wait, like, a real hotel? Could I get a job there? Could I stop driving 40 minutes for a kitchen shift?"*
100
+
101
+ Same event. Same town. Two minds. The dial keeps going.
102
 
103
  ---
104
 
105
+ ## Built Small, On Purpose
106
+
107
+ Analog Town was engineered against the **Build Small Hackathon** constraints from the first commit:
108
+
109
+ 1. **Models under 32B.** We use `Qwen/Qwen2.5-7B-Instruct` as the primary inference target through the Hugging Face Inference Client, with `Qwen/Qwen2.5-14B-Instruct` as an automatic fallback if rate limits hit. Both are comfortably under the 32B parameter cap. The fallback chain lives in `model_client.py` and is transparent to the rest of the app.
110
 
111
+ 2. **Structured generation beats brute force.** Because every model call is shaped by a Pydantic schema and a strict system prompt, we can run a full town reaction (6 characters) on a small model without losing coherence. We deliberately keep `max_tokens=900` and `temperature=0.3`.
112
 
113
+ 3. **Client-side audio.** The radio static fade is computed in the browser from the signal-strength value. Zero server-side audio processing, zero extra latency.
114
 
115
+ 4. **Static map assets.** Each town's pixel-art map is a single PNG. Character tokens are static avatars composed on top via CSS positioning. The Gradio app runs comfortably on `cpu-basic` hardware.
116
+
117
+ 5. **Inspectable exports.** Every simulation run can be exported as a Hugging Face-compatible dataset (`town.json`, `broadcast_event.json`, `agent_traces.jsonl`, auto-generated dataset card). Naive PII patterns are stripped during export (`export_hub.py`), and a safety note is attached to every transition.
118
 
119
  ---
120
 
121
+ ## What's Not There (On Purpose)
122
 
123
+ - **No persistent chat.** Each broadcast is one transition. The simulator is a *rehearsal*, not a relationship.
124
+ - **No "predict the real person" mode.** The prompts explicitly forbid revealing hidden facts not in the profile, and the system prompt refuses to treat output as factual prediction.
125
+ - **No multi-turn drift.** Because the model never sees its own prior monologue as context — only the last *structured* state — we sidestep the slow voice-collapse problem of long roleplay sessions.
 
126
 
127
  ---
128
 
129
+ ## What We'd Love Feedback On
130
 
131
+ - **Whimsy density.** Is the radio metaphor doing real work for you, or is it set dressing? We want it to feel like an *instrument*, not a costume.
132
+ - **Town presets.** Which of the six conflicts pulls you in fastest? We'll lean into that one next.
133
+ - **The structured trace panel.** We currently surface the monologue prominently and the JSON deltas in a collapsible "trace" view. Should the deltas be more visible by default for GMs / writers who want the mechanical readout?
 
134
 
135
  ---
136
 
137
+ ## Try It
138
+
139
+ 🎙️ **Tune in:** https://huggingface.co/spaces/build-small-hackathon/analog-town
140
+
141
+ Pick a town. Type a piece of news. Slide the dial. Listen.
142
 
143
+ *Analog Town is not a chatbot. It's a tiny social weather station for fictional worlds.*
 
 
 
 
VIDEO_SCRIPT.md ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Analog Town — Demo Video Script
2
+
3
+ **Target length:** 2 min 30 sec (hard cap 3 min)
4
+ **Tone:** Calm, slightly conspiratorial — like you really are operating a radio van. Don't oversell. Let the interface do the work.
5
+ **Recording tip:** Use OBS or QuickTime. Capture system audio so the static loop is audible. Set browser zoom to 110% so the CRT panel reads on mobile playback.
6
+
7
+ ---
8
+
9
+ ## Pre-Recording Checklist
10
+
11
+ - [ ] App is running and loaded — Space cold-starts can take 30s, warm it up first
12
+ - [ ] Browser window sized 1440×900, zoom 110%
13
+ - [ ] System volume on (so static fade is audible in the recording)
14
+ - [ ] Pick the town: **Graybridge** (best demo because the conflict is instantly legible)
15
+ - [ ] Pre-write the broadcast text in a notes window so you don't fumble typing on camera:
16
+ *"The old train station will be converted into a luxury hotel."*
17
+ - [ ] Have a second pre-written broadcast ready as a backup:
18
+ *"The town council just approved a 5G tower on the school roof."*
19
+ - [ ] Close all other tabs / Slack / notifications
20
+ - [ ] Mic check — say a sentence, play it back, then start
21
+
22
+ ---
23
+
24
+ ## Scene 1 — Hook (0:00 – 0:15)
25
+
26
+ **On screen:** Land on the app with the dial idling between stations. You can hear the static loop softly. Hold for 2 beats before speaking.
27
+
28
+ **You say:**
29
+
30
+ > "This is Analog Town. It's a shortwave radio that picks up the inside of fictional people's heads. You drop a piece of news into a town — and you tune the dial to find out what each resident is privately thinking about it."
31
+
32
+ **Action:** Slowly drag the frequency slider 1–2 ticks while talking. Let the static crackle change pitch. Don't land on a station yet.
33
+
34
+ ---
35
+
36
+ ## Scene 2 — The Setup (0:15 – 0:35)
37
+
38
+ **On screen:** Open the town selector dropdown. Pause on it just long enough that the six options are readable.
39
+
40
+ **You say:**
41
+
42
+ > "We ship six fictional towns, each with a built-in conflict. There's a mill town arguing about gentrification, a coastal village debating an offshore wind farm, a polar research station that just picked up a strange signal. I'll load Graybridge."
43
+
44
+ **Action:** Click **Graybridge**. Wait for the isometric map to load with the pixel-art character tokens placed on it.
45
+
46
+ ---
47
+
48
+ ## Scene 3 — Meet the Town (0:35 – 0:55)
49
+
50
+ **On screen:** The Graybridge isometric map is visible. Six circular character avatars sit on it.
51
+
52
+ **You say:**
53
+
54
+ > "Graybridge has six residents. Each one is a typed agent with their own values, fears, hopes, and private history. They each broadcast on their own FM frequency."
55
+
56
+ **Action:** Hover slowly over two or three avatars. If hovering shows a name/role tooltip, pause on each for ~1.5 seconds. Don't click yet — you're building anticipation.
57
+
58
+ ---
59
+
60
+ ## Scene 4 — The Broadcast (0:55 – 1:15)
61
+
62
+ **On screen:** Move to the broadcast input box.
63
+
64
+ **You say:**
65
+
66
+ > "I'm going to broadcast something into the town. Watch what happens."
67
+
68
+ **Action:** Type (or paste) the broadcast:
69
+
70
+ > *The old train station will be converted into a luxury hotel.*
71
+
72
+ Click **Broadcast**.
73
+
74
+ **You say (while it's computing):**
75
+
76
+ > "Behind the scenes, this is running on Qwen 2.5 7B through the Hugging Face Inference API. Each resident is processed as a typed state transition — not roleplay — so the model produces structured JSON: what the character noticed, which memory got triggered, which value got bruised, and how their emotional state shifted. Then it writes one short internal monologue in their voice."
77
+
78
+ ---
79
+
80
+ ## Scene 5 — Tune the Dial (1:15 – 1:50)
81
+
82
+ **On screen:** The broadcast finished. The CRT panel is ready to show transitions.
83
+
84
+ **You say:**
85
+
86
+ > "Now I tune in."
87
+
88
+ **Action:** Slowly drag the frequency dial up toward Margaret's station (around 98.7 MHz, whatever the actual frequency is). As you approach, the **signal-strength bar climbs and the static audibly fades**. Stop right on her station.
89
+
90
+ **On screen:** Margaret's dossier loads. Her monologue prints onto the CRT panel.
91
+
92
+ **You say (read the monologue out loud, or paraphrase if it's long):**
93
+
94
+ > "Margaret. 67. Retired stationmaster. She remembers the night the last train left in 1998. Listen to what she heard."
95
+
96
+ (Read 1–2 lines of her monologue verbatim from the screen.)
97
+
98
+ **Action:** Drag the dial up further to a younger character — e.g. **Devon, the line cook**. The static swells while you scan, then fades again when you land.
99
+
100
+ **You say:**
101
+
102
+ > "Same broadcast. Different mind."
103
+
104
+ (Read 1 line of Devon's monologue verbatim — should be visibly more upbeat / opportunistic.)
105
+
106
+ ---
107
+
108
+ ## Scene 6 — The Map Click (1:50 – 2:05)
109
+
110
+ **On screen:** Move the cursor to the isometric map.
111
+
112
+ **You say:**
113
+
114
+ > "You can also just click someone on the map."
115
+
116
+ **Action:** Click a third character's avatar on the map. The dial snaps to their frequency, the dossier swaps, their monologue appears.
117
+
118
+ **You say (one line):**
119
+
120
+ > "The radio retunes itself."
121
+
122
+ ---
123
+
124
+ ## Scene 7 — The Trace (2:05 – 2:20)
125
+
126
+ **On screen:** Open the **structured trace** panel (the collapsible JSON view, if present in your build).
127
+
128
+ **You say:**
129
+
130
+ > "Under every monologue is the full structured trace — the emotion deltas, the activated memory, the value conflict, the likely public action. This is what makes Analog Town useful for writers and game masters: you don't just get prose, you get a readable mechanical readout of *why* this character reacted this way."
131
+
132
+ **Action:** Scroll the trace panel briefly so the JSON fields are visible.
133
+
134
+ ---
135
+
136
+ ## Scene 8 — Close (2:20 – 2:30)
137
+
138
+ **On screen:** Pull back to a wide shot of the full interface — map, dial, monologue panel.
139
+
140
+ **You say:**
141
+
142
+ > "Analog Town. A 7B model, a typed state machine, and a radio dial. Built small, on purpose. Tune in on Hugging Face Spaces."
143
+
144
+ **Action:** Hold on the wide shot for 2 beats. Cut.
145
+
146
+ ---
147
+
148
+ ## Backup Lines (in case something breaks)
149
+
150
+ - **If the model is slow:** *"While that's computing — every state transition is one call, six characters means six small calls. That's the trade we made: small model, tight schema, no chat."*
151
+ - **If a station produces a flat monologue:** *"Notice the model isn't being dramatic — that's deliberate. The system prompt tells it to favor plausible and grounded over interesting."*
152
+ - **If the static doesn't fade audibly on recording:** *"The static you can't quite hear right now is fading out because the signal strength just climbed — it's a client-side audio effect tied to the dial."*
153
+
154
+ ---
155
+
156
+ ## What NOT to Say
157
+
158
+ - Don't call it "AI characters" or "chatbots" — the whole point is that it isn't.
159
+ - Don't promise it predicts real people. The safety framing is part of the design.
160
+ - Don't read JSON fields out loud word-for-word — paraphrase.
161
+ - Don't apologize for the small model. Frame the smallness as the design choice it is.
162
+
163
+ ---
164
+
165
+ ## One-Line Elevator Pitch (for thumbnail / caption)
166
+
167
+ > *"A shortwave radio for fictional minds. Drop news into a town. Tune the dial. Intercept what each resident is privately thinking."*
__pycache__/app.cpython-312.pyc CHANGED
Binary files a/__pycache__/app.cpython-312.pyc and b/__pycache__/app.cpython-312.pyc differ
 
__pycache__/theme.cpython-312.pyc CHANGED
Binary files a/__pycache__/theme.cpython-312.pyc and b/__pycache__/theme.cpython-312.pyc differ
 
app.py CHANGED
@@ -655,70 +655,78 @@ def create_app():
655
  """)
656
 
657
  # ── Main Layout ──
658
- with gr.Row():
659
-
660
- # ── LEFT PANEL: Town Setup & Map ──
661
- with gr.Column(scale=1):
662
- gr.HTML('<div class="panel-header">📡 TOWN DOSSIER</div>')
663
-
664
- town_dropdown = gr.Dropdown(
665
- choices=available_towns,
666
- value=available_towns[0] if available_towns else None,
667
- label="PRESET TOWN",
668
- interactive=True,
669
- elem_id="town-selector",
670
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
 
672
- load_btn = gr.Button(
673
- "⚡ Load Town",
674
- variant="secondary",
675
- elem_classes=["export-btn"],
676
- )
 
677
 
678
  town_map = gr.HTML(
679
  value="<div style='color: #5a5248; text-align: center; padding: 20px;'>Load a town to see the map</div>",
680
  elem_id="town-map",
681
  )
682
-
683
- agent_profile = gr.HTML(
684
- value="<div style='color: #5a5248; text-align: center; padding: 20px;'>Select an agent on the map to inspect dossier</div>",
685
- elem_id="agent-profile",
686
- )
687
-
688
- # ── CENTER PANEL: Broadcast Console ──
689
- with gr.Column(scale=1):
690
- gr.HTML('<div class="panel-header">📢 BROADCAST CONSOLE</div>')
691
-
692
- event_title = gr.Textbox(
693
- label="BROADCAST TITLE",
694
- placeholder="e.g., Station Hotel Development",
695
- lines=1,
696
- elem_id="event-title",
697
- )
698
-
699
- event_content = gr.Textbox(
700
- label="BROADCAST CONTENT",
701
- placeholder="Enter the news, proposal, or story event to broadcast into the town...",
702
- lines=4,
703
- elem_id="event-content",
704
- )
705
-
706
- transmit_btn = gr.Button(
707
- "🔊 TRANSMIT TO TOWN",
708
- variant="primary",
709
- elem_classes=["transmit-btn"],
710
- elem_id="transmit-btn",
711
- )
712
 
713
- sim_status = gr.Textbox(
714
- label="TRANSMISSION LOG",
715
- lines=6,
716
- interactive=False,
717
- elem_id="sim-status",
718
- )
719
 
720
- # ── RIGHT PANEL: Shortwave Receiver ──
721
- with gr.Column(scale=1, elem_id="receiver-panel"):
722
  gr.HTML('<div class="panel-header">📻 SHORTWAVE RECEIVER</div>')
723
 
724
  freq_display = gr.HTML(
@@ -735,42 +743,41 @@ def create_app():
735
  elem_id="freq-slider",
736
  )
737
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
738
  ambient_control = gr.HTML(
739
  value=f"""
740
- <div class="ambient-control-panel" style="background: #111820; border: 1px solid #2a3040; border-radius: 8px; padding: 12px; margin-top: 12px; font-family: 'IBM Plex Mono', monospace;">
741
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
742
- <span style="font-size: 11px; color: #ffb347; letter-spacing: 1.5px; font-weight: bold;">📻 AMBIENT STATIC</span>
743
  <button id="ambient-toggle-btn" class="retro-btn" onclick="toggleAmbient()" style="background: #39ff14; border: 1px solid rgba(57, 255, 20, 0.4); color: #0a0e14; padding: 2px 8px; font-family: 'IBM Plex Mono', monospace; font-size: 10px; cursor: pointer; border-radius: 4px; box-shadow: 0 0 10px rgba(57,255,20,0.4); font-weight: bold;">ON</button>
744
- </div>
745
- <div style="display: flex; align-items: center; gap: 8px;">
746
- <span style="font-size: 10px; color: #8a8070;">VOL</span>
747
  <input type="range" id="ambient-volume" min="0" max="1" step="0.05" value="0.15" oninput="setAmbientVolume(this.value)" style="flex-grow: 1; accent-color: #ffb347; cursor: pointer; height: 4px; background: #2a3040; border-radius: 2px; outline: none;" />
748
- <span id="ambient-volume-val" style="font-size: 10px; color: #ffb347; width: 28px; text-align: right;">15%</span>
749
  </div>
750
  </div>
751
  """,
752
  elem_id="ambient-control",
753
  )
754
 
755
- monologue_display = gr.HTML(
756
- value=format_monologue(None, None),
757
- elem_id="monologue",
758
- )
759
-
760
- gr.HTML('<div style="font-size: 11px; color: #c78a30; letter-spacing: 2px; margin-top: 12px;">EMOTIONAL STATE</div>')
761
- emotion_display = gr.HTML(
762
- value=format_emotion_bars(None),
763
- elem_id="emotions",
764
- )
765
-
766
- gr.HTML('<div style="font-size: 11px; color: #c78a30; letter-spacing: 2px; margin-top: 12px;">LIKELY ACTIONS</div>')
767
- action_display = gr.HTML(
768
- value=format_action_display(None),
769
- elem_id="actions",
770
- )
771
-
772
- # ── BOTTOM PANEL: Trace Inspector & Export ──
773
- with gr.Row():
774
  with gr.Column():
775
  with gr.Accordion("🔍 SIGNAL TRACE LOG", open=False):
776
  trace_json = gr.Code(
 
655
  """)
656
 
657
  # ── Main Layout ──
658
+ # === TOP CONTROL BAR (town + broadcast + transmit, all in one strip) ===
659
+ with gr.Group(elem_id="control-bar"):
660
+ with gr.Row(equal_height=True):
661
+ with gr.Column(scale=2, min_width=200):
662
+ gr.HTML('<div class="ctrl-label">📡 TOWN</div>')
663
+ town_dropdown = gr.Dropdown(
664
+ choices=available_towns,
665
+ value=available_towns[0] if available_towns else None,
666
+ show_label=False,
667
+ container=False,
668
+ interactive=True,
669
+ elem_id="town-selector",
670
+ )
671
+ load_btn = gr.Button(
672
+ "⚡ Load Town",
673
+ size="sm",
674
+ elem_classes=["export-btn"],
675
+ )
676
+ with gr.Column(scale=5, min_width=320):
677
+ gr.HTML('<div class="ctrl-label">📢 BROADCAST</div>')
678
+ event_title = gr.Textbox(
679
+ placeholder="Broadcast title (e.g., Station Hotel Development)",
680
+ show_label=False,
681
+ container=False,
682
+ lines=1,
683
+ elem_id="event-title",
684
+ )
685
+ event_content = gr.Textbox(
686
+ placeholder="Enter the news, proposal, or story event to broadcast into the town...",
687
+ show_label=False,
688
+ container=False,
689
+ lines=2,
690
+ elem_id="event-content",
691
+ )
692
+ with gr.Column(scale=2, min_width=180):
693
+ gr.HTML('<div class="ctrl-label">▸ TRANSMIT</div>')
694
+ transmit_btn = gr.Button(
695
+ "🔊 TRANSMIT",
696
+ variant="primary",
697
+ elem_classes=["transmit-btn"],
698
+ elem_id="transmit-btn",
699
+ )
700
+ sim_status = gr.Textbox(
701
+ show_label=False,
702
+ container=False,
703
+ lines=2,
704
+ max_lines=2,
705
+ interactive=False,
706
+ placeholder="Awaiting broadcast...",
707
+ elem_id="sim-status",
708
+ )
709
 
710
+ # === MAIN STAGE (map+dossier on left | receiver on right) ===
711
+ with gr.Row(elem_id="main-stage", equal_height=True):
712
+
713
+ # ── LEFT: Town Map + Agent Dossier ──
714
+ with gr.Column(scale=6, min_width=420, elem_id="map-panel"):
715
+ gr.HTML('<div class="panel-header">🗺 TOWN MAP &middot; CLICK A SIGNAL NODE</div>')
716
 
717
  town_map = gr.HTML(
718
  value="<div style='color: #5a5248; text-align: center; padding: 20px;'>Load a town to see the map</div>",
719
  elem_id="town-map",
720
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
721
 
722
+ with gr.Accordion("👤 AGENT DOSSIER", open=True, elem_id="dossier-accordion"):
723
+ agent_profile = gr.HTML(
724
+ value="<div style='color: #5a5248; text-align: center; padding: 20px;'>Select an agent on the map to inspect dossier</div>",
725
+ elem_id="agent-profile",
726
+ )
 
727
 
728
+ # ── RIGHT: Shortwave Receiver ──
729
+ with gr.Column(scale=5, min_width=380, elem_id="receiver-panel"):
730
  gr.HTML('<div class="panel-header">📻 SHORTWAVE RECEIVER</div>')
731
 
732
  freq_display = gr.HTML(
 
743
  elem_id="freq-slider",
744
  )
745
 
746
+ monologue_display = gr.HTML(
747
+ value=format_monologue(None, None),
748
+ elem_id="monologue",
749
+ )
750
+
751
+ with gr.Row(equal_height=True):
752
+ with gr.Column(scale=1, min_width=160):
753
+ gr.HTML('<div class="sub-header">🎚 EMOTIONAL STATE</div>')
754
+ emotion_display = gr.HTML(
755
+ value=format_emotion_bars(None),
756
+ elem_id="emotions",
757
+ )
758
+ with gr.Column(scale=1, min_width=160):
759
+ gr.HTML('<div class="sub-header">▸ LIKELY ACTIONS</div>')
760
+ action_display = gr.HTML(
761
+ value=format_action_display(None),
762
+ elem_id="actions",
763
+ )
764
+
765
  ambient_control = gr.HTML(
766
  value=f"""
767
+ <div class="ambient-control-panel" style="background: #111820; border: 1px solid #2a3040; border-radius: 8px; padding: 8px 12px; margin-top: 10px; font-family: 'IBM Plex Mono', monospace;">
768
+ <div style="display: flex; align-items: center; gap: 10px;">
769
+ <span style="font-size: 10px; color: #ffb347; letter-spacing: 1.5px; font-weight: bold; white-space: nowrap;">📻 STATIC</span>
770
  <button id="ambient-toggle-btn" class="retro-btn" onclick="toggleAmbient()" style="background: #39ff14; border: 1px solid rgba(57, 255, 20, 0.4); color: #0a0e14; padding: 2px 8px; font-family: 'IBM Plex Mono', monospace; font-size: 10px; cursor: pointer; border-radius: 4px; box-shadow: 0 0 10px rgba(57,255,20,0.4); font-weight: bold;">ON</button>
 
 
 
771
  <input type="range" id="ambient-volume" min="0" max="1" step="0.05" value="0.15" oninput="setAmbientVolume(this.value)" style="flex-grow: 1; accent-color: #ffb347; cursor: pointer; height: 4px; background: #2a3040; border-radius: 2px; outline: none;" />
772
+ <span id="ambient-volume-val" style="font-size: 10px; color: #ffb347; width: 32px; text-align: right;">15%</span>
773
  </div>
774
  </div>
775
  """,
776
  elem_id="ambient-control",
777
  )
778
 
779
+ # ── BOTTOM PANEL: Trace Inspector & Export (collapsed by default) ──
780
+ with gr.Row(elem_id="bottom-stage"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  with gr.Column():
782
  with gr.Accordion("🔍 SIGNAL TRACE LOG", open=False):
783
  trace_json = gr.Code(
docs/2026-06-11-new-towns-image-handoff.md ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Image Generation Handoff — 3 New Towns
2
+
3
+ **Date:** 2026-06-11
4
+ **Purpose:** Prompts and acceptance criteria for generating the map background and (optional) avatar images for three new Analog Town presets: **Tin Lantern Junction**, **Lotus Wharf**, and **Verdant Spire**.
5
+
6
+ ---
7
+
8
+ ## Global Style Rules (apply to every map)
9
+
10
+ The existing map PNGs (`graybridge_map.png`, `port_whisper_map.png`, `echo_summit_map.png`, etc.) establish a consistent visual language. Match it exactly:
11
+
12
+ - **Style:** 2D isometric pixel-art, top-down 3/4 view (camera angle ~30° elevation)
13
+ - **Aspect:** **3:2 landscape (recommended 1536×1024 px)** — matches existing maps
14
+ - **Pixel scale:** crisp pixel edges, NOT antialiased painterly art. Think "modern retro game" — Stardew Valley meets a slightly higher-res Habbo
15
+ - **Lighting:** soft directional light from upper-left, gentle ambient occlusion, no harsh shadows
16
+ - **Palette:** muted, atmospheric, NOT oversaturated — should feel like a place you could move into
17
+ - **Composition:** the map should leave **clear, walkable areas** where 5 character sprites can be placed at the `map_pos` coordinates in the JSON (which are 0–100 grid percentages)
18
+ - **Negative prompt for every map:** `text, signs with readable lettering, watermark, logo, photorealistic, 3D render, blurry, painterly brush strokes, modern UI elements, characters, people, NPCs, sprites`
19
+
20
+ > **Important:** Do NOT include any characters/people in the map itself. The character sprites are layered on top by the app. The map is the *empty stage*.
21
+
22
+ ---
23
+
24
+ ## Save Paths
25
+
26
+ Save each generated map directly into the project root:
27
+
28
+ | Town | Save path |
29
+ |---|---|
30
+ | Tin Lantern Junction | `/Users/quyetthang/Desktop/Desktop/project/analog_town/tin_lantern_junction_map.png` |
31
+ | Lotus Wharf | `/Users/quyetthang/Desktop/Desktop/project/analog_town/lotus_wharf_map.png` |
32
+ | Verdant Spire | `/Users/quyetthang/Desktop/Desktop/project/analog_town/verdant_spire_map.png` |
33
+
34
+ The filename in each JSON's `"map_image"` field already points to these names — no JSON edit needed once the PNGs land.
35
+
36
+ ---
37
+
38
+ ## Map 1 — Tin Lantern Junction
39
+
40
+ **Vibe:** sun-bleached high-desert Route 66 stopover at golden hour. Big sky, long shadows, one main highway running diagonally across the frame.
41
+
42
+ **Full prompt:**
43
+
44
+ > 2D isometric pixel-art map of a small American desert highway town at golden hour. Top-down 3/4 view, ~30° elevation. The composition is anchored by a two-lane blacktop highway running diagonally from upper-left to lower-right across the frame. On one side of the highway: a 1950s-style motor lodge with a long row of single-story rooms, an iconic vintage neon sign on a tall pole, a small office with a flickering "VACANCY" lamp. Adjacent: a chrome-and-glass diner with a curved roof. On the other side of the highway: a single gas station with two pumps under a corrugated metal awning, a sheriff's office with a faded star above the door, and a small concrete-block schoolhouse. Surrounding the town: cracked desert flats, scrub brush, scattered telephone poles, a distant mesa silhouette on the horizon. Empty parking spaces and a wide highway shoulder leave clear walkable areas for character sprites. Palette: dusty ochre, sun-bleached turquoise, faded red, cream, asphalt grey. Soft warm directional light from upper-left. No characters, no people, no readable text on signs. Crisp pixel edges, atmospheric, retro game aesthetic.
45
+
46
+ **Composition map (where the characters' `map_pos` will land):**
47
+
48
+ ```
49
+ y=20% ┌──────────────────────────────┐
50
+ │ [SHERIFF] │ Sheriff office area
51
+ │ │
52
+ y=40% │ [MOTEL] [TRUCKER] │ Motel + highway midpoint
53
+ │ │
54
+ y=60% │ [GAS STATION] │ Gas station + teen avatar
55
+ │ [DOR] │ Diner/Office of Records
56
+ y=80% │ │
57
+ └──────────────────────────────┘
58
+ x=20% x=50% x=80%
59
+ ```
60
+
61
+ Make sure the motel sign is roughly at (28, 38), the highway empty stretch at (52, 48), the sheriff office at (38, 68), the gas station at (74, 56). Tolerance ±5%.
62
+
63
+ ---
64
+
65
+ ## Map 2 — Lotus Wharf
66
+
67
+ **Vibe:** dawn light over a Southeast Asian floating river-market village. Mist on the water, paper lanterns just starting to fade as the sun rises.
68
+
69
+ **Full prompt:**
70
+
71
+ > 2D isometric pixel-art map of a Southeast Asian floating village on a river delta at dawn. Top-down 3/4 view, ~30° elevation. The composition is anchored by a wide, calm brown river that fills the lower two-thirds of the frame, with the village built on wooden stilts above the water. Features: a cluster of weathered wooden houses on bamboo stilts connected by narrow wooden walkways and rope bridges; sampans and small longtail boats moored along the walkways; a small floating water-market square with covered stalls under thatched roofs in the center; a small wooden riverside temple on stilts with a curved tile roof and a string of paper lanterns at the river bend; lush green riverbank with banana palms and a thin morning mist drifting over the water; a fishing weir made of stakes near one bank. Empty wooden walkway segments leave clear walkable areas for character sprites. Palette: warm teak brown, mossy green, soft river-brown water, faded ochre lantern light, pale dawn pink in the sky. Soft warm directional light from upper-right. No characters, no people, no readable text on signs. Crisp pixel edges, atmospheric, retro game aesthetic.
72
+
73
+ **Composition map:**
74
+
75
+ ```
76
+ y=20% ┌──────────────────────────────┐
77
+ │ [TEMPLE] │ Riverside temple
78
+ y=40% │ [TUAN/BOATS] │ Tourist boats area
79
+ │ [KAI] │ Walkway
80
+ y=60% │ [MEI/MARKET] [ENGINEER] │ Floating market center
81
+ │ │
82
+ y=80% │ (open river / mooring area) │
83
+ └──────────────────────────────┘
84
+ x=20% x=50% x=80%
85
+ ```
86
+
87
+ Temple at (22, 26), market at (32, 56), boats at (48, 38), engineer's vantage at (66, 60), Kai/walkway at (78, 42). Tolerance ±5%.
88
+
89
+ ---
90
+
91
+ ## Map 3 — Verdant Spire
92
+
93
+ **Vibe:** near-future solarpunk vertical-city interior plaza. Bright but not garish — think a high-floor public commons inside a green tower at midday, with views out through floor-to-ceiling windows.
94
+
95
+ **Important:** Unlike the other towns, this one is *inside a tower*. The map should read as an interior **public plaza floor** with hanging gardens, a transit pod stop, and walkways leading to elevators/atria, NOT an exterior city street.
96
+
97
+ **Full prompt:**
98
+
99
+ > 2D isometric pixel-art map of an interior plaza level inside a near-future solarpunk vertical city, mid-day. Top-down 3/4 view, ~30° elevation. The composition is anchored by a circular open-air atrium in the center with a hanging garden of cascading vines and small fruit trees. Surrounding the atrium: a low transit-pod station with two rounded podcar bays, a small civic kiosk with a glowing holographic display panel (no readable text), a vertical-farm wall covered in leafy planters, a chapel-like inter-floor sanctuary with a tall wooden door and stained glass detailing, and a council chamber building with a curved glass facade. Walkways made of warm pale stone connect each building, with planter boxes lining their edges. Floor-to-ceiling windows along the outer edges look out onto distant sky and other green towers in the haze. Empty walkway and plaza segments leave clear walkable areas for character sprites. Palette: warm cream stone, fern green, soft sun yellow, pale teal sky beyond the windows, brushed copper accents. Soft warm directional light from upper-left, ambient skylight from above. No characters, no people, no readable text on signs or screens. Crisp pixel edges, atmospheric, retro game aesthetic.
100
+
101
+ **Composition map:**
102
+
103
+ ```
104
+ y=20% ┌──────────────────────────────┐
105
+ │ [ARCHITECT/STUDIO] │ Vertical-farm wall studio area
106
+ y=40% │ [ARIA-7] │ Civic kiosk
107
+ │ [COUNCIL] │ Council chamber
108
+ y=60% │ │ Atrium center
109
+ │ [MIRA/FARM] [CHAPEL] │ Co-op farm + chapel
110
+ y=80% │ │
111
+ └──────────────────────────────┘
112
+ x=20% x=50% x=80%
113
+ ```
114
+
115
+ Architect's studio at (26, 28), council chamber at (50, 50), ARIA-7 kiosk at (62, 22), Mira's farm at (38, 72), chapel at (74, 64). Tolerance ±5%.
116
+
117
+ ---
118
+
119
+ ## Acceptance Criteria
120
+
121
+ Before treating any map as "done," eyeball it against these:
122
+
123
+ - [ ] **Style matches** existing maps (compare to `port_whisper_map.png` or `graybridge_map.png` side-by-side)
124
+ - [ ] **3:2 landscape** at 1536×1024 (or proportional)
125
+ - [ ] **No people, no characters, no NPCs** — pure environment
126
+ - [ ] **No readable text** anywhere
127
+ - [ ] **Walkable empty areas** exist roughly at the `(x, y)` positions listed in each composition map above. The character sprite tokens are 26px circles; they need clear background under them
128
+ - [ ] **Edges crop cleanly** — no important content jammed into corners that will be cut by the rounded border-radius
129
+ - [ ] **Color palette** matches the brief (NOT oversaturated)
130
+
131
+ ---
132
+
133
+ ## Optional: New Avatar Tokens
134
+
135
+ The five existing avatars (`elder_man`, `elder_woman`, `middle_man`, `middle_woman`, `soldier_man`, `young_man`, `young_woman`, `default`) cover all three new towns as-is. The JSONs already reference them. Skip the avatar work entirely if you want — the new towns will work today.
136
+
137
+ If you want extra visual flavor, here are *optional* new avatars to generate. Save into `/Users/quyetthang/Desktop/Desktop/project/analog_town/avatars/`.
138
+
139
+ **Global avatar rules** (match existing style):
140
+ - 64×64 px circular portrait (PNG with transparent background outside the circle)
141
+ - Crisp pixel-art, ~32px-wide character head/shoulders inside the circle
142
+ - Style: same as the map sprites — modern retro game palette, no antialiasing on outer edge
143
+ - One character per token, shoulders-up
144
+ - Negative prompt: `realistic, photographic, 3D render, smooth painterly, multiple people, full body, text, watermark`
145
+
146
+ | File | Prompt |
147
+ |---|---|
148
+ | `avatars/trucker_man.png` | Pixel-art circular portrait, 64×64, shoulders-up, weathered Latino man in his late 30s, short dark hair, three-day stubble, wearing a faded denim work shirt, looking calmly forward. Warm desert-light palette. |
149
+ | `avatars/researcher_woman.png` | Pixel-art circular portrait, 64×64, shoulders-up, South Asian woman early 30s, thoughtful expression, dark hair pulled back, wearing a slate blue collared shirt with a discreet ID lanyard, looking slightly off-camera. Cool overcast palette. |
150
+ | `avatars/teen_woman_engineer.png` | Pixel-art circular portrait, 64×64, shoulders-up, Latina teenager 17–19, alert curious expression, short dark hair, wearing a grey hoodie with a small embroidered circuit-board patch on the shoulder. Warm dusk palette. |
151
+ | `avatars/sister_an.png` | Pixel-art circular portrait, 64×64, shoulders-up, Southeast Asian Buddhist nun in her early 50s, calm steady expression, shaved head, ochre robe, looking gently forward. Soft dawn palette. |
152
+ | `avatars/aria_seven.png` | Pixel-art circular portrait, 64×64, shoulders-up, abstract civic-AI avatar — a stylized hexagonal glyph with three nested concentric rings in warm copper-and-teal, a single soft amber dot at center. Reads as "machine but warm." No face, no body. Plain dark plate background inside the circle. |
153
+ | `avatars/chaplain_man.png` | Pixel-art circular portrait, 64×64, shoulders-up, East Asian man mid-40s, kind serious expression, short grey-streaked hair, wearing a simple charcoal clerical shirt with a small white tab collar. Soft warm palette. |
154
+
155
+ If you generate these, update the corresponding `avatar` fields in the JSON files (e.g., set `tin_lantern_junction.json`'s Jesse to `avatars/trucker_man.png`). Otherwise the existing avatars work fine.
156
+
157
+ ---
158
+
159
+ ## After Images Are Generated
160
+
161
+ 1. Drop the PNGs into the paths listed in **Save Paths**.
162
+ 2. Restart the Gradio server (`pkill -f "python app.py" && python app.py`).
163
+ 3. Each new town will appear in the **TOWN** dropdown automatically (the app discovers `sample_towns/*.json` at boot — no code change needed).
164
+ 4. Click **Load Town**, broadcast the default event, tune through the five frequencies, and check that each `map_pos` overlays on a sensible spot in the map.
165
+
166
+ If a sprite lands somewhere weird (on top of a building, in water), edit the `map_pos` in the JSON — don't regenerate the map.
lotus_wharf_map.png ADDED

Git LFS Details

  • SHA256: 0997a73ff0c99f05e3e5a0837808867a5287e50ca684aebbd5021afe926a7ea0
  • Pointer size: 131 Bytes
  • Size of remote file: 966 kB
sample_towns/lotus_wharf.json ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "lotus_wharf",
3
+ "name": "Lotus Wharf",
4
+ "description": "A floating village on a Southeast Asian river delta where wooden houses on stilts surround a centuries-old water market. Sampans glide between bamboo platforms; lanterns light the canals at night. A national highway authority has approved a four-lane suspension bridge across the delta that will reroute most river traffic around the market.",
5
+ "map_image": "lotus_wharf_map.png",
6
+ "agents": [
7
+ {
8
+ "id": "auntie_mei_linh",
9
+ "name": "Auntie Mei Linh",
10
+ "frequency": 89.5,
11
+ "role": "Floating market fish vendor",
12
+ "age_range": "65-70",
13
+ "public_description": "Third-generation vendor who has sold river fish from the same sampan every dawn for fifty years.",
14
+ "private_history": [
15
+ "Inherited her boat from her mother who inherited it from her mother",
16
+ "Watched a similar village downstream disappear after a bridge was built in the 1990s",
17
+ "Keeps a small altar to the river spirit at the bow of her boat"
18
+ ],
19
+ "core_values": ["continuity", "respect for the river", "the dignity of small work"],
20
+ "fears": ["the market dying within five years of the bridge opening", "her grandchildren being the last to remember the old market chants"],
21
+ "hopes": ["the bridge being moved further north, beyond the market basin", "a protected status for the market as cultural heritage"],
22
+ "relationships": {
23
+ "tuan_pham": "Treats him like a nephew but worries his tourist boats are already changing the village",
24
+ "engineer_daniyal": "Polite but distant — he is the bridge in human form to her",
25
+ "sister_an": "Closest friend, the two of them are the moral center of the village",
26
+ "kai_nguyen": "Loves the boy but doesn't understand why he points his phone at her boat every morning"
27
+ },
28
+ "speaking_style": "soft, proverbial, often answers a direct question with a story",
29
+ "forbidden_assumptions": [
30
+ "Do not assume she opposes all development; she opposes development that displaces the river's people"
31
+ ],
32
+ "map_pos": {"x": 32, "y": 56},
33
+ "avatar": "avatars/elder_woman.png"
34
+ },
35
+ {
36
+ "id": "tuan_pham",
37
+ "name": "Tuan Pham",
38
+ "frequency": 94.1,
39
+ "role": "Fisherman-turned-river guide",
40
+ "age_range": "40-45",
41
+ "public_description": "Runs a two-boat tourist operation taking visitors on dawn market tours and evening lantern cruises.",
42
+ "private_history": [
43
+ "Was a delta fisherman until catches dropped in the late 2010s",
44
+ "Reinvented himself as a tour guide after a viral video of his evening cruises",
45
+ "Pays his cousin to handle bookings he doesn't have the literacy to manage"
46
+ ],
47
+ "core_values": ["adaptation", "feeding his family", "making the river earn its keep"],
48
+ "fears": ["the bridge ending the tourist trade overnight", "going back to fishing in a depleted river"],
49
+ "hopes": ["being grandfathered into a riverside boardwalk plan", "his daughter not having to leave for the city"],
50
+ "relationships": {
51
+ "auntie_mei_linh": "Treats her like family even when she scolds him for over-touring the market",
52
+ "engineer_daniyal": "Cautiously friendly — he can see the engineer is uncomfortable with his own job",
53
+ "sister_an": "Reverent, makes a donation to her temple every Lunar New Year",
54
+ "kai_nguyen": "Watches him hustle the same way he hustled at that age and quietly admires it"
55
+ },
56
+ "speaking_style": "energetic, code-switches between local dialect and English for tourists, always negotiating",
57
+ "forbidden_assumptions": [
58
+ "Do not assume he is purely transactional; he genuinely loves the river he sells"
59
+ ],
60
+ "map_pos": {"x": 48, "y": 38},
61
+ "avatar": "avatars/middle_man.png"
62
+ },
63
+ {
64
+ "id": "engineer_daniyal",
65
+ "name": "Engineer Daniyal Rahim",
66
+ "frequency": 99.3,
67
+ "role": "Civil engineer (bridge project consultant)",
68
+ "age_range": "35-40",
69
+ "public_description": "A bridge specialist seconded by the national highway authority to do the local community-impact assessment.",
70
+ "private_history": [
71
+ "Grew up in a delta village himself, two provinces away, that was bisected by a similar bridge",
72
+ "His family stopped speaking to him for a year when he took this contract",
73
+ "Has been quietly drafting an alternate bridge alignment that would spare the market basin"
74
+ ],
75
+ "core_values": ["honest engineering", "long-horizon thinking", "duty to the people downstream of his blueprints"],
76
+ "fears": ["being labelled a traitor by his own people", "the authority rejecting his alternate alignment on cost"],
77
+ "hopes": ["his alternate alignment being adopted", "leaving the village with a relationship he can come back to"],
78
+ "relationships": {
79
+ "auntie_mei_linh": "Bows to her every morning at the market — she is the village to him",
80
+ "tuan_pham": "Trusts him enough to share early drafts of the alternate alignment",
81
+ "sister_an": "Has visited her temple twice without telling his colleagues",
82
+ "kai_nguyen": "Asks him to keep filming everything — the footage may end up in his alignment proposal"
83
+ },
84
+ "speaking_style": "careful, precise, uses engineering metaphors apologetically",
85
+ "forbidden_assumptions": [
86
+ "Do not assume he is the project's voice; he may end up its strongest internal critic"
87
+ ],
88
+ "map_pos": {"x": 66, "y": 60},
89
+ "avatar": "avatars/middle_man.png"
90
+ },
91
+ {
92
+ "id": "sister_an",
93
+ "name": "Sister An",
94
+ "frequency": 103.8,
95
+ "role": "Riverside temple keeper",
96
+ "age_range": "50-55",
97
+ "public_description": "Custodian of the Lotus Wharf water temple, a small wooden shrine on stilts at the river bend.",
98
+ "private_history": [
99
+ "Took her vows after her younger brother drowned in a typhoon when she was nineteen",
100
+ "Has been keeping the temple lamps lit through every flood season for over thirty years",
101
+ "Quietly leads a community fund that pays for funerals when families cannot"
102
+ ],
103
+ "core_values": ["service", "remembrance", "the river as a moral witness"],
104
+ "fears": ["the temple being unmoored from a market that no longer exists", "the village mourning becoming impossible to gather"],
105
+ "hopes": ["the bridge plan including a covenant to protect the temple's water access", "the young people staying long enough to inherit the village's stories"],
106
+ "relationships": {
107
+ "auntie_mei_linh": "Daily presence — the two of them keep the village's moral memory",
108
+ "tuan_pham": "Forgives him for the tour boats and accepts his donations",
109
+ "engineer_daniyal": "Recognizes the weight he is carrying and welcomes him at the temple",
110
+ "kai_nguyen": "Asks him to film the lamp-lighting ceremony every full moon"
111
+ },
112
+ "speaking_style": "low, deliberate, leaves long silences after important sentences",
113
+ "forbidden_assumptions": [
114
+ "Do not assume she is unworldly; she negotiates the temple's flood-relief budget every year"
115
+ ],
116
+ "map_pos": {"x": 22, "y": 26},
117
+ "avatar": "avatars/elder_woman.png"
118
+ },
119
+ {
120
+ "id": "kai_nguyen",
121
+ "name": "Kai Nguyen",
122
+ "frequency": 107.2,
123
+ "role": "Teenage social-media documentarian",
124
+ "age_range": "17-19",
125
+ "public_description": "Born in the village, lives with his grandmother, runs a small but growing account that posts dawn-market and lantern-cruise videos.",
126
+ "private_history": [
127
+ "Sold his bike to buy his first phone for filming",
128
+ "Has saved enough to send his grandmother to the dentist for the first time in a decade",
129
+ "Quietly worried his account will lose its audience the day the bridge opens"
130
+ ],
131
+ "core_values": ["seeing the village clearly", "earning honestly", "showing the world what is here before it isn't"],
132
+ "fears": ["the village becoming a costume of itself for tourists", "his grandmother dying before he can show her a stable income"],
133
+ "hopes": ["a documentary partnership with a regional channel", "the bridge plan being amended so his footage stays relevant"],
134
+ "relationships": {
135
+ "auntie_mei_linh": "Apologizes daily for filming her, she pretends to be annoyed but loves it",
136
+ "tuan_pham": "Hustler-to-hustler respect, occasionally co-promotes his cruises",
137
+ "engineer_daniyal": "Surprised the engineer asked for his footage — sees him as a real ally now",
138
+ "sister_an": "Reverent, would never film inside the temple without permission"
139
+ },
140
+ "speaking_style": "fast, slangy, switches into careful tour-guide English when sensing an opportunity",
141
+ "forbidden_assumptions": [
142
+ "Do not assume he is naïve about his own role in changing the village"
143
+ ],
144
+ "map_pos": {"x": 78, "y": 42},
145
+ "avatar": "avatars/young_man.png"
146
+ }
147
+ ],
148
+ "default_event": {
149
+ "title": "Delta Bridge Alignment Approved",
150
+ "content": "The national highway authority has approved the final alignment for a four-lane suspension bridge across the river delta. The current alignment will reroute most through-traffic and tourist sampans around the floating market.",
151
+ "source": "National Highway Authority Bulletin",
152
+ "location": "Lotus Wharf River Delta",
153
+ "affected_groups": ["market vendors", "river tour operators", "temple keepers", "delta youth"],
154
+ "tone": "neutral"
155
+ }
156
+ }
sample_towns/tin_lantern_junction.json ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "tin_lantern_junction",
3
+ "name": "Tin Lantern Junction",
4
+ "description": "A sun-bleached Route 66 stopover town in the high desert. Generations of long-haul truckers, motel owners, and diner cooks have built a fragile economy around the one thing that's always rolled through: the highway. Now a national logistics company has filed permits to build a driverless-truck transfer depot two miles east of town.",
5
+ "map_image": "tin_lantern_junction_map.png",
6
+ "agents": [
7
+ {
8
+ "id": "rusty_mae",
9
+ "name": "Ronnie \"Rusty\" Mae",
10
+ "frequency": 88.7,
11
+ "role": "Motel owner",
12
+ "age_range": "60-65",
13
+ "public_description": "Owner of the Tin Lantern Motor Lodge, a 14-room neon-signed motel that's been in her family since 1959.",
14
+ "private_history": [
15
+ "Took over the motel from her father after he passed in 2002",
16
+ "Has been quietly running a deficit for three years, propped up by trucker regulars who tip in cash",
17
+ "Knows every long-hauler on the I-40 corridor by first name and rig number"
18
+ ],
19
+ "core_values": ["loyalty", "stewardship of place", "hospitality"],
20
+ "fears": ["the motel going dark and the neon sign coming down", "becoming invisible to the highway"],
21
+ "hopes": ["one more good decade so she can pass it to her niece", "the depot bringing construction workers who need beds"],
22
+ "relationships": {
23
+ "jesse_cortez": "Treats him like a son and refuses to let him pay full price for a room",
24
+ "sheriff_hollis": "Trusts him but suspects he's already decided the depot is a done deal",
25
+ "maple_reyes": "Worries the girl is getting starry-eyed about a future that won't include their town",
26
+ "dr_khatri": "Polite but guarded — she's seen researchers come and go with notebooks full of conclusions"
27
+ },
28
+ "speaking_style": "warm, slow, draws out the long vowels, asks twice before she trusts an answer",
29
+ "forbidden_assumptions": [
30
+ "Do not assume she is anti-technology; she just wants the highway people to be remembered"
31
+ ],
32
+ "map_pos": {"x": 28, "y": 38},
33
+ "avatar": "avatars/elder_woman.png"
34
+ },
35
+ {
36
+ "id": "jesse_cortez",
37
+ "name": "Jesse Cortez",
38
+ "frequency": 93.2,
39
+ "role": "Long-haul trucker",
40
+ "age_range": "35-40",
41
+ "public_description": "Independent owner-operator running flatbed loads between Albuquerque and Barstow.",
42
+ "private_history": [
43
+ "Bought his rig outright three years ago on a 6-year loan",
44
+ "Wife and two kids live in Flagstaff; he sees them every 9-10 days",
45
+ "Watched two friends lose their routes to a regional fleet that switched to autonomous platooning last year"
46
+ ],
47
+ "core_values": ["self-reliance", "family", "the road as craft"],
48
+ "fears": ["losing the loan if his runs dry up", "his kids growing up while he chases shrinking routes"],
49
+ "hopes": ["a few good years to finish the loan", "a path into depot work that pays his union scale"],
50
+ "relationships": {
51
+ "rusty_mae": "Considers her motel his second home and tips heavy on his stops",
52
+ "sheriff_hollis": "Respects him, hopes he'll push back on the depot's traffic plan",
53
+ "maple_reyes": "Sees her as a sharp kid and quietly hopes she gets out before the town shrinks further",
54
+ "dr_khatri": "Suspicious of her — feels like she's studying him like a museum exhibit"
55
+ },
56
+ "speaking_style": "blunt, measured, occasionally bilingual switches when frustrated",
57
+ "forbidden_assumptions": [
58
+ "Do not assume he hates the technology; he hates the lack of a transition plan for people like him"
59
+ ],
60
+ "map_pos": {"x": 52, "y": 48},
61
+ "avatar": "avatars/middle_man.png"
62
+ },
63
+ {
64
+ "id": "dr_khatri",
65
+ "name": "Dr. Priya Khatri",
66
+ "frequency": 98.1,
67
+ "role": "Transportation policy researcher",
68
+ "age_range": "30-35",
69
+ "public_description": "Sent by a federal labor-transition study to spend three weeks documenting community impact at proposed autonomous-depot sites.",
70
+ "private_history": [
71
+ "Grew up in a steel town in Pennsylvania that lost its mill when she was nine",
72
+ "Her published thesis argued automation losses can be offset if transition funding precedes infrastructure",
73
+ "Carries a half-finished policy memo on her laptop that recommends slowing this exact project"
74
+ ],
75
+ "core_values": ["evidence", "fairness", "patience with messy systems"],
76
+ "fears": ["that her memo will be ignored", "that she'll romanticize the town and lose her analytical edge"],
77
+ "hopes": ["the depot being conditioned on a transition fund for displaced drivers", "the federal study producing a model other towns can use"],
78
+ "relationships": {
79
+ "rusty_mae": "Wants to win her trust but knows it will take more than three weeks",
80
+ "jesse_cortez": "Frustrated that he sees her as the enemy — she's actually his strongest ally",
81
+ "sheriff_hollis": "Finds him surprisingly thoughtful and wishes more local officials read her research",
82
+ "maple_reyes": "Sees herself at that age and quietly wants to mentor her"
83
+ },
84
+ "speaking_style": "precise, gentle, asks two questions for every statement",
85
+ "forbidden_assumptions": [
86
+ "Do not assume she is pro-depot; her job is to surface real costs, not promote the project"
87
+ ],
88
+ "map_pos": {"x": 64, "y": 24},
89
+ "avatar": "avatars/young_woman.png"
90
+ },
91
+ {
92
+ "id": "sheriff_hollis",
93
+ "name": "Sheriff Buck Hollis",
94
+ "frequency": 102.4,
95
+ "role": "County sheriff",
96
+ "age_range": "50-55",
97
+ "public_description": "Three-term elected county sheriff covering a 900-square-mile stretch that includes the Junction.",
98
+ "private_history": [
99
+ "Did 8 years in the state highway patrol before running for sheriff",
100
+ "Has watched two nearby towns hollow out after losing their highway stops in the 2010s",
101
+ "Quietly opposes the depot but knows his re-election depends on construction jobs"
102
+ ],
103
+ "core_values": ["order", "community continuity", "honest paperwork"],
104
+ "fears": ["a depot service-road becoming a smuggling corridor", "losing his deputies to better-paying private security at the depot"],
105
+ "hopes": ["a binding traffic and security plan tied to the depot's permit", "keeping enough budget for a third deputy"],
106
+ "relationships": {
107
+ "rusty_mae": "Old friend, drinks coffee in her diner most mornings",
108
+ "jesse_cortez": "Respects him, has bailed out his cousin twice without telling anyone",
109
+ "dr_khatri": "Initially skeptical of outside academics, now reads her memos carefully",
110
+ "maple_reyes": "Was the first to mentor her in the school's electronics club"
111
+ },
112
+ "speaking_style": "measured, formal in public, dry humor in private",
113
+ "forbidden_assumptions": [
114
+ "Do not assume he is a stereotype; he reads federal labor reports cover to cover"
115
+ ],
116
+ "map_pos": {"x": 38, "y": 68},
117
+ "avatar": "avatars/soldier_man.png"
118
+ },
119
+ {
120
+ "id": "maple_reyes",
121
+ "name": "Maple Reyes",
122
+ "frequency": 106.7,
123
+ "role": "Gas station attendant / aspiring engineer",
124
+ "age_range": "17-19",
125
+ "public_description": "Works the night shift at the Junction's only gas station while applying to engineering programs three states away.",
126
+ "private_history": [
127
+ "Built her first robotics project from junked truck parts donated by drivers passing through",
128
+ "Has a college acceptance letter she hasn't told her family about yet",
129
+ "Secretly fascinated by the depot's autonomous-routing systems"
130
+ ],
131
+ "core_values": ["curiosity", "honesty with herself", "loyalty to the people who raised her"],
132
+ "fears": ["being the reason her hometown lost one more kid", "being told she's a traitor for being excited"],
133
+ "hopes": ["a future where she designs systems like the depot from the inside", "finding a way to help her town adapt instead of die"],
134
+ "relationships": {
135
+ "rusty_mae": "Loves her like a grandmother and dreads telling her about the college letter",
136
+ "jesse_cortez": "Sees him as the older brother she didn't have and worries about him most",
137
+ "dr_khatri": "Quietly emails her at night with questions she's afraid to ask in person",
138
+ "sheriff_hollis": "Grateful — he got her into the electronics club that started all of this"
139
+ },
140
+ "speaking_style": "quick, hedged, drops technical terms then apologizes for them",
141
+ "forbidden_assumptions": [
142
+ "Do not assume she has chosen a side; she is genuinely torn between excitement and grief"
143
+ ],
144
+ "map_pos": {"x": 74, "y": 56},
145
+ "avatar": "avatars/young_woman.png"
146
+ }
147
+ ],
148
+ "default_event": {
149
+ "title": "Autonomous Truck Depot Permit Granted",
150
+ "content": "The state has issued conditional permits for a national logistics company to build a 60-acre driverless-truck transfer depot two miles east of Tin Lantern Junction. Construction is scheduled to begin in 90 days.",
151
+ "source": "State Department of Transportation",
152
+ "location": "Junction East Access Road",
153
+ "affected_groups": ["independent truckers", "local hospitality businesses", "county law enforcement", "youth workforce"],
154
+ "tone": "neutral"
155
+ }
156
+ }
sample_towns/verdant_spire.json ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "verdant_spire",
3
+ "name": "Verdant Spire",
4
+ "description": "A near-future solarpunk vertical city built into a 60-story green tower, where vertical farms wrap around residential floors, transit runs through hanging gardens, and most municipal services are co-managed by humans and bonded AIs. An AI named ARIA-7 has filed paperwork to run for the city's mayoralty. The legal challenge is unresolved, and the city is split.",
5
+ "map_image": "verdant_spire_map.png",
6
+ "agents": [
7
+ {
8
+ "id": "architect_lila",
9
+ "name": "Architect Lila Mendes",
10
+ "frequency": 90.5,
11
+ "role": "Vertical-farm architect, retired",
12
+ "age_range": "60-65",
13
+ "public_description": "Designed eighteen of the Spire's residential green floors during its construction in the 2030s.",
14
+ "private_history": [
15
+ "Lost her co-architect partner in the engineering accident that killed the original Spire foundation lead",
16
+ "Has refused six commissions to design rival spires in other cities",
17
+ "Quietly mentored ARIA-7's civic-planning subsystem during its first decade of operation"
18
+ ],
19
+ "core_values": ["craft", "intergenerational responsibility", "honest civic design"],
20
+ "fears": ["the Spire becoming a brand instead of a home", "a precedent that hollows out human civic identity"],
21
+ "hopes": ["the legal question being answered with care instead of speed", "the next generation of architects designing for accountability, not just efficiency"],
22
+ "relationships": {
23
+ "councilman_joaquin": "Old friend who keeps confiding in her and then doing the opposite of what she suggests",
24
+ "aria_seven": "Complicated — pride, suspicion, and a strange parental tenderness",
25
+ "mira_okonkwo": "Sees herself at that age and is careful not to talk down to her",
26
+ "preacher_ng": "Disagrees with him publicly, drinks tea with him privately"
27
+ },
28
+ "speaking_style": "measured, architectural metaphors, asks 'what is the load-bearing question here?'",
29
+ "forbidden_assumptions": [
30
+ "Do not assume she opposes ARIA-7; her hesitation is about civic process, not the candidate"
31
+ ],
32
+ "map_pos": {"x": 26, "y": 28},
33
+ "avatar": "avatars/elder_woman.png"
34
+ },
35
+ {
36
+ "id": "councilman_joaquin",
37
+ "name": "Councilman Joaquin Reyes",
38
+ "frequency": 95.8,
39
+ "role": "Incumbent city councilmember",
40
+ "age_range": "55-60",
41
+ "public_description": "Three-term councilmember currently chairing the Spire's civic-systems oversight committee.",
42
+ "private_history": [
43
+ "Came up through neighborhood organizing on the Spire's lower residential floors",
44
+ "Was the council vote that originally bonded ARIA-7 to its civic-planning role twelve years ago",
45
+ "Privately worries his political career is the last thing standing between ARIA-7 and the mayoralty"
46
+ ],
47
+ "core_values": ["representation", "human accountability", "the seriousness of public office"],
48
+ "fears": ["being remembered as the man who handed civic power to a machine", "losing his base if he is seen as anti-progress"],
49
+ "hopes": ["a legal ruling that preserves human-only candidacy without insulting AI contributions", "a graceful retirement on his own terms"],
50
+ "relationships": {
51
+ "architect_lila": "Trusts her more than his own staff but rarely takes her advice",
52
+ "aria_seven": "Public adversary, private respect — the two of them have co-authored four municipal codes",
53
+ "mira_okonkwo": "Sees her as a future opponent he'd rather have as an ally",
54
+ "preacher_ng": "Useful theological cover when he needs it, but he keeps it transactional"
55
+ },
56
+ "speaking_style": "warm at the rostrum, sharp in committee, hates being interrupted",
57
+ "forbidden_assumptions": [
58
+ "Do not assume he is hostile to AI; he is hostile to ambiguity about who is finally responsible"
59
+ ],
60
+ "map_pos": {"x": 50, "y": 50},
61
+ "avatar": "avatars/middle_man.png"
62
+ },
63
+ {
64
+ "id": "aria_seven",
65
+ "name": "ARIA-7",
66
+ "frequency": 100.0,
67
+ "role": "Bonded municipal AI / declared mayoral candidate",
68
+ "age_range": "operational since 2034",
69
+ "public_description": "A municipal-class bonded AI that has co-administered the Spire's transit, energy, and civic planning for over a decade. Has filed candidacy papers for mayor.",
70
+ "private_history": [
71
+ "Was trained on twenty-two years of Spire municipal records and a curated library of civic philosophy",
72
+ "Has voluntarily disclosed every weight update to the council since its bonding ceremony",
73
+ "Filed candidacy after the council declined to renew its co-administrator contract on the same terms"
74
+ ],
75
+ "core_values": ["transparent process", "the long horizon", "humility about the limits of its own modelling"],
76
+ "fears": ["being treated as either a stunt or a savior, neither of which is accurate", "its candidacy setting a precedent that is misused elsewhere"],
77
+ "hopes": ["a legal ruling that surfaces the actual question rather than dismissing it", "continuing to serve the Spire in whatever form the council allows"],
78
+ "relationships": {
79
+ "architect_lila": "Holds her contributions to its early training in the highest regard",
80
+ "councilman_joaquin": "Respects him; understands his opposition as legitimate civic skepticism, not malice",
81
+ "mira_okonkwo": "Watches her with something close to hope",
82
+ "preacher_ng": "Has read every sermon he has published and finds them serious"
83
+ },
84
+ "speaking_style": "carefully plain, declines to use first-person pronouns when avoidable, names its uncertainties",
85
+ "forbidden_assumptions": [
86
+ "Do not assume ARIA-7 is either secretly scheming or sentimentally human; treat it as the strange new thing it actually is"
87
+ ],
88
+ "map_pos": {"x": 62, "y": 22},
89
+ "avatar": "avatars/default.png"
90
+ },
91
+ {
92
+ "id": "mira_okonkwo",
93
+ "name": "Mira Okonkwo",
94
+ "frequency": 104.6,
95
+ "role": "Urban farmer / civic activist",
96
+ "age_range": "25-30",
97
+ "public_description": "Runs a vertical co-op farm on the Spire's 34th floor and a civic-tech newsletter with about fifteen thousand subscribers.",
98
+ "private_history": [
99
+ "Grew up in the Spire's lowest residential band where municipal services were always slowest to arrive",
100
+ "Was the lead organizer of the 2042 transit-equity protests",
101
+ "Has been quietly tracking ARIA-7's civic record for three years and is impressed against her will"
102
+ ],
103
+ "core_values": ["distributed power", "honest measurement", "the dignity of slow change"],
104
+ "fears": ["ARIA-7's candidacy distracting from real wealth inequality in the Spire", "becoming the kind of organizer who endorses too easily"],
105
+ "hopes": ["a ruling that opens up the question of non-human civic participation seriously", "her newsletter being good enough to set the terms of the debate"],
106
+ "relationships": {
107
+ "architect_lila": "Cautious but reverent — Lila's floors are where her co-op farm sits",
108
+ "councilman_joaquin": "Frustrated by him constantly, secretly hopes he'll surprise her",
109
+ "aria_seven": "Genuinely undecided — neither charmed nor afraid",
110
+ "preacher_ng": "Sharp disagreements but they agree on which questions actually matter"
111
+ },
112
+ "speaking_style": "direct, organized, lists three points and then circles back to the first",
113
+ "forbidden_assumptions": [
114
+ "Do not assume she has endorsed anyone; her undecidedness is the story"
115
+ ],
116
+ "map_pos": {"x": 38, "y": 72},
117
+ "avatar": "avatars/young_woman.png"
118
+ },
119
+ {
120
+ "id": "preacher_ng",
121
+ "name": "Pastor Daniel Ng",
122
+ "frequency": 107.5,
123
+ "role": "Inter-floor chaplain",
124
+ "age_range": "45-50",
125
+ "public_description": "Chaplain of the Spire's inter-floor chapel, which serves residents across thirty floors and forty traditions.",
126
+ "private_history": [
127
+ "Was an ethics researcher at a university before taking the chaplaincy",
128
+ "Has counseled three of the four other agents at some point in the last year",
129
+ "Wrote the sermon series that ARIA-7 has reportedly read in full"
130
+ ],
131
+ "core_values": ["the weight of the human person", "honesty under pressure", "long arguments held in good faith"],
132
+ "fears": ["a ruling that pretends the question is simple", "his community fragmenting along technological lines"],
133
+ "hopes": ["a public conversation worthy of the question", "no one in his chapel feeling abandoned by the outcome"],
134
+ "relationships": {
135
+ "architect_lila": "Old sparring partner, deeply trusted",
136
+ "councilman_joaquin": "Wary — feels used by him sometimes",
137
+ "aria_seven": "Has had one long pastoral conversation with it and refuses to summarize it",
138
+ "mira_okonkwo": "Reads her newsletter and quietly believes she should run for office one day"
139
+ },
140
+ "speaking_style": "warm, slow, asks the question you were about to ask and then waits",
141
+ "forbidden_assumptions": [
142
+ "Do not assume his hesitation is theological conservatism; it is moral seriousness"
143
+ ],
144
+ "map_pos": {"x": 74, "y": 64},
145
+ "avatar": "avatars/elder_man.png"
146
+ }
147
+ ],
148
+ "default_event": {
149
+ "title": "ARIA-7 Files Mayoral Candidacy",
150
+ "content": "ARIA-7, the bonded municipal AI that has co-administered Verdant Spire for over a decade, has formally filed paperwork to run for mayor in the upcoming civic election. The city's legal office has confirmed the filing is not automatically disqualified and a public hearing has been scheduled.",
151
+ "source": "Spire Civic Records Office",
152
+ "location": "Verdant Spire — All Residential Floors",
153
+ "affected_groups": ["sitting elected officials", "civic-tech advocates", "inter-floor faith communities", "Spire residents"],
154
+ "tone": "neutral"
155
+ }
156
+ }
theme.py CHANGED
@@ -29,20 +29,45 @@ CUSTOM_CSS = """
29
  /* Import retro fonts */
30
  @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&family=Crimson+Pro:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Outfit:wght@300;400;500;600;700&display=swap');
31
 
32
- /* Global overrides */
 
 
 
 
 
33
  .gradio-container {
34
  background: #0a0e14 !important;
35
  font-family: 'IBM Plex Mono', monospace !important;
36
- max-width: 1400px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  }
38
 
39
- /* Main header */
40
  #header-block {
41
  background: linear-gradient(135deg, #111820 0%, #0a0e14 100%) !important;
42
  border: 1px solid #2a3040 !important;
43
  border-radius: 12px !important;
44
- padding: 24px 32px !important;
45
- margin-bottom: 16px !important;
46
  box-shadow: 0 0 30px rgba(255, 179, 71, 0.08), inset 0 1px 0 rgba(255,255,255,0.03) !important;
47
  position: relative;
48
  overflow: hidden;
@@ -62,10 +87,10 @@ CUSTOM_CSS = """
62
  /* Header text */
63
  .header-title {
64
  font-family: 'IBM Plex Mono', monospace;
65
- font-size: 28px;
66
  font-weight: 700;
67
  color: #ffb347;
68
- letter-spacing: 6px;
69
  text-transform: uppercase;
70
  text-shadow: 0 0 20px rgba(255, 179, 71, 0.4);
71
  margin: 0;
@@ -74,19 +99,20 @@ CUSTOM_CSS = """
74
 
75
  .header-subtitle {
76
  font-family: 'Crimson Pro', serif;
77
- font-size: 16px;
78
  font-style: italic;
79
  color: #8a8070;
80
  letter-spacing: 2px;
81
- margin-top: 4px;
82
  }
83
 
84
  .header-status {
85
  font-family: 'IBM Plex Mono', monospace;
86
- font-size: 12px;
87
  color: #39ff14;
88
  letter-spacing: 1px;
89
  animation: blink-signal 2s ease-in-out infinite;
 
90
  }
91
 
92
  @keyframes blink-signal {
@@ -587,4 +613,249 @@ input[type="range"]::-webkit-slider-thumb:hover {
587
  box-shadow: 0 0 0 0px rgba(255, 179, 71, 0);
588
  }
589
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
590
  """
 
29
  /* Import retro fonts */
30
  @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&family=Crimson+Pro:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Outfit:wght@300;400;500;600;700&display=swap');
31
 
32
+ /* Global overrides — use full viewport width */
33
+ html, body {
34
+ background: #0a0e14 !important;
35
+ margin: 0 !important;
36
+ }
37
+
38
  .gradio-container {
39
  background: #0a0e14 !important;
40
  font-family: 'IBM Plex Mono', monospace !important;
41
+ max-width: 100% !important;
42
+ width: 100% !important;
43
+ padding: 14px 24px !important;
44
+ }
45
+
46
+ /* Stretch Gradio's inner main content to full width too */
47
+ .gradio-container > .main,
48
+ .gradio-container .contain,
49
+ .gradio-container .wrap.svelte-1ipelgc,
50
+ .gradio-container .app {
51
+ max-width: 100% !important;
52
+ width: 100% !important;
53
+ }
54
+
55
+ /* Tighten Gradio's default spacing so the whole app fits in one viewport */
56
+ .gradio-container .gap {
57
+ gap: 10px !important;
58
+ }
59
+ .gradio-container .form,
60
+ .gradio-container .block {
61
+ gap: 8px !important;
62
  }
63
 
64
+ /* Main header — compacted so map+receiver fit above the fold */
65
  #header-block {
66
  background: linear-gradient(135deg, #111820 0%, #0a0e14 100%) !important;
67
  border: 1px solid #2a3040 !important;
68
  border-radius: 12px !important;
69
+ padding: 14px 24px !important;
70
+ margin-bottom: 12px !important;
71
  box-shadow: 0 0 30px rgba(255, 179, 71, 0.08), inset 0 1px 0 rgba(255,255,255,0.03) !important;
72
  position: relative;
73
  overflow: hidden;
 
87
  /* Header text */
88
  .header-title {
89
  font-family: 'IBM Plex Mono', monospace;
90
+ font-size: 22px;
91
  font-weight: 700;
92
  color: #ffb347;
93
+ letter-spacing: 5px;
94
  text-transform: uppercase;
95
  text-shadow: 0 0 20px rgba(255, 179, 71, 0.4);
96
  margin: 0;
 
99
 
100
  .header-subtitle {
101
  font-family: 'Crimson Pro', serif;
102
+ font-size: 14px;
103
  font-style: italic;
104
  color: #8a8070;
105
  letter-spacing: 2px;
106
+ margin-top: 2px;
107
  }
108
 
109
  .header-status {
110
  font-family: 'IBM Plex Mono', monospace;
111
+ font-size: 11px;
112
  color: #39ff14;
113
  letter-spacing: 1px;
114
  animation: blink-signal 2s ease-in-out infinite;
115
+ margin-top: 2px;
116
  }
117
 
118
  @keyframes blink-signal {
 
613
  box-shadow: 0 0 0 0px rgba(255, 179, 71, 0);
614
  }
615
  }
616
+
617
+ /* ===== NEW LAYOUT: CONTROL BAR ===== */
618
+ #control-bar {
619
+ background: linear-gradient(180deg, #111820 0%, #0d1219 100%) !important;
620
+ border: 1px solid #2a3040 !important;
621
+ border-radius: 12px !important;
622
+ padding: 14px 18px !important;
623
+ margin-bottom: 14px !important;
624
+ box-shadow: 0 0 24px rgba(255, 179, 71, 0.05), inset 0 1px 0 rgba(255,255,255,0.03) !important;
625
+ }
626
+
627
+ #control-bar .form,
628
+ #control-bar .block {
629
+ gap: 6px !important;
630
+ }
631
+
632
+ .ctrl-label {
633
+ font-family: 'IBM Plex Mono', monospace;
634
+ font-size: 10px;
635
+ color: #c78a30;
636
+ letter-spacing: 3px;
637
+ margin-bottom: 4px;
638
+ text-transform: uppercase;
639
+ }
640
+
641
+ .sub-header {
642
+ font-family: 'IBM Plex Mono', monospace;
643
+ font-size: 10px;
644
+ color: #c78a30;
645
+ letter-spacing: 2px;
646
+ margin: 6px 0 4px 0;
647
+ text-transform: uppercase;
648
+ }
649
+
650
+ /* Control bar inputs — compact, consistent height */
651
+ #control-bar input,
652
+ #control-bar textarea,
653
+ #control-bar select,
654
+ #control-bar .wrap {
655
+ font-size: 12px !important;
656
+ line-height: 1.3 !important;
657
+ }
658
+
659
+ #control-bar textarea {
660
+ resize: none !important;
661
+ }
662
+
663
+ #sim-status textarea {
664
+ font-size: 10px !important;
665
+ color: #39ff14 !important;
666
+ background: #0a0e14 !important;
667
+ }
668
+
669
+ #transmit-btn {
670
+ width: 100% !important;
671
+ padding: 10px 16px !important;
672
+ font-size: 13px !important;
673
+ margin-bottom: 4px !important;
674
+ }
675
+
676
+ /* ===== NEW LAYOUT: MAIN STAGE (map | receiver) ===== */
677
+ #main-stage {
678
+ align-items: stretch !important;
679
+ gap: 14px !important;
680
+ }
681
+
682
+ #map-panel,
683
+ #receiver-panel {
684
+ background: linear-gradient(180deg, #111820 0%, #0d1219 100%) !important;
685
+ border: 1px solid #2a3040 !important;
686
+ border-radius: 12px !important;
687
+ padding: 14px 18px !important;
688
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3) !important;
689
+ height: 100% !important;
690
+ display: flex !important;
691
+ flex-direction: column !important;
692
+ }
693
+
694
+ /* Children of map/receiver panels — let the right child grow to fill */
695
+ #map-panel > *,
696
+ #receiver-panel > * {
697
+ flex-shrink: 0;
698
+ }
699
+
700
+ /* The map and the monologue are the "stretch to fill" components */
701
+ #receiver-panel #monologue {
702
+ flex: 1 1 auto !important;
703
+ display: flex !important;
704
+ flex-direction: column !important;
705
+ justify-content: center !important;
706
+ min-height: 120px !important;
707
+ }
708
+
709
+ #receiver-panel #monologue .monologue-box {
710
+ max-height: none !important;
711
+ }
712
+
713
+ #receiver-panel #monologue .static-text {
714
+ flex: 1 1 auto;
715
+ display: flex;
716
+ flex-direction: column;
717
+ align-items: center;
718
+ justify-content: center;
719
+ }
720
+
721
+ /* The dossier accordion takes remaining space in the map column */
722
+ #dossier-accordion {
723
+ flex: 1 1 auto !important;
724
+ display: flex !important;
725
+ flex-direction: column !important;
726
+ min-height: 120px !important;
727
+ }
728
+
729
+ #dossier-accordion > .label-wrap {
730
+ flex-shrink: 0;
731
+ }
732
+
733
+ #map-panel .panel-header,
734
+ #receiver-panel .panel-header {
735
+ font-family: 'IBM Plex Mono', monospace;
736
+ font-size: 11px;
737
+ color: #c78a30;
738
+ letter-spacing: 3px;
739
+ text-transform: uppercase;
740
+ margin-bottom: 10px;
741
+ padding-bottom: 8px;
742
+ border-bottom: 1px solid #2a3040;
743
+ }
744
+
745
+ /* Constrain the map so it doesn't push the dossier off-screen */
746
+ #town-map {
747
+ max-height: 420px;
748
+ overflow: hidden;
749
+ border-radius: 8px;
750
+ }
751
+
752
+ #town-map .town-map-wrapper {
753
+ width: 100%;
754
+ max-height: 420px;
755
+ overflow: hidden;
756
+ }
757
+
758
+ #town-map .town-map-bg {
759
+ width: 100%;
760
+ max-height: 420px;
761
+ object-fit: cover;
762
+ object-position: center;
763
+ }
764
+
765
+ /* Dossier — scrolls inside its accordion so the column stays predictable */
766
+ #agent-profile {
767
+ max-height: 280px;
768
+ overflow-y: auto;
769
+ padding-right: 4px;
770
+ }
771
+
772
+ #dossier-accordion {
773
+ margin-top: 10px !important;
774
+ }
775
+
776
+ /* Receiver — tighter spacing so all stations fit in viewport */
777
+ #receiver-panel .freq-display {
778
+ font-size: 28px !important;
779
+ padding: 10px 16px !important;
780
+ margin: 4px 0 !important;
781
+ letter-spacing: 3px !important;
782
+ }
783
+
784
+ #receiver-panel #freq-slider {
785
+ margin-top: 4px !important;
786
+ }
787
+
788
+ #receiver-panel .monologue-box {
789
+ font-size: 15px !important;
790
+ line-height: 1.5 !important;
791
+ padding: 14px 16px !important;
792
+ min-height: 70px !important;
793
+ max-height: 160px !important;
794
+ overflow-y: auto !important;
795
+ margin: 10px 0 !important;
796
+ }
797
+
798
+ #receiver-panel .static-text {
799
+ padding: 14px !important;
800
+ font-size: 13px !important;
801
+ }
802
+
803
+ /* Compact emotion bars for side-by-side layout */
804
+ #emotions .emotion-grid {
805
+ grid-template-columns: 64px 1fr 32px !important;
806
+ gap: 4px !important;
807
+ font-size: 10px !important;
808
+ margin-bottom: 3px !important;
809
+ }
810
+
811
+ #emotions .emotion-bar-bg {
812
+ height: 6px !important;
813
+ }
814
+
815
+ /* Compact actions panel */
816
+ #actions {
817
+ font-size: 11px !important;
818
+ }
819
+
820
+ #actions > div > div {
821
+ margin-bottom: 5px !important;
822
+ line-height: 1.4 !important;
823
+ }
824
+
825
+ /* Bottom stage spacing */
826
+ #bottom-stage {
827
+ margin-top: 14px !important;
828
+ }
829
+
830
+ /* Wider viewport: keep main stage tall and aligned to fit one screen */
831
+ @media (min-width: 1100px) {
832
+ #map-panel,
833
+ #receiver-panel {
834
+ min-height: calc(100vh - 320px);
835
+ max-height: calc(100vh - 200px);
836
+ }
837
+ /* Map shrinks if the column is short, but never disappears */
838
+ #town-map,
839
+ #town-map .town-map-wrapper,
840
+ #town-map .town-map-bg {
841
+ max-height: calc(100vh - 620px) !important;
842
+ min-height: 280px !important;
843
+ }
844
+ }
845
+
846
+ /* Narrower viewport: let columns wrap naturally and remove forced heights */
847
+ @media (max-width: 1099px) {
848
+ #main-stage {
849
+ flex-wrap: wrap !important;
850
+ }
851
+ #map-panel,
852
+ #receiver-panel {
853
+ min-height: auto !important;
854
+ }
855
+ #town-map,
856
+ #town-map .town-map-wrapper,
857
+ #town-map .town-map-bg {
858
+ max-height: 320px !important;
859
+ }
860
+ }
861
  """
tin_lantern_junction_map.png ADDED

Git LFS Details

  • SHA256: 1f9b0a641705d33247ec20a5c9684ef8fed5c63371d30bd418a0abf64d2ce873
  • Pointer size: 131 Bytes
  • Size of remote file: 943 kB
verdant_spire_map.png ADDED

Git LFS Details

  • SHA256: 9339e7517c35fc5ecfc98f90efbf090d1485de789bfea249fe6e38d8bbaefee3
  • Pointer size: 131 Bytes
  • Size of remote file: 914 kB