Skip to content

S5: The player gets hungry - #369

Merged
jamescrosswell merged 8 commits into
mainfrom
task/352-hunger
Oct 22, 2026
Merged

jamescrosswell merged 8 commits into
mainfrom
task/352-hunger

Conversation

@jamescrosswell

@jamescrosswell jamescrosswell commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

The player gets hungry, eats, and collapses when they starve, on the contract from #367.

Server: simile.player (new; README)

  • Hunger rises by 0.25 a sim hour, from fed to collapsing in four hours, but only while a client is in the game: one that has sent a player position since it connected (InGame). Nobody starves on the menu, in the character creator or with the game closed, and headless and simulate never touch the player. A test connects a menu-only client and an in-game one, and hunger rises only while the in-game one is there.
  • Eating is eat:
    • At home, anywhere in the building, a meal is free and relieves all of it.
    • At the Daily Grind or the noodle bar, anywhere in the building, a meal costs the economy's meal price (5 or 10) and relieves 0.5 or all of it. It's on offer only while B1's supply hook would feed a resident there: the barista on shift, the keeper in and the batch not sold out. The keeper is paid through the ledger as from OUTSIDE, since the ledger holds residents' money and the player isn't one, and pays the supplies as for any sale. A noodle-bar meal comes out of the batch. The ledger's gap stays 0.
    • Food they carry relieves 0.4 an item.
  • Collapsing. At 1 they collapse:
    • Everything they carry but the lamp is left in a pile where they fell, on the ground below if they were mid-air or on a ladder.
    • They wake at home, at the maker space's entry, with hunger 0.75. The client gets player_collapsed.
    • The lamp and their money stay with them, as decided on S5a: Contract: eating #367's review, so they can see to go back.
  • Picking up. pick_up takes from the nearest pile within 1.5 tiles each item that still fits under the carry limit, in order. Any that don't fit stay in the pile.
  • player_state goes to each new client after the snapshot, clock and the other greetings, and on change (PlayerSync):
    • any change but to hunger or a lamp's charge goes at once, including the meal on offer as they walk into or out of somewhere to eat;
    • hunger or a charge alone goes once one has moved by 0.01, so hunger sends one every 2.4 sim minutes.

Client

  • playerState in net/ keeps the last player_state.
  • A hint in the top-left corner (ui/hunger.ts):
    • "You're hungry." from 0.5, and "You're starving. Eat soon, or you'll collapse." from 0.85.
    • From 0.3, what F eats: "Press F to eat at home.", "…at Golden Bowl Noodles for 10.", or "…your pork bun."
    • With nothing F can eat, where they could, or what the meal here costs against their money.
    • "You collapsed from hunger and woke at home." for six seconds after a collapse.
  • E picks up a pile within a tile. It's a new pile kind in world/reach.ts, so a door beside it goes to ↑ as for any reachable.
  • The side scene puts the player at home on player_collapsed, as through a door. If the street isn't shown yet, or a door is under way, it waits until it's done. The collapse also becomes the side scene's start position (sideResidents.start), so a tab that wasn't in the game when it happened starts the player at home.
  • F is in the controls help and the README's controls table.

Checked by hand

  • Residents behave as before. test_simulate's 24-hour day matches main move for move: 75,577 EntityMoveds, identical with this module's running hook entered too, and 48 meals.
  • Saves still load. A copy of the real simile.db, saved before the player existed, loads with the starting player. A player who collapsed there, leaving a pile, saves and loads back.
  • In the browser, on ?play against a hungry in-memory village:
    • the hint shows;
    • E picks up a pile;
    • F eats the bun;
    • starving collapses the player and wakes them inside the maker space with "Press F to eat at home.", and F there clears the hint.

Please decide

  • Home is the maker space. The village has no house for the player, every house is a resident's, and adding one is content/street/, which S3 owns. Aris's workshop is where the story puts the player, so it stands in. It's PlayerConfig.home, one line to change once there's a house.
  • The café's counter order is unchanged and free. The issue says to eat at the café or noodle bar through the existing ordering. Only the café has player ordering: a drink, over a conversation with the barista. The noodle bar has none. So a meal at either is eat, priced and staffed through the economy, and the café's drink order is left alone.
  • The numbers are placeholders to tune: four hours from fed to collapsing, waking at 0.75, and the relief and prices above. All are in PlayerConfig.
  • Hunger pauses unless someone's in the game. Under simile serve the clock runs at 1x, so otherwise closing the tab overnight, or leaving it on the menu, would collapse the player. InGame tells clients apart by the task each one's messages arrive on, since the hub doesn't say which client sent a message. If the hub ever says which client sent a message, that's a small protocol change.

Beyond Owns, for review:

  • client/src/world/reach.ts, ui/keys.ts, ui/dialogue-scene.ts, ui/help.ts and ui/tuning.ts, for E and F.
  • client/src/side/side-scene.ts, to wake the player at home. It also resends the player's position after each snapshot. Before, a page that outlived a server restart left the server not knowing where the player was until they moved, so it couldn't offer a meal or leave a pile where they fell.
  • README.md's controls table.
  • One line in server/tests/protocol/test_lifespan.py: the full app now greets with player_state after murals. simile.player greets from running so it comes after the other greetings, which leaves the gallery test's four greetings as they were.

Closes #352

🤖 Generated with Claude Code

jamescrosswell and others added 7 commits October 23, 2026 02:14
Hunger rises while someone's playing. The player eats at home for
nothing, at the café or the noodle bar at the economy's meal price while
it would feed a resident, or from food they carry. At 1 they collapse,
leave what they carried where they fell and wake at home, still hungry,
and pick it up again with E. player_state goes out on connect and on
change, a change to hunger or a lamp's charge alone once it's moved by
0.01. The client shows a hunger hint, eats on F and wakes at home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r a reconnect

