feat(exchange): an offer is a call, not six clicks #142
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/exchange-offer-api"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
World.Content.Exchange.place_offer/3places one offer and takes its escrow,with no screen anywhere in it.
free_slot/1is promoted beside it.confirm/1becomes the screen's thin layer over the same function, keeping only the gate
that is genuinely a screen's — that somebody chose an item at all — and the
slot-in-use check moves below the split so there is one implementation of it.
The bug this came out of
Bot.Goals.Restockdrove the interface: a slot'sSell, the sell-side pack,the price guide,
Confirm. Watched on a live world, the whole leg reads:…and then the goal walked home reporting success. Every trip it had ever made
did this. A green suite, a design pass, a sitting and a merge all went by
without it, because nothing on the server distinguishes "three clicks landed"
from "three clicks were refused" if the caller never looks.
467:0is the click that names the item, and a real client sends it with achild index — measured directly, by clicking a pot in the sell-side pack on a
real client:
467,0 op 1 sub=1 item=1931. The goal sentsub: nil, which fallsthrough
World.Interfaces.Button.authorise/1to the component's static cachemask; the exchange's controls are made clickable at runtime by
enable/0,which the guard has no way to learn about. The guard was right. The bot was
wrong.
Why the child index is not the fix
A bot never reads a screen. It holds no interface state, so its clicks cannot
be derived from anything the server sent — they are a client transcribed by hand
into a goal, free to drift from the real one with nothing comparing them. That
is not a test of the interface; it is a second implementation of it, filed
somewhere nobody looks.
Adding the missing
subwould have fixed this instance and left the mechanismintact for the next one.
So a non-client caller gets the operation instead. Every rule still
applies — eight slots, tradability, price, quantity, having the goods — because
they were always in
Contract.Exchange, and the screen was only ever one routeto them.
Bot.Act.exchange_offer/2andexchange_collect/1sit on the{:bot, …}shelf besiderelocate, and the session runs them against its ownplayer through the
rewrite/2seam the test host already uses.What the goal looks like now
travelling → selling → confirming → leaving. Gone:sell_child/0,control/1,child_for/2,sell_grid_component/0,setup_panel_component/0,confirm_component/0, theScreenalias, the:listingphase,@open_grace,@screen_graceand thelisted?flag — about 65 lines net.Gained:
:confirming, which reads the offer back out ofsnapshot.exchangerather than assuming its emissions landed. That phase is the direct lesson of
the bug above.
It still walks to the booth. Standing at the exchange is not enforced by
place_offer/3— it is not enforced for a human either, where the gate is thatthe screen only opens at a booth — but a fleet exists to make the world look
inhabited, so the goal hops until it is on the booth's own tile before it offers.
Verified in a live world
Hot-reloaded into a running server, a trader seeded with 24 yew logs:
24 × 160 = 3840, exact; the slot released. That is the restock leg completing
for the first time.
Tests
Four new, in
World.Content.ExchangeTest, all without a screen: thatplace_offer/3escrows byte-identically to aConfirmclick (asserted byrunning both and comparing the claims), that a buy takes coins the same way,
that every gate still refuses —
bad_price,bad_quantity,untradeable,shortfall,bad_slot— and that the eight-slot cap holds against a callerasking for an occupied slot directly.
Suite green on
main@923fb6a(rebased onto it after the runecrafting mergelanded mid-flight): 3365 tests, 1456 doctests, 59 properties, 0 failures. Credo
clean.
mix format --check-formattedandmix dialyzerfail identically on astashed clean
mainon this machine — that is #131, not this branch.Follow-up, not in this change
The Grand Exchange clerk is still unreachable: it stands on the desk block
and the booth counter is a wall loc on the shared edge, so
Flags.interactable_from?/4'sedge_open?refuses it. PressingExchangeona clerk does nothing at all. That is the ordinary wall rule applied to an NPC
behind a counter, it affects bankers the same way, and it wants an engine
decision rather than a Grand Exchange fix.
`World.Content.Exchange.place_offer/3` places one offer and takes its escrow, with no screen anywhere in it, and `free_slot/1` is promoted beside it. `confirm/1` becomes the screen's thin layer over the same function -- it keeps the one gate only a screen has, that somebody chose an item at all, and everything else moves below the split. `Bot.Goals.Restock` drove the interface: a slot's `Sell`, the sell-side pack, the price guide, `Confirm`. It sent the pack click without the child index a real client carries, so `Button.authorise/1` refused it as `{:op_disabled, 1}`, the price guide then read `item=nil` and computed `price 0 -> 0`, and `Confirm` was refused with "Choose an item to trade first". The goal reported success and listed nothing on every trip it ever made, with a green suite behind it. Found by watching a live world, which is the only place it was visible. The child index is not the fix. A bot never *reads* a screen, so its clicks are not derived from anything the server sent -- they are a second, hand-written client living in a goal, drifting from the real one with nothing comparing them. Fidelity to the client buys a test only when the clicks come from the client. So the goal states offers instead, through `Bot.Act.exchange_offer/2` and `exchange_collect/1` on the `{:bot, ...}` shelf beside `relocate`. Every rule still applies -- eight slots, tradability, price, quantity, having the goods -- because they were always in `Contract.Exchange`. It still walks to the booth, because a fleet exists to make the world look inhabited, and `:confirming` reads the offer back out of the snapshot rather than assuming three clicks landed. The whole screen layer goes: `sell_child/0`, `control/1`, `child_for/2`, three component lookups, the `:listing` phase and two grace counters.ccd60b83700232c9c012