Skip to content

S4: Darkness and the lamp - #379

Merged
jamescrosswell merged 6 commits into
mainfrom
task/351-darkness-lamp
Oct 22, 2026
Merged

jamescrosswell merged 6 commits into
mainfrom
task/351-darkness-lamp

Conversation

@jamescrosswell

@jamescrosswell jamescrosswell commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Darkness in the metro's unlit stretches, a lamp that lights a circle around the player, a battery that runs down while it's on and charges at plugs, and a gauge. Built on the contract in #377.

Closes #351

What it does

  • Lit and dark. TownMap reads each map's light layer (S3's): TownMap.light, and lit(x, y), dark where a dark stretch covers a cell and no lit one does. The client reads the same layer and draws darkness over the dark stretches (side/dark-view.ts), above everything in the world, on a canvas of four px a cell scaled up smoothly, so the edges are soft.
  • The lamp's light. With the lamp on, a circle of 4.5 tiles' radius around the player's chest is lit, fully within 45% of the radius and fading to its edge. Below 20% charge the circle draws in, to 55% of its size when flat. A flat lamp gives no light. Without the lamp a faint glimmer is enough to make out the player, and nothing else.
  • Seen but not visible. A dark cell the lamp has lit is drawn dimmed from then on (35% brightness) rather than near black, so the way you came stays readable. It's kept per street while the page is open.
  • The battery (server/simile/player/lamp.py): it drains while the lamp is on, and at a plug it charges, on or off, as S4a: Contract: the lamp switch #377 now says (test_it_charges_at_a_plug_on_or_off_up_to_full). Plugged in, it doesn't drain. When it runs flat the lamp goes off by itself, and switching on a flat one does nothing. Like hunger, it only changes while a client is in the game. A collapse switches it off, so it doesn't run down at home while the player recovers (test_a_collapse_switches_the_lamp_off).
  • Plugs (plug interactables, from S4a: Contract: the lamp switch #377): the metro's maintenance plug, now a plug rather than a machine, and three in the village, on the ground floors of home (the maker space, at the entry where you wake after collapsing), the Daily Grind (by the stairs, at 16) and Golden Bowl Noodles (by the entry). The village's are drawn in code as a small charging point with a yellow bolt; the metro's is its breaker box.
  • L switches the lamp (lamp from S4a: Contract: the lamp switch #377). The help panel and the README's controls list it.
  • A full or flat battery reaches the client. PlayerSync sends a charge alone once it has moved by step (0.01), and now also when it reaches exactly 0 or 1, so the gauge never sticks at 99% after charging (test_a_charge_that_reaches_full_or_flat_is_sent_off_the_step, and over the socket in test_the_client_switches_the_lamp_and_hears_it_run_down_and_charge).
  • The gauge, top right under "? controls": L, the battery (green, amber from 50%, red from 20%), and on, off or ⚡ while charging. A line under it says "It's dark. Press L for your lamp.", "Your lamp is running low.", "Your lamp is flat. Charge it at a plug." or "Charging your lamp." when it matters.
  • Things to pick up in the dark need the lamp. S6's pile glints sit under the darkness, so in an unseen dark stretch they're hidden, where the lamp has been they're dimmed, and in its light they glint as anywhere else. The salvage in the west tunnel and the vault's catwalk secret have to be found by lamplight. Within reach, the corner's "Press E to pick up…" still says what's there.
  • No night underground. A street with the new underground map property (the metro, from UNDERGROUND) is always lit as early afternoon, with the glow maps at 55%, so the clinic's front glows; the village keeps its time of day.

Rates

  • Drain: 3 a sim hour, so a full battery lasts 20 minutes on.
  • Charge: 60 a sim hour, flat to full in a minute at a plug.

Both in LampConfig. The metro's dark stretches take seconds to cross, so the battery only bites if it's left on, or in S9's sewers.

Please look at

All from the real client in the ?harness=side harness (which answers lamp as the server would).

Taste calls worth a look: the radius, how dark unseen (95%) and seen (65%) are, the glimmer, the charging point's look and size, and where the gauge sits.

Notes

  • Residents' days are unchanged. test_simulate's 24-hour day (from 08:00, seed 0, talk 3) on main and on this branch: 75,577 EntityMoved events, identical, and the same summary. Residents never use plugs.
  • Saves still load. The real app on a copy of simile.db in a temp dir (fake LLM, no news): all 11 residents back, the player loaded with a full lamp; switched on in the west tunnel and drained, saved, reloaded with the same charge and switch, then charged to full at the maintenance plug.
  • The walk has a new leg: into the dark west tunnel, checking it's dark and the hint, L, checking it's lit, then on to the plug, checking it charges, and L off.
  • Outside the Owns: the lead widened them to street/metro.py, street/village.py, tests/street/test_metro.py and content/street/ for the plugs, and to player/sync.py for the full and flat charge. Beyond that, client/src/ui/lamp-gauge.ts (new, with its CSS and tests) and four lines in ui/dialogue-scene.ts to mount it, a section in ui/README.md, and three tests that assumed the café's ground floor had one interactable: server/tests/world/test_movement.py, server/tests/persistence/test_state.py and client/src/world/interactables.test.ts now pick the counter by id or list the plug.
  • Merged main after S6 (S6: Salvage, and selling it #373) and S10 (S10: The player, visibly bionic #374): player/lifespan.py wires the lamp beside S6's pickups and trade, TownMap has S6's pickups beside light, and the side scene draws S6's glints and the darkness.
  • Light moved from simile.street.plan to simile.world.map, beside TownMap, and plan imports it.

Follow-ups

  • What the lamp has seen stays client-side in stage 1, so a reload forgets it. Keeping it with the world would need a Player field, in a contract of its own.
  • The lamp drains in the village too if it's left on. Lit places could pause it, if that's kinder.
  • docs/protocol.md's player_collapsed could say the player wakes with their lamp switched off.

🤖 Generated with Claude Code

jamescrosswell and others added 3 commits October 23, 2026 06:43
The metro's dark stretches are drawn dark, the lamp lights a circle around
the player, and what it has lit stays dimmed. The battery drains while the
lamp is on and charges on a plug: home, the Daily Grind, the noodle bar and
the metro's maintenance alcove. L switches it, and a gauge shows the charge.
Underground streets keep one light rather than the village's time of day.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	client/src/side/side-scene.ts
#	client/src/ui/dialogue-scene.ts
#	server/simile/player/lifespan.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jamescrosswell

Copy link
Copy Markdown
Contributor Author

Changes requested. The darkness, the lamp and the plugs work, and the village is unchanged. There's one bug in what the gauge hears, and one docstring to trim.

What I checked:

  • pytest tests/player tests/world tests/street tests/persistence: 772 passed. Vitest on side, ui and world: 758 passed. Merged with today's main, player, world, street, persistence, social and conversation pass: 1432 tests.
  • S4a: Contract: the lamp switch #377 has merged. This PR's contract files are identical to it, and it merges cleanly with main (358e627).
  • S13: The journal #378 conflicts in four files: README.md, ui/help.ts, ui/dialogue-scene.ts and player/lifespan.py. Each is two adjacent added lines, so keep both.
  • Scope: every path is within the widened Owns or flagged in the body. The flagged ones are minimal: the 4 lines in dialogue-scene.ts, and the three tests that pick the counter by id or list the plug.
  • The Done-when tests are there: drain, charge at a plug on or off, nothing at zero, and goes out when flat. Unlit is dark without the lamp on both the server and the client. The lit, dark and lamp-lit screenshots are under Please look at.
  • The previews match the description, and the darkness reads well. The glint in the west tunnel only shows by lamplight.
  • The .tmj changes are the three new plug objects, metro_plug's type and the metro's underground. No tile changed and every id was kept; only Tiled's internal object ids moved. preview.png is pixel-identical.
  • Residents' 24-hour day from 08:00 (seed 0, talk 3), on af6faa9 and on this branch: 75,577 moves each, with identical event hashes and summaries.
  • A copy of the current simile.db loads: 11 residents, and the player with a full lamp that's off.
  • Behaviour:
    • 3 an hour to drain and 60 to charge, with no drain on a plug.
    • The lamp only changes while a client is in the game.
    • lamp is ignored without a lamp, when it's already as asked, or to switch a flat one on.
    • L sends it, and typing in the chat doesn't, since the chat box stops the key.
    • Underground, the light is fixed at 13:00 with the glow at 55%.
    • No plug shares its stand tile with another interactable. A collapse keeps the lamp and wakes you on the home plug.
  • The Side walk passed 111 of 111 legs, the lamp leg among them.
  • No anthropic or API keys. player/lifespan.py imports inside running. Nothing personal is committed.

Changes

  1. A full charge doesn't reach the client. PlayerSync sends a charge on its own only once it has moved by step (0.01), and the plug caps the charge at 1.0. So after any drain, the last charge sent is 0.99-something. The 1.0 goes out only with hunger's next step, up to 2.4 sim minutes later. Until then the gauge shows 99%, ⚡ and "Charging your lamp.", and "Your lamp is charged." never shows.
    • To reproduce, put Lamp and PlayerSync at the metro plug with 0.1 s ticks. Charging from 0.6234, the last charge sent is 0.9934. From 0.4567 it's 0.9967. Only charges that start on the 0.01 grid get to 1.0.
    • Suggested fix: in _changed in player/sync.py, also send when a charge reaches 0 or 1. Add a test that a charge to full is sent. sync.py is outside the Owns, but this is a line or two.
  2. Make Lamp's class docstring one line. It's the only comment or docstring in the diff over one line.

Nit: test_the_client_switches_the_lamp_and_hears_it_run_down_and_charge only checks the drain over the socket.

Notes for the user, not blockers

  • The radius: 4.5 tiles is enough to read a tunnel's floor and wall. It shrinks visibly at 8%.
  • The darkness: unseen dark at 95% is near black, and places you've seen, at 65%, stay readable. The light stops in a hard vertical band at the lit alcoves, and the parallax background slides behind the lit columns as you walk.
  • Charging without a key: standing on the plug is enough, and the ⚡ and the hint say it's happening.
  • Plugs drawn in code: the Daily Grind's charging point is small, right of the stairs' foot.
  • The gauge sits under "? controls" in green, amber and red. It shows in the village too whenever you carry the lamp.
  • A reload forgets the places you've seen, as the lead decided.
  • The lamp drains in the village if it's left on, and a collapse leaves it on. The PR lists this as a follow-up.

@jamescrosswell jamescrosswell mentioned this pull request Oct 22, 2026
PlayerSync also sends a charge that reaches exactly 0 or 1, so the gauge
doesn't stop at 99% after charging. Lamp's docstring is one line, and the
socket test checks charging too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jamescrosswell

Copy link
Copy Markdown
Contributor Author

Approved. Both changes are in. A collapse now switching the lamp off is a sensible addition.

What I checked since e0cd996:

  • The merge with main is the same tree I got from merging main myself. The only other commit is the review commit: sync.py, lamp.py, lifespan.py, test_lamp.py and the player README.
  • The full charge. _changed now also sends a charge that reaches 0 or 1. With the old rule, the 0.6234-to-full test fails at 0.9934 and the socket test at 0.99992, so both test the fix.
    • The flat case passes under the old rule too, since the lamp going off already sent the 0. That's still a fine guard.
  • The rule is harmless for hunger. Hunger never stays at 1, because Hunger.tick collapses at 1 or more and resets it to waking in the same tick. It only reaches 0 by eating, and eating already sends through sync.update(). The rule needs a != b, so it sends at most once each time a value arrives at 0 or 1, and nothing sits at a bound and repeats.
  • The collapse. collapsed switches the lamp off before player_collapsed. The next tick's player_state carries it, since on is a steady change, and test_a_collapse_switches_the_lamp_off covers it. Off, at home on the plug, it still charges.
  • The socket test now walks to the plug and checks the charge rising to exactly 1 before the switch.
  • No comment or docstring in the diff is over one line now.
  • pytest tests/player tests/world tests/street tests/persistence: 775 passed. Vitest on side, ui and world: 758 passed. All 7 checks are green.
  • Merging: it merges cleanly with main (358e627). S13: The journal #378 still conflicts in README.md, ui/help.ts, ui/dialogue-scene.ts and player/lifespan.py, each with two adjacent added lines, so keep both.

My earlier notes for the user still stand. The lamp no longer stays on after a collapse, but it still drains in the village if you leave it on.

@jamescrosswell
jamescrosswell marked this pull request as ready for review October 22, 2026 19:11
@jamescrosswell
jamescrosswell merged commit a0918e2 into main Oct 22, 2026
7 checks passed
@jamescrosswell
jamescrosswell deleted the task/351-darkness-lamp branch October 22, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S4: Darkness and the lamp

1 participant