Skip to content

feat: resume runs, soften the first descent and add contract admin - #37

Merged
salazarsebas merged 7 commits into
mainfrom
feat/resume-run-and-gentler-first-descent
Oct 12, 2026
Merged

salazarsebas merged 7 commits into
mainfrom
feat/resume-run-and-gentler-first-descent

Conversation

@salazarsebas

@salazarsebas salazarsebas commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

What

Resume an unverified run (#17). The client now keeps { seed, loadout, actions } in local storage on every effective action of a verified run. On load, once the player's record has been read, it asks the contract for the open run (get_open_run) and decides:

  • the saved run matches the open run and is still in progress: the board, hit points, relics and dice are rebuilt by replaying the log through the same simulation the contract mirrors, and the player continues;
  • it matches but had already ended (closed tab right after a win or a defeat): the result screen is shown and the log is submitted;
  • the contract holds no open run, or a different seed or delver: the saved data is dropped silently;
  • Stellar cannot be read: nothing is dropped, the save waits for the next visit.

The save is cleared once the run settles, or when the contract refuses it for good. Saved data is validated before use (version, ranges, action codes, 2,048 actions, 4 KB). To make replay safe, state.ts no longer writes progress to storage while a run ends; the app records it once, on the live end-of-run event, so a restored run cannot be counted twice.

Softer first descent (#18). The balance simulator showed kits clearing from 20.0% to 58.8% at rank 0. Two changes:

  • Warden (floor 1 guard): damage 1-3 becomes 1-2.
  • Reaver Hook: max hit +2 becomes +1, so the stronger starts do not run past the top of the band once floor 1 is gentler.

Both are mirrored in world.rs and the golden vectors are regenerated.

The creator now shows a Forgiving / Balanced / Risky label and "Clears about N in 100 descents" for the selected kit, plus a button back to the recommended kit. The numbers come from client/scripts/kit-difficulty.ts (bun run kits), which plays 2,000 seeded runs per kit with the balanced policy and writes kitDifficulty.generated.ts. CI regenerates it and fails on any difference, so the hints cannot drift. The default draft also reads its kit from that table instead of a hand typed one.

Balance (rank 0, balanced policy)

Before (800 runs) After (2,000 runs, new table)
Lowest kit 20.0% (Lantern, Bulwark Pike, Stone Heart) 28%
Highest kit 58.8% (Cinder, Reaver Hook, Stone Heart) 50% (Cinder, Plumb Blade, Stone Heart)
Mean 37.4% 39%

Every kit now sits inside the 25 to 60 percent band, and a test enforces it.

Tests

  • bun test (new, 20 tests): replay equals live state at every point of a full descent including the next dice roll, discard rules for another seed or delver, 4 KB and 2,048 action caps, malformed data, unavailable storage, resume of a won run and of a lost run, kit table band, recommended kit and tiers.
  • cargo test: 71 contract tests (8 new for the admin paths) and the golden replay of 600 vectors pass.
  • bun run typecheck and bun run build pass. CI now also runs bun test and checks the kit table.
  • Run in a real Chrome against testnet: reloading mid run on floor 2 restored 7/8 HP, 1 relic and the same board. Closing the page right after an extraction and reopening it submitted the saved log and showed the sealed result with rank and the earned delver.

Contract admin and upgrade path

The simulation changed, and the previous deployment could never be changed, so this branch also gives the contract an admin and redeploys it.

  • The constructor takes an admin that must sign the deployment. admin.rs adds upgrade(new_wasm_hash), set_admin(new_admin) (both the current and the new admin must sign, so the role cannot go to a mistyped address), renounce_admin() (the contract can never change afterwards, later calls fail with Frozen), and the read only admin() and version(). Each change emits an event (upgraded, admin_changed, admin_renounced).
  • Instance storage (admin, relic tokens, and with it the code) now has its TTL extended on start_run and finish_run.
  • Trust model: while the role exists, an admin could ship code that skips the replay. The contract README and onchain-boundary.md say so plainly, and name the way out: renounce, or move the role to a multisig, before any mainnet release. The old claims that there is no admin and that the issuer was locked are corrected.

New testnet deployment

  • Game contract: CDEAFSBG7JBN6XMP4HRL7LFBL7L67K4RJK3CIRBV23W3TJQCEADZ7R6N
  • Admin: GCZCKB4FUIS5BGEMJOC3EQWKLGH662BOY3HFLH3WWPCGQJXOILDXP7GZ
  • Relic issuer: GAD752QFRTNEMQW2SELGHQ5VAMIV3O7H4XRLJN2K2TQFGGXKNFG7CQUK
  • DMCOMMON CAAFYL2QZU274NLA2IFDB4L6SUNVQKT6OLB6UXGL5L5LMQCFJXSUYFO3, DMRARE CC4RQ4HLTPTXMCZ3OKDL546XXJGOQ5PI2ET6U33C2XTO4BTS6AJV4ANA, DMCORE CC27JVKNDSUM5FDGP4JZDZTKGHJ5RJV7XHJGITTUGOUFE4DVPN6PGLMI

Verified against the network, not the local keystore:

  • Horizon shows the issuer with a single signer of weight 0 and all thresholds 0, and a transaction signed by the issuer's own key is rejected with TxBadAuth.
  • The admin of each relic Stellar Asset Contract is the game contract.
  • client/scripts/chain-smoke.ts ran end to end with a throwaway account: a lost run, a full clear that earned delver 0, and a veteran clear that raised it to 2 clears. The relics minted on chain (2 DMCOMMON and 2 DMCORE on the account, read from Horizon). The finish_run transactions, all successful on Horizon:
    • defeat: 602d5a167cadfd23e81857282429f8ae41d85522a367613c9888edf6e6da89c0
    • clear, delver earned: 28b5a324723890ff494860b0661a6083f5dcdf7f9f6bef2f1fbee8928fadf850
    • veteran clear: a73fa14a5c92ef3fcc4fece8a7803322ef2170672cfcea6f0de9982d775bed2f
  • The real client against the new contract: a run opened from the browser, six moves, a reload, and the same board, hit points and relics came back.

Upgrade was exercised for real on a separate throwaway deployment so the main one stays untouched: deploy, start_run, upload a build with version 2, upgrade. Afterwards version read 2 and the open run was unchanged. Then renounce_admin: admin() returned null and upgrade failed with error 12 (Frozen). An upgrade attempted without the admin's signature was refused. In cargo test the upgrade path runs against a minimal uploaded wasm, so the authorization, the event and the refusals are covered without a testnet.

The deployment steps are scripted and repeatable in the private repository, including the Horizon and Stellar Asset Contract admin checks. Nothing here touches mainnet.

Closes #17
Closes #18

@salazarsebas salazarsebas changed the title feat: resume unverified runs and soften the first descent feat: resume runs, soften the first descent and add contract admin Oct 12, 2026
@salazarsebas salazarsebas self-assigned this Oct 12, 2026
@salazarsebas
salazarsebas merged commit c57531e into main Oct 12, 2026
2 checks passed
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.

First descent difficulty: soften floor 1 and show how risky a kit is Reliability: persist an unverified run and resume after a reload

1 participant