Skip to content

S7a: Contract for the clinic - #381

Merged
jamescrosswell merged 6 commits into
mainfrom
task/380-clinic-contract
Oct 22, 2026
Merged

jamescrosswell merged 6 commits into
mainfrom
task/380-clinic-contract

Conversation

@jamescrosswell

Copy link
Copy Markdown
Contributor

The contract S7 (#354) needs for the clinic's doctor to buy a kidney and fit the bionics. Nothing sends or handles any of it yet; S7 does.

Client → server

  • sell_part: {"type": "sell_part", "part": "left_kidney"} sells a part of the player's body, by its Body field, to the buyer where they stand, at its price in buyer.parts. The part is "sold" from then on. sell stays as it is: it goes by carried item id, and a kidney isn't carried.
  • fit: {"type": "fit", "bionic": "infrared_eye", "part": "right_eye"} has the buyer where they stand fit a bionic in place of part, one of the parts it can go in, for its price. The server ignores one that isn't on offer, a part it can't go in, or one they can't pay for.

Server → client

  • player_state's buyer gains parts, what it pays for each part of their body it would buy now ({"left_kidney": 100}), and fittings, the bionics it would fit now, whether or not they can pay. A Fitting is {"bionic": "hydraulic_legs", "name": "Hydraulic legs", "price": 80, "parts": ["legs"]}; the eye's parts are ["left_eye", "right_eye"]. Both are left out while empty, as at the scrap trader's stall.
  • What the keeper would buy or fit now is already in them, so stock stays the server's along with the prices, and the client never decides what's on offer. The doctor only voices them.

Schema

  • BodyPart, a literal of the six Body field names, for sell_part, fit and Fitting.parts, so the client gets a typed union. A test keeps it equal to Body's fields.

Additive only

  • parts and fittings are left out of dumps while empty and aren't required in the exported schema, so every existing message, buyer included, dumps as before. Nothing saved changes: Player.json is the same.
  • sell_part and fit are only added to the client union. protocol.schema.json and protocol.ts are regenerated, and docs/protocol.md has both messages and the new fields.

Choices

  • On buyer, not a new clinic field. The doctor is a keeper at their place who also buys salvage, so one object says everything they'd trade where the player stands, and the client's buyer hint and reach code extend rather than fork.
  • A message of its own for a part. sell's item: null already means "everything it buys", so a part there would have been ambiguous.
  • fit names the part. For the legs and the datajack there's only one, but the eye can go in either, and the player chooses.
  • parts keyed by Key, not BodyPart, like prices: a literal-keyed dict generates no better TypeScript.

Closes #380

🤖 Generated with Claude Code

jamescrosswell and others added 2 commits October 23, 2026 07:19
Adds what S7 needs for the clinic's doctor to buy a kidney and fit the
bionics: `sell_part` sells a part of the player's body by its `Body` field,
`fit` asks for a bionic in place of a part, and `player_state`'s `buyer`
gains `parts`, what it pays for parts of the body, and `fittings`, the
bionics it fits with their prices and the parts each can take. `BodyPart`
names the six `Body` fields. The new fields are left out of dumps while
empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	client/src/net/protocol.ts
#	docs/protocol.md
#	server/simile/protocol/messages.py
#	server/simile/protocol/protocol.schema.json
#	server/tests/protocol/test_messages.py
@jamescrosswell

Copy link
Copy Markdown
Contributor Author

Scope and additivity check out: only the owned paths change. All 44 of main's example messages re-dump byte for byte on this branch. simile schema --check passes, re-exporting protocol.schema.json changes nothing, and protocol.ts matches it. tests/schema, tests/protocol and tests/persistence (868 tests) and src/net (30) pass, so old saves still load. No code comments and no API use.

The design covers S7 and S15. sell_part sells either kidney. fit puts the legs in place of the legs, the datajack in the vertebra and the eye in either socket, and part is that socket. For the client messages, refusing on the server is enough. Whether a part is already sold, a second eye, whether they can pay and what's in stock all depend on state. S7 has to check a fit against the fitting on offer anyway. And Body's validate_assignment already rejects a second eye or a bionic in the wrong part. S8 needs nothing more from this contract, since it reads player.body.

But the fixed rules aren't checked on what the server sends:

  • Fitting accepts a part its bionic can't replace ({"bionic": "hydraulic_legs", "parts": ["left_kidney"]} validates) and repeated parts (["right_eye", "right_eye"]). Add a mode="before" check that parts are distinct and each is in Bionic(bionic).replaces, like Body._bionics_fit, with tests.
  • Buyer.fittings accepts two fittings for the same bionic, but fit picks one by bionic. Reject repeats.
  • Key Buyer.parts by BodyPart instead of Key: {"tail": 100} validates today. protocol.ts comes out the same ({[k: string]: number}, compiled with json-schema-to-typescript to check), and the schema gains propertyNames of BodyPart.
  • Buyer's docstring still says it buys "by sending sell". Mention sell_part and fit too.

A decision for S7, not this PR: the docs leave parts out "once they've sold a kidney", so only one kidney is ever sold. That matches the plan's "a kidney". I'm flagging it so that S7 chooses it on purpose.

@jamescrosswell jamescrosswell mentioned this pull request Oct 22, 2026
…s by BodyPart

A Fitting rejects a part its bionic can't replace or a part named twice,
a Buyer offers each bionic once, and Buyer.parts is keyed by BodyPart, so
the schema gains propertyNames. The clinic's doctor buys one kidney, never
the second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jamescrosswell jamescrosswell mentioned this pull request Oct 22, 2026
@jamescrosswell

Copy link
Copy Markdown
Contributor Author

Re-review at 03eb34f: everything is still additive and the generated files are current. All 44 of main's example messages re-dump byte for byte. simile schema --check passes, a fresh simile protocol export changes nothing, and protocol.ts matches it. tests/schema, tests/protocol and tests/persistence (874) and src/net (30) pass, and CI is green. Each new rejection is tested, and so is the schema's propertyNames. Assignment doesn't apply here: Fitting and Buyer are protocol models without validate_assignment, like Meal. The one-kidney decision reads well in both places.

One gap:

  • Fitting's check runs only when parts is a list, so parts given as a tuple skip it. Fitting(bionic="hydraulic_legs", ..., parts=("left_kidney",)) validates, and so does ("left_eye", "left_eye") for the eye. Run the check mode="after" on the validated self.parts, which has no assignment to guard here, or drop the list gate. Add a tuple case to test_rejects_invalid_fittings.

@jamescrosswell

Copy link
Copy Markdown
Contributor Author

Re-review at 54e2445: approved. Fitting now checks its parts after parsing, so tuples are caught too, and the tuple cases are tested. With main merged in, the PR still only touches its 9 files. All of main's example messages give the same JSON byte for byte when dumped again on this branch. simile schema --check passes, a fresh simile protocol export changes nothing, and protocol.ts matches it. tests/schema, tests/protocol and tests/persistence (876) and src/net (30) pass.

@jamescrosswell
jamescrosswell marked this pull request as ready for review October 22, 2026 20:01
@jamescrosswell
jamescrosswell merged commit 8f77096 into main Oct 22, 2026
7 checks passed
@jamescrosswell
jamescrosswell deleted the task/380-clinic-contract branch October 22, 2026 20:01
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.

S7a: Contract: the clinic

1 participant