A player_collapsed that arrives before the street is shown is kept until
it is, and a snapshot makes the client report where the player is again,
so a restarted server knows before they next move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Everything else they carried stays in the pile where they fell, as the
lead decided on the contract's review.

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

Copy link
Copy Markdown
Contributor Author

Changes requested: one small item. The behaviour, the contract and the scope are right. Only the comment rule is left.

Changes:

  1. Trim these docstrings to one line. None of them is exported. Move anything longer to the module README, which already says most of it:
    • server/simile/player/hunger.py:
      • Hunger, 3 lines
      • meal, eat, collapse, pick_up, _pay and ground, 2 lines each
    • server/simile/player/sync.py: PlayerSync, 4 lines

Checked and fine:

  • Scope. Each change outside Owns is small and needed for E, F or waking at home. That includes the snapshot resend in side-scene.ts and the one line in tests/protocol/test_lifespan.py. S3 (S3: The metro, by hand #370) doesn't touch side-scene.ts, and git merge-tree merges this branch cleanly with both S3: The metro, by hand #370 and S12: Aris and the incident #368.
  • Eating.
    • A meal at home is free.
    • A shop meal is offered only while supply would feed a resident, and the keeper gets the purchase and pays the supplies.
    • The ledger's gap stays 0, and a noodle meal comes out of the batch.
    • Without the money, nothing changes.
    • A resident's meal is never taken or charged twice. The player can take the last meal in the 5 s before the economy charges a resident who has started eating, so a batch can sell one more meal than it cooked. Two residents can already do that, so it's not blocking.
  • Collapse and piles. I ran ground() on every open cell that isn't a standing cell, on main's maps and on S3: The metro, by hand #370's maps with the metro. All 5,200 and 6,729 cells reach a standing cell. A collapsed player with a pile round-trips through WorldStore. Every change to the player assigns a whole value.
  • player_state.
    • New clients get it after the snapshot, the clock, paintings and murals.
    • Reconnects get the current state.
    • The 0.01 throttle matches docs/protocol.md.
    • player_collapsed comes before the player_state with the pile.
  • Residents. I ran a 24-hour simulate from 06:00 with and without this module's running hook. Both runs give the same 49,391 EntityMoveds in the same order, and the same report.
  • Code. There's no anthropic SDK, API key or Claude API call. Nothing heavy is imported at module level, and nothing personal is committed.
  • Tests. pytest tests/player tests/cafe tests/economy tests/persistence tests/protocol: 413 passed. vitest run src/net src/ui src/side src/world: 653 passed.

Not blocking:

  • Any /ws client counts as playing. That includes the menu and ?creator, so hunger rises there too. A collapse on the menu is missed, because the side scene isn't running yet. The player then starts at the snapshot's position from before the collapse, which is where they fell, not at home.
  • The server ties playing to hub.connections. No test covers that wiring. test_hunger_waits_while_nobody_is_playing only covers Hunger with a fake.
  • S4 will need another key for the lamp, since F is now eat.

- Hunger rises only while a client that has sent a player position
  since connecting is still connected, so the menu and the creator
  don't count.
- A tab whose side scene wasn't running when the player collapsed
  starts them at home.
- Docstrings in hunger.py and sync.py are one line; the README has
  the rest.

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

Copy link
Copy Markdown
Contributor Author

Approved. I re-reviewed at a5ace46. The docstrings are one line now, and hunger rises only while a client is in the game.

  • Docstrings. Every docstring in simile/player is now one line, and the two new comments are one line each.
  • The asyncio-task check in InGame:
    • It's one task per connection, for the whole connection. Hub.serve reads frames and calls receive, and so the subscribers, directly on the task that runs the ASGI app. Neither Starlette's routing nor Guard starts a task for a websocket. Every uvicorn websocket implementation runs the app on one run_asgi task per connection. The TestClient runs it on one anyio task per session.
    • It doesn't leak. That task ends once serve returns after a disconnect. Hunger.tick evaluates playing every tick, which drops finished tasks, so the set never holds more than the live connections plus ones that ended within the last tick.
    • It holds under uvicorn as well as the TestClient. I ran a real uvicorn.Server in-process with ws set to websockets-sansio (what auto picks) and then websockets, against the test's module set, and connected with the websockets client:
      • With only a menu client connected, hunger doesn't rise. It still doesn't when a second client connects, until that client sends a player_move.
      • Four moves add one task (run_asgi), and hunger then rises 0.25 a sim hour.
      • After a clean close, and after aborting the TCP connection without a close frame, the set is empty and hunger stops.
      • A reconnect counts again once it moves.
    • Callers of hub.receive outside the hub. Only ambient.playthrough's ScriptedPlayer calls it, from the task that runs the playthrough. That task counts as in the game for the run, as the fake connection did before, so nothing changes there.
  • Client. residents.ts sets sideResidents.start from player_collapsed, and the new vitest covers it. Only the side scene sends player_move, and it resends after every snapshot, so a reconnected game counts again.
  • Tests.

Nit, not blocking: the new sentence in client/src/side/README.md runs past the file's 100-column wrap.

@jamescrosswell
jamescrosswell marked this pull request as ready for review October 22, 2026 14:42
@jamescrosswell
jamescrosswell merged commit c8f5c4a into main Oct 22, 2026
7 checks passed
@jamescrosswell
jamescrosswell deleted the task/352-hunger branch October 22, 2026 14:43
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.

S5: The player gets hungry

1 participant