@loothero/dm-engine
v0.10.0
Published
Death Mountain game rules and commit-reveal protocol, ported from the Cairo contracts. Shared by the client, the indexer and the API.
Readme
@provable-games/dm-engine
The Death Mountain game rules and commit-reveal protocol in TypeScript — a mirror of the Cairo
in contracts/src.
Consumed by three places that must agree exactly:
| | |
| --- | --- |
| death-mountain-client | the board the player acts on |
| indexer | off-chain replay of committed transcripts into the event tables |
| api | token-id decoding |
Why it has to be shared
Under commit-reveal the chain executes nothing. The client's simulation is not a guess that on-chain events correct a second later — it is the board, and nothing corrects it. If it diverges from the Cairo replay by one point of health, the next committed action is state-invalid, settlement rejects the transcript, and the run is permanently unplayable.
Two copies of the rules would drift. There is one.
Keeping it honest
The rules are hand-maintained, so nothing in Cairo forces them to stay in step. Three things do:
test/vectors.scheme.json—root,fold,action_entropyand every opcode encoding, printed by the real Cairo. Regenerate withsnforge test commit_reveal::test_vectorsincontracts/, then paste into the fixture.test/replay.entropy.test.ts— asserts each action is a pure function of(state, action, entropy)and draws zeroMath.random(). A missed roll is the bug class that bricks runs.GameSettledcross-check — in production the indexer diffs its replayed adventurer against the contract's packed result and writes areplay_divergencesrow on mismatch. That is the only genuinely independent check, since the client and the indexer share this code.
Configurable markets
Every item has a configurable probability from 0% to 100% in 3.125% steps. The sum of these probabilities is the target average number of distinct items; it is not a hard size limit. Default settings put every item at 25%, averaging 25.25 items per market.
GameSettingsData.market_config is required and comes directly from get_settings. Its three
felts may be decimal/hex strings or the bigints starknet.js returns; a felt coerced to JS
number, or a string that is not a decimal or hex literal ("" included), is rejected with a
TypeError rather than silently producing the wrong market.
packMarketConfig builds it from 101 densities (0..32), and unpackMarketConfig recovers them.
uniformMarketConfig(density) builds a single-density profile and DEFAULT_MARKET_CONFIG is the
registered default; reach for packMarketConfig only for a genuinely per-item profile.
getAvailableItemsBitmap takes the raw level_salt; marketBitmapToIds converts the bitmap
for display. An all-zero profile is an empty market.
These rules target a new contract deployment and a new frontend. Use the new address and ABI;
both the settings storage layout and the market algorithm differ from the existing deployment.
SettingsAdded announces an ID and name; fetch its profile with get_settings, or from the
minigame standard's settings_details view, whose Market Cutoff Plane 0..4 and
Market Enabled Plane rows carry the profile's six 101-bit planes as decimal ascii (a plane is at
most 31 digits, exactly what a felt252 short string holds) -- marketConfigFromSettingRows finds
them by name, takes the values felt-encoded or already ascii-decoded, and returns a
PackedMarketConfig.
See the market guide for the complete API/event mapping,
per-item examples, target-size tradeoffs and encoding. The purchase/drop differential fixtures
and market vectors are checked against Cairo.
V0 action packets
Version 0.10 uses the packet protocol in COMMIT-REVEAL.md.
Each felt contains 1–15 aligned 16-bit instructions; bits 240–243 hold count minus one,
and version/reserved bits are zero. This is a protocol cutover, incompatible with legacy aggregate
transcripts. Initialization is separate: call initializeAdventurer with the mint-block hash
before replaying packet zero. There is no START instruction.
encodeActionPacket and decodeActionPacket preserve player instruction order. Families may
repeat; random actions need not be last; equipment and combat can share a packet. Consecutive
stat upgrades, equipment operations, and drops aggregate within their existing uniqueness and
size limits. Consecutive item purchases absorb one following potion instruction. A potion before
items and repeated potion instructions execute in separate calls. encodeBuy and
gameActionInstructions emit items before potions to preserve the market handler's ordering.
applyActionPacket validates the entire reached packet before dispatch and brackets each group
with stat boosts. Every dispatch derives its seeds from its current state and the packet's shared
entropy. It stops immediately at death or surrender, returning only executed groups and their
events, plus terminalReason. Replay callers must skip all later packets without decoding them;
commitment folding still includes their raw felts. Counts and transcript cursors measure packets.
The compatibility primary/packetPrimary helpers expose only the first scalar gameplay
instruction; use groups or instructions for all actions and execution results for terminality.
encodeActionPacketBundle packs ordered subsystem groups while preserving dispatch boundaries.
It separates adjacent groups only when concatenation would merge their dispatches or exceed
capacity. decodeGameAction supports a single group; use decodeGameActions or
applyActionPacket for a complete packet. ATTACK replaces the beast-health threshold with
BeastDeathPolicy: allow kills, stop before a guaranteed kill, or stop before a possible kill.
The latter two inspect reachable normal/critical damage before every exchange without inspecting
its entropy.
Publishing
indexer_v2 consumes this package through workspace:*. Rebuild the engine before validating
that consumer. The player-facing client lives in a separate repository and needs a published
release containing the matching wire protocol.
From the repository root:
pnpm --dir packages/engine test
pnpm --dir packages/engine typecheck
pnpm --dir packages/engine buildPublish from packages/engine only as part of the coordinated release. The manifest already
names version 0.10.0; do not increment it again while publishing that release. Update the
external client's package range and deployment configuration together with the compatible
contracts and v2 indexer. Legacy aggregate transcripts require the previous protocol.
Layout
src/
protocol.ts domains, action codec, root / fold / actionEntropy
transcript.ts ActionCommitted decoding, re-folding, chain ordering
actions.ts GameAction <-> encoded felt
replay.ts applyAction(state, action, entropy)
packing.ts unpackAdventurer / unpackBag
rules/ combat, exploration, loot, market, beast, token idsapplyAction is the client's useGameCore body verbatim; only the wrapper differs. It never
themes a beast — the indexer stores raw ids and every reader restyles on read.
