@xgamesfun/solana 0.25.4 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/js/approve/details.mjs +52 -2
- package/js/approve/live-check.mjs +0 -15
- package/js/approve/server.mjs +5 -4
- package/js/chain/token2022.js +3 -2
- package/js/client/accounts.js +67 -25
- package/js/client/grant.js +7 -2
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +716 -183
- package/js/client/ix.js +617 -113
- package/js/client/sizes.js +210 -64
- package/js/economy/check.mjs +691 -261
- package/js/economy/cost.mjs +2 -4
- package/js/economy/explain.mjs +16 -2
- package/js/economy/guide.mjs +12 -12
- package/js/economy/model.mjs +133 -14
- package/js/economy/plan.mjs +68 -19
- package/js/economy/review.mjs +50 -8
- package/js/economy/schema.json +73 -29
- package/js/economy/simulate.mjs +116 -13
- package/js/mcp/tools.mjs +2 -2
- package/js/play/index.js +855 -296
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +12 -4
- package/skills/xsolana-economy/references/after-launch.md +1 -1
- package/skills/xsolana-economy/references/apps.md +11 -2
- package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
- package/skills/xsolana-economy/references/telling-the-person.md +22 -4
- package/skills/xsolana-economy/references/trust-and-cheating.md +15 -5
- package/types/chain/token2022.d.ts +3 -2
- package/types/client/accounts.d.ts +34 -11
- package/types/client/idl/loot.d.ts +25 -4
- package/types/client/ix.d.ts +217 -45
- package/types/client/sizes.d.ts +42 -8
- package/types/economy/check.d.mts +11 -2
- package/types/economy/cost.d.mts +1 -0
- package/types/economy/live.d.mts +2 -0
- package/types/economy/model.d.mts +75 -12
- package/types/economy/plan.d.mts +31 -2
- package/types/economy/simulate.d.mts +12 -1
- package/types/mcp/server.d.mts +12 -1
- package/types/mcp/tools.d.mts +12 -1
- package/types/play/index.d.ts +210 -144
package/js/economy/cost.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Rent is (bytes + 128) × 5,080 lamports (model.mjs); the loot program's own records are sized byte for byte from
|
|
5
5
|
// their Borsh layout, others (a Core collection, a farm) roughly, and marked so.
|
|
6
|
-
import { boxOrderBytes, plan as compile, contestBytes, farmEnds, roundRentOf } from './plan.mjs';
|
|
6
|
+
import { boxOrderBytes, plan as compile, contestBytes, entryShape, farmEnds, roundRentOf } from './plan.mjs';
|
|
7
7
|
import {
|
|
8
8
|
ACCOUNT_BYTES,
|
|
9
9
|
CONTEST_ENTRY_BYTES,
|
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
RENT_PER_BYTE,
|
|
27
27
|
SIGNATURE_FEE,
|
|
28
28
|
model,
|
|
29
|
-
lotterySplit,
|
|
30
29
|
prizeLine,
|
|
31
30
|
prizeRolls,
|
|
32
31
|
prizes,
|
|
@@ -219,8 +218,7 @@ export function actionCosts(economy, { now } = NO_OPTIONS) {
|
|
|
219
218
|
// (`roundRent`, on by default) pays them, back to it at the close: the buyer pays only the fee and the price. Without
|
|
220
219
|
// it, or once it runs short, the buyer pays them (to the treasury at the close), and a round's first buyer fronts its
|
|
221
220
|
// header. A share the buyer chooses makes each entry keep what arrived.
|
|
222
|
-
const
|
|
223
|
-
const entry = BigInt(entryWidth(chosen, Boolean(l.ticketNft))) * RENT_PER_BYTE;
|
|
221
|
+
const entry = BigInt(entryWidth(...entryShape(l))) * RENT_PER_BYTE;
|
|
224
222
|
const held = roundRentOf(l, m);
|
|
225
223
|
const funded = held > 0n;
|
|
226
224
|
row(
|
package/js/economy/explain.mjs
CHANGED
|
@@ -338,8 +338,22 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
338
338
|
);
|
|
339
339
|
continue;
|
|
340
340
|
}
|
|
341
|
+
// Sales that close before the draw, tickets weighed by time, and places paid from a pool that may be short.
|
|
342
|
+
const timing = [
|
|
343
|
+
src.closesBefore && `sales close ${src.closesBefore} before each draw`,
|
|
344
|
+
src.timeWeighted &&
|
|
345
|
+
`each purchase weighs its tickets times the seconds left until sales close, and a place is won by weight: a ticket bought when a round opens counts about twice the average, one in its last hour next to nothing, so the buy screen shows what a purchase weighs before it is bought`,
|
|
346
|
+
].filter(Boolean);
|
|
347
|
+
const short = (l.shortPools ?? []).map(
|
|
348
|
+
x =>
|
|
349
|
+
`its places paid from “${x.pool}” win ${num(x.wins)} and the pool starts with ${num(x.holds)}: a pool is read once, when the draw is asked, and the project can spend it until then; short, they get ${x.each.map(num).join(', ')} each ("whenShort": "${x.whenShort}")`,
|
|
350
|
+
);
|
|
351
|
+
const late =
|
|
352
|
+
src.noLoss && !src.timeWeighted
|
|
353
|
+
? ` (tickets are not weighted by time: someone may buy most of them ${src.closesBefore ? 'just before sales close' : 'right before the draw'} and risk only the fees)`
|
|
354
|
+
: '';
|
|
341
355
|
actions.push(
|
|
342
|
-
`Buy tickets for “${l.name}”: ${num(l.price)} ${sym(l.token)} a ticket; one ticket bought alone is ${inAll(l.realCost)}, and the fee is per purchase, so several tickets at once share it. ${l.drawsOnce ? 'Drawn once, then it ends' : `Drawn every ${num(l.roundDays)} days`}; ${paysWords(src, treasuryWords)}. ${keepsPot(src) ? 'The pot reaches' : 'A round’s money comes to'} about ${num(l.potBeforeDraw)} ${sym(l.potToken)} and pays ${l.prizes.map(num).join(' / ')}; ${src.noLoss ? `no loss: every ticket comes back to its buyer once the round is drawn (anyone can pull it), and the winners take only the pot, what is paid into it, so a ticket wins ${num(l.expectedPerTicket)} on average on top of its price coming back
|
|
356
|
+
`Buy tickets for “${l.name}”: ${num(l.price)} ${sym(l.token)} a ticket; one ticket bought alone is ${inAll(l.realCost)}, and the fee is per purchase, so several tickets at once share it. ${l.drawsOnce ? 'Drawn once, then it ends' : `Drawn every ${num(l.roundDays)} days`}; ${paysWords(src, treasuryWords)}. ${keepsPot(src) ? 'The pot reaches' : 'A round’s money comes to'} about ${num(l.potBeforeDraw)} ${sym(l.potToken)} and pays ${l.prizes.map(num).join(' / ')}; ${src.noLoss ? `no loss: every ticket comes back to its buyer once the round is drawn (anyone can pull it), and the winners take only the pot, what is paid into it, so a ticket wins ${num(l.expectedPerTicket)} on average on top of its price coming back${late}` : `a ticket is worth ${num(l.expectedPerTicket)} on average${l.unpricedPrizes ? ', plus its chance at the places’ prizes' : ''}${l.returnPercent !== null ? ` (${num(l.returnPercent)}%)` : ''}`}${l.cutPercent && !(src.split ?? []).length ? `; ${num(l.cutPercent)}% of ticket money goes to ${treasuryWords}, the rest ${rest}` : ''}${others}${src.perWallet ? `; at most ${src.perWallet} tickets per wallet a round` : ''}${[...timing, ...short].map(x => `; ${x}`).join('')}.${src.gate ? ` ${gateWords(src.gate, 'buy tickets', m)}.` : ''}`,
|
|
343
357
|
);
|
|
344
358
|
}
|
|
345
359
|
for (const f of sim.farms) {
|
|
@@ -433,7 +447,7 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
433
447
|
continue;
|
|
434
448
|
}
|
|
435
449
|
actions.push(
|
|
436
|
-
`Bid on “${a.name}”, which sells ${lot}: bids in ${sym(a.token)}${a.reserve ? ` from ${num(a.reserve)}` : ''}, each topping the last by at least ${src.minRaise ?? 5}% (+ ${fee} SOL player fee a bid); a bid that is topped goes back to its bidder at once. It runs ${src.runs}${toSeconds(src.extendBy ?? '10m') > 0 ? `, and a bid in the last ${src.extendBy ?? '10m'} moves the end to ${src.extendBy ?? '10m'} after it` : ''}; anyone ends it then, the lot goes to the highest bidder and the money: ${splitOf(src.split)}.${src.lot?.asset ? ' A seller whose account can’t take the money
|
|
450
|
+
`Bid on “${a.name}”, which sells ${lot}: bids in ${sym(a.token)}${a.reserve ? ` from ${num(a.reserve)}` : ''}, each topping the last by at least ${src.minRaise ?? 5}% (+ ${fee} SOL player fee a bid); a bid that is topped goes back to its bidder at once. It runs ${src.runs}${toSeconds(src.extendBy ?? '10m') > 0 ? `, and a bid in the last ${src.extendBy ?? '10m'} moves the end to ${src.extendBy ?? '10m'} after it` : ''}; anyone ends it then, the lot goes to the highest bidder and the money: ${splitOf(src.split)}.${src.lot?.asset ? ' The end pays the seller and gives the lot together; while the lot can’t move (its collection froze it) the end waits and the winning bid stays in the project’s vault, until the lot can move or the project’s keys call the sale off (the bid back to its bidder, the lot home); a lot its collection took back or burned sends the bid back. A seller whose account can’t take the money doesn’t hold up the winner: the lot goes, the auction stays open owing it, and the next end pays and closes it.' : ''} With no bid the lot goes home; an auction nobody ends waits, and sells to the highest bidder whenever anyone ends it.${src.gate ? ` ${gateWords(src.gate, 'bid', m)}.` : ''}`,
|
|
437
451
|
);
|
|
438
452
|
}
|
|
439
453
|
for (const h of m.holds)
|
package/js/economy/guide.mjs
CHANGED
|
@@ -39,48 +39,48 @@ export const PARTS = {
|
|
|
39
39
|
boxes: {
|
|
40
40
|
def: 'box',
|
|
41
41
|
nested: ['outcome', 'odds', 'gate', 'rule', 'effect', 'rise', 'fall', 'split', 'perWalletEach'],
|
|
42
|
-
says: 'A paid (or free) roll on chain with MagicBlock’s verifiable randomness, optionally on an NFT (`rollsOn`: an enchant, a mission’s result, a risky forge), whose outcomes then move its numbers (`shifts`) and send it back, burn it or give it away (`then`; a box that can burn or give it away holds it until the roll settles): each
|
|
42
|
+
says: 'A paid (or free) roll on chain with MagicBlock’s verifiable randomness, optionally on an NFT (`rollsOn`: an enchant, a mission’s result, a risky forge), whose outcomes then move its numbers (`shifts`) and send it back, burn it or give it away (`then`; a box that can burn or give it away holds it until the roll settles): each outcome gives an item, a fixed prize from a pool, tokens minted (`tokens`), an NFT of a stash (picked at random, or a named one: `nft`), a roll of another box (`roll`), several of these together (`bundle`: { "gives": "bundle", "prizes": [...] }, a pack; it gives all it names or isn’t given, so it names a pool once, a stash’s random NFT beside nothing else of that stash, and two of an item need two left), or nothing. A box of one outcome with nothing random and no NFT it rolls on is bought in one step (`buy`): the price split and what it gives handed over at once, with no order and no oracle. A box takes an NFT as its price through `rollsOn` (which NFTs) and `then` (where the one paid goes: burned, given, or into a stash, a trade-in that becomes another player’s prize). A box is also the prize any route gives: a grant (`gives: { "roll": box }`), a lottery’s place (`{ "percent", "roll" }`), a contest’s winners (`roll`), an auction’s lot (`{ "roll": box }`), a mission’s tier and a recipe roll it for a winner, who gets whatever it gives; such a box is left `active: false`. How long a box can be is what one answer of the oracle’s holds (measured: 128 outcomes that give nothing, 29 of token amounts, 18 each of its own item; 41 slots; 72 odds rules; 16 token prizes or 18 collectibles in one outcome; 340 texts in a gate; fewer where its outcomes name pools or stashes, each search for one’s address counted: sixteen stashes fit 9 draws an open where finding them takes 36 tries): a longer weighted list goes in a table or a stash, drawn by one outcome. A roll is on its written odds whatever is in stock, and `whenOut` says what a draw on an outcome that is out does: "stopSales" (the default) stops selling while one couldn’t be covered, a paid miss gives that draw’s worth back at the settle (with no draw made a paid open fails and is refunded), and a free roll goes back to waiting and is asked again until it hits; "drawLeft" draws among what is left; "giveNothing" keeps selling and a miss gives nothing, paid or free. Odds rules (`odds`) add any percent (99,900 makes an outcome a thousand times likelier; a weight stops at the largest, 4,294,967,295) and count on every route, read when a roll is first asked. A sale in one step sells only while its pools hold every fixed prize. A prize won is set aside for its winner at once. An edit of a box’s roll fails only a purchase still waiting (for its refund); any other roll waiting rolls on the box as edited. A gate (a collectible shown, a few wallets, or a list of any size: `{ "list": [address, …] }`, an allowlist whose proof each player passes) and odds can read what a player shows; `opens`/`closes` time a drop; `perWallet`/`perWalletEach` limit each wallet (one ticket per wallet). A split share "chosen" lets the buyer name a referrer; "chosen:<item>" the holder of a collectible they name (plot rent).',
|
|
43
43
|
acts: `the player opens (${sol(FEES.player)} SOL + ${sol(ORACLE_FEE)} SOL to the oracle); its player, a key they named or the project’s keys settle. Chance that pays money is flagged`,
|
|
44
44
|
},
|
|
45
45
|
payments: {
|
|
46
46
|
def: 'payment',
|
|
47
47
|
nested: ['split'],
|
|
48
|
-
says: 'A player pays for something given off chain (an attempt, an entry, a tip), split up to four ways at once. With `any`, the player chooses the amount, at least `amount` (tips, donations). Its split may name one `"chosen"` share, a wallet the payer names each time they pay (a referrer: `app.pay(key, { chosenRecipient })`),
|
|
48
|
+
says: 'A player pays for something given off chain (an attempt, an entry, a tip), split up to four ways at once. With `any`, the player chooses the amount, at least `amount` (tips, donations). Its split may name one `"chosen"` share, a wallet the payer names each time they pay (a referrer: `app.pay(key, { chosenRecipient })`), their own too (so a chosen share is at most a rebate the payer controls: price it so); not `"chosen:<item>"`.',
|
|
49
49
|
acts: `the player pays (${sol(FEES.player)} SOL)`,
|
|
50
50
|
},
|
|
51
51
|
recipes: {
|
|
52
52
|
def: 'recipe',
|
|
53
53
|
nested: ['input', 'output'],
|
|
54
|
-
says: 'Collectibles and tokens in, collectibles and tokens out, all or nothing: merges, upgrades, salvage, breeding, exchanges, repairs, a daily claim. `raises` buy a level or a stat on a collectible kept (+1 level for 100 DUST, up to 10), or with `fromNow` move a time (a day of fuel, a day off a lock); a `staked` line takes a collectible only while it is staked in the farm it names (`farm`; a set bound into a badge, its raises moving that farm’s `lockAttribute`), a `show` line takes any NFT of a collection, the project’s or not (its stats live in the project’s Sheet of it), so players can train an NFT the project didn’t make, and a `trade` line takes one and keeps it: burned, given on, or into a stash (`then`), a trade-in. Each `out` line is a prize (the same prize line every route takes: an item, tokens minted or from a pool, a share of a pool, an NFT of a stash, a roll of a box), given in the craft; `{ "gives": "roll", "box": key, "on": place }` rolls on a collectible it keeps (a risky upgrade).',
|
|
54
|
+
says: 'Collectibles and tokens in, collectibles and tokens out, all or nothing: merges, upgrades, salvage, breeding, exchanges, repairs, a daily claim. Any number of lines; one craft carries what is measured (60 payments in one coin, 26 in different coins, 59 outputs from a pool, 108 raises or needs), and past that a recipe is written but never crafts. Several rolls a craft are taken, each its own order; past 15 the receipts outrun a transaction’s log. `raises` buy a level or a stat on a collectible kept (+1 level for 100 DUST, up to 10), or with `fromNow` move a time (a day of fuel, a day off a lock); a `staked` line takes a collectible only while it is staked in the farm it names (`farm`; a set bound into a badge, its raises moving that farm’s `lockAttribute`), a `show` line takes any NFT of a collection, the project’s or not (its stats live in the project’s Sheet of it), so players can train an NFT the project didn’t make, and a `trade` line takes one and keeps it: burned, given on, or into a stash (`then`), a trade-in. Each `out` line is a prize (the same prize line every route takes: an item, tokens minted or from a pool, a share of a pool, an NFT of a stash, a roll of a box), given in the craft; `{ "gives": "roll", "box": key, "on": place }` rolls on a collectible it keeps (a risky upgrade).',
|
|
55
55
|
acts: `the player crafts (${sol(FEES.player)} SOL)`,
|
|
56
56
|
},
|
|
57
57
|
schedules: {
|
|
58
58
|
def: 'schedule',
|
|
59
|
-
says: 'On a timer: mint a game token into a pool, burn from a pool, move between pools, or pay a wallet, an account, a collectible’s holder, a lottery’s pot ("lottery:<key>") or a farm’s rewards ("farm:<key>"). `start` sets the first run (a cliff), `maxRuns` the last (a vesting of 12 monthly runs), `halveEvery` halves it every N runs; halving with `maxRuns` caps the total for good (an emission with a fixed most). A run missed is missed: one sent late pays once, for the latest run due, and the runs before it lapse; only a sealed schedule’s fixed amount pays every run due. `sealed` (with `fund`): it pays a wallet, the treasury or a burn from a vault of its own that nobody, the owner included, can change, stop or take back; its last run pays what is left.',
|
|
59
|
+
says: 'On a timer: mint a game token into a pool, burn from a pool, move between pools, or pay a wallet, an account, a collectible’s holder, a lottery’s pot ("lottery:<key>") or a farm’s rewards ("farm:<key>"). `start` sets the first run (a cliff), `maxRuns` the last (a vesting of 12 monthly runs), `halveEvery` halves it every N runs; halving with `maxRuns` caps the total for good (an emission with a fixed most). A run missed is missed: one sent late pays once, for the latest run due, and the runs before it lapse; only a sealed schedule’s fixed amount pays every run due. `sealed` (with `fund`): it pays a wallet, the treasury or a burn from a vault of its own that nobody, the owner included, can change, stop or take back; its last run pays what is left. Any number of runs and any end is taken (the checker says a far last run’s date in words); a sealed vault is only as safe as its token (a permanent delegate, a fee its issuer can raise, a freeze), and a payee nobody signs for (a program’s address) is warned of: a multisig vault is fine, an address of the loot program never gets paid.',
|
|
60
60
|
acts: 'the project’s keys, or who it pays, run it when it is due (a sealed one needs nobody else)',
|
|
61
61
|
},
|
|
62
62
|
lotteries: {
|
|
63
63
|
def: 'lottery',
|
|
64
64
|
nested: ['gate', 'rule', 'split'],
|
|
65
|
-
says: 'Tickets sold in rounds, a draw on a timer by the oracle, up to 64 runs of places (a place with `times` is a run of places alike: { "percent": 1, "times": 100 } is a hundred). It names where its places are paid from, exactly one of two, never a default: `"pot": 0` (a number 0 to 15) keeps a pot of its own from round to round (nothing but its winners’ payouts takes from it): each place takes its percent of what the pot holds when the draw is asked, with the round’s ticket money, so `seed`, a sponsor, a share of other sales ("lottery:<key>"), yield and what earlier rounds’ places left all count, and the next round always starts with what the round before left; or `"rest": "<destination>"` pays each round from its own ticket money alone, which nothing from outside can change (no seed, no gifts, no carry-over), and sends what its places leave there ("treasury", "burn", "wallet:<address>", "holder:<collectible>", "account:<token account>", a pool, a farm or another lottery; never "chosen"; a pool, farm or lottery named here counts the leftover only once someone pulls it).
|
|
65
|
+
says: 'Tickets sold in rounds, a draw on a timer by the oracle, up to 64 runs of places (a place with `times` is a run of places alike: { "percent": 1, "times": 100 } is a hundred). It names where its places are paid from, exactly one of two, never a default: `"pot": 0` (a number 0 to 15) keeps a pot of its own from round to round (nothing but its winners’ payouts takes from it): each place takes its percent of what the pot holds when the draw is asked, with the round’s ticket money, so `seed`, a sponsor, a share of other sales ("lottery:<key>"), yield and what earlier rounds’ places left all count, and the next round always starts with what the round before left; or `"rest": "<destination>"` pays each round from its own ticket money alone, which nothing from outside can change (no seed, no gifts, no carry-over), and sends what its places leave there ("treasury", "burn", "wallet:<address>", "holder:<collectible>", "account:<token account>", a pool, a farm or another lottery; never "chosen"; a pool, farm or lottery named here counts the leftover only once someone pulls it). `split` sends part of ticket money elsewhere first (a charity, a referrer the buyer names with "chosen", the treasury). Leave out `every` for a raffle that draws once and ends. `perWallet` caps each wallet’s tickets in a round. A place can win a prize instead of (or as well as) its percent: `{ "percent": 0, "prize": { "gives": "nft", "stash": key, "nft": address } }` raffles one NFT, which its winner pulls after the draw (if that NFT is gone from its stash, a stranger’s pull is refused: the winner or a project key passes it over); a lottery that draws once with `nfts` (put in its own stash at go-live) and places { "prize": { "gives": "raffled" }, "times": n } raffles many, each winner drawing one at random; `{ "gives": "roll", "box": key }` makes it a random prize. `ticketNft` ({ "rule", "then" }) makes a ticket cost one NFT instead of tokens: held until the draw, then burned, given on or stashed (back to its buyer if the draw is cancelled; no price, no split). The draw’s ask fixes the round’s money and opens the next round at once; then anyone pulls each part (a refund, a place, a share) in any order, and the last pull closes the round. The owner puts SOL in the lottery at go-live for its rounds’ records and every ticket’s entry (`roundRent`, on by default and sized from `expect`: rounds at once and purchases a round; back at each close), so buyers pay no program rent; 0 turns it off, and then a round’s first buyer fronts its record and each buyer pays their entry’s bytes. `noLoss: true` is a no-loss lottery: every ticket (tokens or an NFT) goes back to its buyer once the round is drawn, and the winners take only the pot, what is paid into it (`seed`, a sponsor, a share of other sales to "lottery:<key>"), or with `rest` only its places’ prizes; no split, no cut, no `ticketCurrency`. `closesBefore` closes sales that long before each draw (less than `every`), and `timeWeighted: true` (no-loss lotteries only) weighs each purchase by its tickets times the seconds from it to the close of sales, so a ticket held longer has a better chance and a late buyer can’t take most of the pot for the fees alone; a buyer is shown what their purchase weighs before they buy. A place of tokens from a pool reads the pool once, when the draw is asked: `whenShort` says what each gets when it holds less than they win ("inOrder", the default: whole prizes, first place first; "inProportion": each the same share; "nothing"); prizes of 1,000, 500 and 500 from a pool holding 1,700 give 1,000, 500 and 0, or 850, 425 and 425, or nothing. Every place paid from one pool says the same. A raffle takes at most 256 places from its own stash, and an NFT ticket’s rule at most 170 "one of" texts.',
|
|
66
66
|
acts: `players buy tickets (${sol(FEES.player)} SOL); anyone draws and pays (free). Flagged as chance`,
|
|
67
67
|
},
|
|
68
68
|
farms: {
|
|
69
69
|
def: 'farm',
|
|
70
70
|
nested: ['tier', 'rule', 'effect', 'growth'],
|
|
71
|
-
says: 'Staking: players stake a token or NFTs of any kind (the game’s, or any collection its gate names: Core, Token Metadata legacy and pNFT, compressed) and share what lands in the farm’s own rewards vault by weight (`rewards` names its token; fund it with `seed`, a share "farm:<key>" or a schedule that pays it; what lands is released to the seats over the farm’s longest lock, a day at least, so a stake earns only while it stays and nobody takes what was funded before them at once), or earn a fixed `rate` a day each (minted, or from the vault while it lasts, as most NFT staking pays; `shared` makes it the whole farm’s a day, shared by weight, and `halveEvery` halves it), with lock tiers (
|
|
71
|
+
says: 'Staking: players stake a token or NFTs of any kind (the game’s, or any collection its gate names: Core, Token Metadata legacy and pNFT, compressed) and share what lands in the farm’s own rewards vault by weight (`rewards` names its token; fund it with `seed`, a share "farm:<key>" or a schedule that pays it; what lands is released to the seats over the farm’s longest lock, a day at least, so a stake earns only while it stays and nobody takes what was funded before them at once), or earn a fixed `rate` a day each (minted, or from the vault while it lasts, as most NFT staking pays; `shared` makes it the whole farm’s a day, shared by weight, and `halveEvery` halves it), with lock tiers (above x0, up to x100) and up to 32 boosts from what a collectible says (each, and all together, up to +100,000%). `growth` writes points (and a level, from `levels`) on a staked collectible for each day in, at its tier and boosts, with nobody’s signature: on the NFT when the game has authority over it, in its Sheet otherwise; a boost per level, a gate, box odds, a recipe’s need or a grant then read that level. A boost counts a level from when it is written, by a seat’s refresh, so a farm whose boost reads what its own growth writes should have its page or a keeper refresh seats (anyone may, for free). A tier’s `prize` (a prize line, as everywhere; give it a `lock`, or one NFT can come and go and take it each time) makes it a mission: a collectible that stays its lock (topped up, not forfeited) leaves with it, given as it leaves: tokens (minted or from a pool, grown by its boosts), an item, an NFT of a stash, or a chest (`{ "gives": "roll", "box": key }`, settled after the oracle). A tier’s `roll` of a box with `rollsOn` is a roll on the NFT leaving it (success, a stat lost, the NFT lost). A collectible can also boost a token seat. `growth` takes up to 32 (+1 "level" and +2 "affection" a day; without `levels` the points are the level; up to 128 levels each), `gate` up to 32 rules an NFT must all pass, and `runs` ends a farm that long after it goes live (a mission open for a week): it takes no stake at its end or after it, nor one whose lock would run past it, and in a farm that shares what lands, what is not released by the end stays in its vault, as does what a seat forfeits after it, so fund it at least its release span before the end. A tier’s `earnsFor` is a term: a seat earns and grows that long after its lock started, and what it earned by then is exact however late it is next touched: nobody has to act at its end. A farm with a term or an upkeep that pays keeps a record of those times: each new one costs whoever writes it 44 bytes of rent (about 0.00022 SOL), not refunded on leaving, 238,312 at most (when it is full, a stake at a new end time waits until older seats are touched), and a farm idle past very many of them is walked in parts first (`app.walkFarm`, anyone’s, free). A farm that mints pays nothing while the project is not the rewards mint’s authority, and what it would have paid then is not owed later. A `rate` of 0 pays nothing: a mission that only grows or gives prizes. Escrow is the default (the game holds the NFT); Freeze locks it in the wallet, with release dependent on Core handling later issuer metadata changes; Watch leaves it free, earning only while its staker holds it, so one NFT can sit in a farm and its missions at once. Soulbound collectibles require Freeze. A farm of collectibles can keep two times on the NFT, as numbers recipes and rolls move: `lockAttribute` (its lock, written as the time it ends: a recipe that raises it by "-1d" `fromNow` for a price is a way out early, a roll that adds "1d" is jail) and `upkeepAttribute` (fuel: it earns and grows only until that time; a recipe sells it "+1d" `fromNow`, up to `upTo` ahead, and it runs out by itself). A recipe reaches a collectible staked in escrow on its seat. Never edited once open: a new mission is a new farm, added to the spec and published.',
|
|
72
72
|
acts: `the player stakes, unstakes and harvests (${sol(FEES.player)} SOL each); pause and suspend do not stop an unstake; external custody controls still apply`,
|
|
73
73
|
},
|
|
74
74
|
contests: {
|
|
75
75
|
def: 'contest',
|
|
76
76
|
nested: ['split', 'gate', 'rule'],
|
|
77
|
-
says: 'Players back a side; the side that wins shares the pot by stake, after the rake, as the pot stands when the side is named: each winner’s part is written then, whatever order they claim in, and anything sent to the pot after it goes to nobody (burned at the close). The authority names it, or a `deciders` key (a results server) signs it, so the authority’s key never sits on a server. One side and a `minPot` is an all-or-nothing crowdfund (short of it, everyone takes their money back); a wallet gate and a `stake` is a 1v1 wager. A contest whose result is overdue is off and pays everyone back. `prize` gives every winning entry a prize too, when it is claimed (a collectible, tokens, an NFT, a roll of a box); one scarce prize goes to the entry claimed first. `stakeNft` (a rule) makes each entry stake one NFT instead of tokens: the NFTs are the pot, each winner takes its own back and picks its share of the losers’ (`losersEach` caps it: 1 for “winner takes one
|
|
77
|
+
says: 'Players back a side; the side that wins shares the pot by stake, after the rake, as the pot stands when the side is named: each winner’s part is written then, whatever order they claim in, and anything sent to the pot after it goes to nobody (burned at the close). The authority names it, or a `deciders` key (a results server) signs it, so the authority’s key never sits on a server. One side and a `minPot` is an all-or-nothing crowdfund (short of it, everyone takes their money back); a wallet gate and a `stake` is a 1v1 wager. A contest whose result is overdue is off and pays everyone back. `prize` gives every winning entry a prize too, when it is claimed (a collectible, tokens, an NFT, a roll of a box); one scarce prize goes to the entry claimed first. `stakeNft` (a rule) makes each entry stake one NFT of any kind, compressed ones too, instead of tokens: the NFTs are the pot, each winner takes its own back and picks its share of the losers’ (`losersEach` caps it: 1 for “winner takes one”; 0 sends every loser’s NFT to the opener), and the opener gets what the winners leave. Whoever names the winner can also enter; the pot is paid only by each entry’s claim. A prize to each winning entry with no stake and no gate pays one person with many wallets once a wallet.',
|
|
78
78
|
acts: `the authority opens and decides (${sol(FEES.project)} SOL each); players enter (${sol(FEES.player)} SOL). Flagged as betting`,
|
|
79
79
|
},
|
|
80
80
|
auctions: {
|
|
81
81
|
def: 'auction',
|
|
82
82
|
nested: ['split', 'rule'],
|
|
83
|
-
says: 'An English auction, opened when the economy goes live: the highest bid when the time runs out wins one new unit of an item, any Core NFT the owner holds (give its `collection` when it isn’t the game’s), or a roll of one of the game’s boxes (`{ "roll": box }`, a mystery lot; its price goes to the treasury). Bids in any token, SOL included; each reaches the reserve and tops the last by the least raise; the bid it tops goes back to its bidder at once. A late bid moves the end. With no bid the lot goes home; an auction nobody ends waits, and still sells to its high bidder whenever someone ends it.
|
|
83
|
+
says: 'An English auction, opened when the economy goes live: the highest bid when the time runs out wins one new unit of an item, any Core NFT the owner holds (give its `collection` when it isn’t the game’s), or a roll of one of the game’s boxes (`{ "roll": box }`, a mystery lot; its price goes to the treasury). Bids in any token, SOL included; each reaches the reserve and tops the last by the least raise; the bid it tops goes back to its bidder at once. A late bid moves the end (`extendBy`, 10 minutes by default; keep it a minute or more, and the least raise `minRaise` 1% or more, or two wallets topping each other in the last seconds keep every other bid out). With no bid the lot goes home; an auction nobody ends waits, and still sells to its high bidder whenever someone ends it. An end pays the seller and gives the lot in one step; an NFT lot that can’t move yet makes the end wait, its winning bid in the project’s vault, until it can or the project’s keys call the sale off (a lot its collection took back or burned sends the bid back). Any time is taken: the checker says a far end’s date in words. `bidNfts` (a rule) makes a bid a bundle of up to three Core or Token Metadata NFTs instead of tokens (never compressed ones: a bid hands the one it tops back at once, and a compressed NFT’s tree can be changed under it): the most NFTs wins, and the reserve counts NFTs.',
|
|
84
84
|
acts: `players bid (${sol(FEES.player)} SOL each); anyone ends it once it is over (free)`,
|
|
85
85
|
},
|
|
86
86
|
holds: {
|
|
@@ -112,14 +112,14 @@ export const CONVENTIONS = [
|
|
|
112
112
|
'The file is xsolana.json: { "version": 2, "network": "devnet", … }.',
|
|
113
113
|
'Amounts are whole units of their currency: 0.1 is 0.1 SOL. Write "0.1" as text when it must be exact.',
|
|
114
114
|
'Shares are percents with at most two decimals: 2.5 is 2.5%.',
|
|
115
|
-
'Times are durations ("45s", "30m", "6h", "7d", "2w", or seconds); a start is "publish", "publish+7d" or a date like "2026-11-01T18:00:00Z".',
|
|
115
|
+
'Times are durations ("45s", "30m", "6h", "7d", "2w", or seconds, never milliseconds); a start is "publish", "publish+7d" or a date like "2026-11-01T18:00:00Z". The program takes any time: one that lands more than three years out is warned of with its date in words, so check the unit before signing.',
|
|
116
116
|
'Every part has a `key` (lower case, digits, dashes) that other parts name: a box pays from "pool": "house"; a split sends to "pool:house".',
|
|
117
117
|
`A split is any number of shares of a payment, each { "to": "burn" | "treasury" | "pool:<key>" | "lottery:<key>" | "wallet:<address>" | "account:<token account>" | "holder:<collectible address>" | "chosen" | "chosen:<item>", "percent": n }; what they leave goes to the treasury, and so does a share that can’t be paid (a wallet with no account, a collectible nobody holds): a sale never fails over it.`,
|
|
118
118
|
'The treasury, where what a split leaves goes (and a lottery’s cut), is the owner’s own wallet: the program pays its account of whatever currency was paid, and opens it when it is not there. `"treasury": "wallet:<address>"` makes it another wallet instead (a charity, a team’s multisig vault).',
|
|
119
119
|
'A box’s outcomes each have a `chance` (percents adding up to 100) or each a `weight` (whole numbers). An outcome gives one thing, or a bundle of any number ({ "gives": "bundle", "prizes": [{ "gives": "item", … }, { "gives": "pay", … }] }).',
|
|
120
120
|
'An NFT can be the price: a box’s `rollsOn` and `then`, a recipe’s `trade` line, a lottery’s `ticketNft`, a contest’s `stakeNft`, an auction’s `bidNfts`. Where an NFT paid goes (`then`): "burn", "treasury", { "giveTo": address } or { "stash": key }. A payment with an NFT is a runtime call (`loot.payNftIx`), not a part of the spec.',
|
|
121
121
|
'A prize is one line, the same on every route that gives one (a grant’s `prize`, a lottery place’s `prize`, a contest’s `prize`, a mission tier’s `prize`, a recipe’s `out` lines): { "gives": "item", "item": key } (with `"compressed": true`, a compressed NFT of it: no rent each, minted into a tree the setup opens, `compressedTree`), { "gives": "tokens", "amount": n, "from": "mint" (with "currency") or "pool:<key>": a fixed amount, all of it or none }, { "gives": "nft", "stash": key, "nft": address } or { "gives": "roll", "box": key }. All but a roll are given in the transaction that wins them; use a roll only for chance.',
|
|
122
|
-
'A rule (a gate, odds, a boost) is { "item": key (or left out for any), "attribute": "level", "atLeast": 3 } or { …, "equals": "gold" }, or { …, "time": "ahead" } (a time still to come: fuel left) or "passed" (out of jail), or just an attribute for anything. A gate may instead list wallets: { "wallets": [address, …] } (at most 16), or { "list": [address, …] } (any size: the program keeps its root, and each player passes their proof), or a stake: { "staked": { "farm": key, "atLeast": n } } (the wallet has at least n of a farm of tokens’ token staked in it and still locked, staked before the second it is used: launchpad tiers, stake to enter. A stake counts only while its tier’s lock runs, so one stake can’t be unstaked, moved to another wallet and count again; a tier with no lock never passes. The app passes the wallet’s seat and the farm).',
|
|
122
|
+
'A rule (a gate, odds, a boost) is { "item": key (or left out for any), "attribute": "level", "atLeast": 3 } or { …, "equals": "gold" }, or { …, "time": "ahead" } (a time still to come: fuel left) or "passed" (out of jail), or just an attribute for anything. "oneOf" takes any number of texts (a lottery’s NFT ticket at most 170). A gate may instead list wallets: { "wallets": [address, …] } (at most 16), or { "list": [address, …] } (any size: the program keeps its root, and each player passes their proof), or a stake: { "staked": { "farm": key, "atLeast": n } } (the wallet has at least n of a farm of tokens’ token staked in it and still locked, staked before the second it is used: launchpad tiers, stake to enter. A stake counts only while its tier’s lock runs, so one stake can’t be unstaked, moved to another wallet and count again; a tier with no lock never passes. The app passes the wallet’s seat and the farm).',
|
|
123
123
|
'A box, a shop item and a recipe take `opens` and `closes` (times), and `perWallet` (in all) and `perWalletEach` ({ "count": 1, "every": "1d" }) to limit each wallet.',
|
|
124
124
|
'Leave out `id`, `index`, `shopBox` and `live`: they are written when the economy goes live.',
|
|
125
125
|
'Public data only. A spec never holds a private key or a seed phrase (the checker refuses one, E-SECRET).',
|
|
@@ -173,7 +173,7 @@ export const CANNOT = [
|
|
|
173
173
|
'a bid any wallet makes for any NFT that passes a rule (“2 SOL for any NFT of another collection”, “50 gold for any level-10 sword”), escrowed in the project’s vault the authority can’t pay from: `openOfferIx` with `deal: { Buy: { price } }` and `left` (how many), `shares` (a royalty, a cut); a holder sells into it with `sellToOfferIx`, the NFT straight to the bidder, the price to the seller; the bidder takes back what is left with `closeOfferIx`. Made from the game’s page at run time, not in xsolana.json',
|
|
174
174
|
'a listing: a holder puts an NFT of any kind up at a fixed price (`openListingIx` with `deal: { Sell: { price } }`, `app.listForSale`); a buyer pays and takes it in one instruction (`takeListingIx`, `app.buyListing`); the maker takes it home with `closeListingIx`. Made from the game’s page at run time',
|
|
175
175
|
'players trade the game’s collectibles on NFT marketplaces that list Metaplex Core assets, with the collection’s royalty to the owner',
|
|
176
|
-
'an auction a player opens for
|
|
176
|
+
'an auction a player opens for any NFT they hold, signing alone (`openAuctionIx` with an asset lot): escrowed bids, the outbid refunded at once, and at the end the lot to the winner and the price (less any shares) to the seller in one step, so the seller is paid only when the lot arrives; for an NFT of a collection the project does not control, whoever controls that collection can stop it moving or take it back, and the winning bid then waits in the project’s vault until the lot can move or the project’s keys call the sale off (a lot taken back or burned sends the bid back); `bidNfts` makes the bids NFTs (an NFT-for-NFT swap by auction)',
|
|
177
177
|
'a swap against the game: a recipe whose `trade` line takes the player’s NFT into a stash and whose `out` gives a named NFT of a stash (`{ "gives": "nft" }`), or a fixed bundle for another (3 iron → 1 steel); the owner can take stash NFTs out',
|
|
178
178
|
],
|
|
179
179
|
},
|
|
@@ -191,7 +191,7 @@ export const CANNOT = [
|
|
|
191
191
|
words: 'lend lending borrow loan leverage perp perpetual options vault yield strategy collateral liquidation',
|
|
192
192
|
why: 'there are no price oracles or liquidations in loot: its only loan is a fixed-term loan against an NFT',
|
|
193
193
|
nearest: [
|
|
194
|
-
'a fixed-term loan against an NFT between players, with no trust in the game: a lender opens an offer with `deal: { Lend: { amount, repay, lasts } }` for any NFT that passes a rule; a holder borrows (`borrowIx`: the NFT held by the project, `amount` to them); anyone repays `repay` before it is due (`repayIx
|
|
194
|
+
'a fixed-term loan against an NFT between players, with no trust in the game: a lender opens an offer with `deal: { Lend: { amount, repay, lasts } }` for any NFT that passes a rule; a holder borrows (`borrowIx`: the NFT held by the project, `amount` to them); anyone repays `repay` before it is due (`repayIx`: the money to the lender and the NFT home to the borrower in the same step; with `home: false` the money alone, for an NFT that can’t move right now, and anyone sends it home later with `releaseIx`); no repayment is taken for an NFT its collection took back or burned; unpaid once due, anyone sends it to the lender (`releaseIx` with `to` the lender). No grace period, no partial repayment, no cut of the interest; the lender trusts the NFT’s collection not to block or pull back the NFT. The borrower trusts the loan’s token: whoever holds its freeze authority, a pause, a transfer hook or a permanent delegate can make repaying fail, and a lender who holds one can take the NFT when it is due (the program takes such tokens; `app.loanOffer` names each power and its holder, and `app.borrow` needs `acceptRisks`): lend in SOL or a plain token. Made from the game’s page at run time, not in xsolana.json',
|
|
195
195
|
'a farm whose rewards the game sends into its vault (staking rewards, not interest)',
|
|
196
196
|
'otherwise nothing in loot: say so plainly (leverage, perps and options need a protocol built for them)',
|
|
197
197
|
],
|
package/js/economy/model.mjs
CHANGED
|
@@ -43,8 +43,8 @@ export const SIGNATURE_FEE = 5_000n;
|
|
|
43
43
|
export const LAMPORTS_PER_SOL = 1_000_000_000n;
|
|
44
44
|
export const WSOL = 'So11111111111111111111111111111111111111112';
|
|
45
45
|
/** The limits a spec meets: the chain's (the oracle's answer carries 21 accounts; a farm names a tier and a recipe a
|
|
46
|
-
* collectible's place in a byte; Bubblegum keeps a compressed NFT's link in 200 bytes and its name in 32)
|
|
47
|
-
* caps that keep a farm's numbers from overflowing
|
|
46
|
+
* collectible's place in a byte; Bubblegum keeps a compressed NFT's link in 200 bytes and its name in 32), the two
|
|
47
|
+
* caps that keep a farm's numbers from overflowing, and the lists the program still bounds. */
|
|
48
48
|
export const LIMITS = Object.freeze({
|
|
49
49
|
callbackAccounts: 21,
|
|
50
50
|
tiers: 256,
|
|
@@ -52,24 +52,50 @@ export const LIMITS = Object.freeze({
|
|
|
52
52
|
vouchers: 3,
|
|
53
53
|
compressedNameBytes: 32,
|
|
54
54
|
compressedUriBytes: 200,
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// A farm's boost alone, and its boosts together, add at most +100,000% (rules.rs `MAX_BOOST_BPS`); a tier weighs at
|
|
56
|
+
// most x100 (farm.rs `MAX_TIER_BPS`). A box's odds have no such cap: a boosted weight stops at the largest weight.
|
|
57
|
+
maxBoostBps: 10_000_000,
|
|
58
|
+
maxTierBps: 1_000_000,
|
|
57
59
|
// The program's caps on a record's lists (constants.rs, not in the IDL): a spec over one is refused before anything is signed.
|
|
58
|
-
boxEntries: 128,
|
|
59
|
-
boxSlots: 32,
|
|
60
|
-
boxOdds: 32,
|
|
61
|
-
boxFates: 64,
|
|
62
|
-
entryPrizes: 14,
|
|
63
60
|
itemTraits: 64,
|
|
64
61
|
traitKeyBytes: 64,
|
|
65
|
-
recipeLines: 48,
|
|
66
62
|
farmRules: 32,
|
|
67
63
|
growthSteps: 128,
|
|
68
64
|
contestSides: 1024,
|
|
69
65
|
deciders: 32,
|
|
70
66
|
collections: 32,
|
|
71
67
|
lotteryRuns: 64,
|
|
72
|
-
|
|
68
|
+
// How many prizes one outcome of a box gives: the count of what a draw set aside is kept in a byte (catalog.rs).
|
|
69
|
+
entryPrizes: 255,
|
|
70
|
+
// A raffle's places from its own stash, raffled or named: the oracle's answer loads that many (lottery.rs `MOST_OWN`).
|
|
71
|
+
ownStashPlaces: 256,
|
|
72
|
+
// "One of" texts in a lottery's NFT ticket rule: the oracle's answer reads them into room for 4,096 bytes (lottery.rs).
|
|
73
|
+
ticketOneOf: 170,
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* What one answer of the oracle's holds, as measured on the program: a box is refused when it is written
|
|
77
|
+
* (PackTooLarge, or TooManyAccounts) when one answer can't hold a draw of it, a box of one draw included. Each number is
|
|
78
|
+
* the most of that one thing in a box that is otherwise small: `entriesOfNothing` outcomes that give nothing,
|
|
79
|
+
* `entriesOfTokens` outcomes of token amounts, `entriesOfItems` outcomes each of its own item, `slots`, `odds` rules,
|
|
80
|
+
* `entryTokens` token prizes or `entryCollectibles` collectibles in one outcome, `gateTexts` "one of" texts in a box's
|
|
81
|
+
* gate. A box that mixes them fits less. A recipe: `recipePayments` payments in one coin, `recipeCoins` in different
|
|
82
|
+
* coins, `recipePoolOutputs` outputs from a pool, `recipeRaises` raises or needs; past those it is written but never
|
|
83
|
+
* crafts. `recipeRolls`: the rolls whose receipts a transaction's log still holds.
|
|
84
|
+
*/
|
|
85
|
+
export const ANSWER_HOLDS = Object.freeze({
|
|
86
|
+
entriesOfNothing: 128,
|
|
87
|
+
entriesOfTokens: 29,
|
|
88
|
+
entriesOfItems: 18,
|
|
89
|
+
slots: 41,
|
|
90
|
+
odds: 72,
|
|
91
|
+
entryTokens: 16,
|
|
92
|
+
entryCollectibles: 18,
|
|
93
|
+
gateTexts: 340,
|
|
94
|
+
recipePayments: 60,
|
|
95
|
+
recipeCoins: 26,
|
|
96
|
+
recipePoolOutputs: 59,
|
|
97
|
+
recipeRaises: 108,
|
|
98
|
+
recipeRolls: 15,
|
|
73
99
|
});
|
|
74
100
|
/** Attribute names the program writes itself, which a recipe cannot copy and `Set` cannot change (craft.rs, make.rs),
|
|
75
101
|
* and the two a collectible's life keeps (`lifeLines`: its uses and its next use). */
|
|
@@ -194,13 +220,106 @@ export const STOCK_WORDS =
|
|
|
194
220
|
* sold (an open checks the stock); a box only other routes roll (a prize, a mission) never stops a sale.
|
|
195
221
|
*/
|
|
196
222
|
export function whenOutWords(b, { sold = b.active !== false } = {}) {
|
|
223
|
+
if (b.whenOut === 'giveNothing')
|
|
224
|
+
return `When an outcome runs out (an item sold out, a pool short of its prize, a stash empty), ${sold ? 'the box keeps selling while its first draw has anything left, and ' : ''}a roll is on the weights as written: a draw that lands on the outcome that is out is made and gives nothing ("whenOut": "giveNothing"). A paid draw is not refunded and a free roll is over, so tell players a draw can miss, and keep what each outcome gives in stock; anyone who empties a source the box shares makes its rolls miss`;
|
|
197
225
|
if (b.whenOut === 'drawLeft' && !sold)
|
|
198
226
|
return 'When an outcome runs out (an item sold out, a pool short of its prize, a stash empty), a roll draws among the outcomes still there by their written weights, so the others’ chances rise; with nothing left at all, a free roll waits to be asked again and a paid one is refunded';
|
|
199
227
|
if (b.whenOut === 'drawLeft')
|
|
200
228
|
return 'When an outcome runs out (an item sold out, a pool short of its prize, a stash empty), the box keeps selling while its first draw has anything left, and a roll draws among the outcomes still there by their written weights: the others’ chances rise as one runs out, so a player can empty a common outcome to make a rare one likelier (a box selling a rare prize should stop selling instead, the default)';
|
|
201
229
|
if (!sold)
|
|
202
|
-
return `${STOCK_WORDS}. A free roll (a prize, a recipe’s, a mission’s chest) is on the odds as written too: a draw on an outcome that is out misses and gives nothing back (a pack keeps what its other draws won; a roll that drew nothing waits to be asked again
|
|
203
|
-
return 'When an outcome runs out, the box stops selling: it isn’t sold while anything an outcome takes (an item’s units left, a pool’s tokens, a stash’s NFTs) couldn’t cover, at the most any outcome sharing it takes in one draw, this purchase’s draws and those of purchases still waiting for the oracle, so every roll is on the odds as written and nobody can buy their way to a refund on every miss; it sells again once the project restocks or edits the box (an outcome whose odds shrink with stock needs no cover). An outcome emptied another way all the same (another route sharing its pool, item or stash) makes no draw, no other outcome taking its place, and gives back that draw’s worth of the price at the settle; with no draw made at all, the open is refunded';
|
|
230
|
+
return `${STOCK_WORDS}. A free roll (a prize, a recipe’s, a mission’s chest) is on the odds as written too: a draw on an outcome that is out misses and gives nothing back (a pack keeps what its other draws won; a roll that drew nothing waits to be asked again, and is asked again until it hits, so with one outcome dry it always ends on what is left). A pack that should draw among what is left needs "whenOut": "drawLeft"; one where a miss should give nothing, "giveNothing"`;
|
|
231
|
+
return 'When an outcome runs out, the box stops selling: it isn’t sold while anything an outcome takes (an item’s units left, a pool’s tokens, a stash’s NFTs) couldn’t cover, at the most any outcome sharing it takes in one draw, this purchase’s draws and those of purchases still waiting for the oracle, so every roll is on the odds as written and nobody can buy their way to a refund on every miss; it sells again once the project restocks or edits the box (an outcome whose odds shrink with stock needs no cover). An outcome emptied another way all the same (another route sharing its pool, item or stash) makes no draw, no other outcome taking its place, and gives back that draw’s worth of the price at the settle; with no draw made at all, the open is refunded. A roll given by another route (another box, a recipe, a grant, a mission, a lottery’s place) is still given after the box has stopped, and is asked again until it hits, so with one outcome dry it always ends on what is left';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Whether a box stops selling while an outcome is out (`whenOut` "stopSales", the default). */
|
|
235
|
+
export const stopsSales = b => (b.whenOut ?? 'stopSales') === 'stopSales';
|
|
236
|
+
|
|
237
|
+
/** The largest weight an outcome has (a `u32`). */
|
|
238
|
+
export const MOST_WEIGHT = 4_294_967_295n;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* An outcome's weight grown by `bps` basis points (rules.rs `boosted`): rounded down, and never past the largest weight.
|
|
242
|
+
* Counted in BigInt: the largest weight times the largest boost passes 2^53. Two outcomes boosted past the largest
|
|
243
|
+
* weight are equally likely.
|
|
244
|
+
*/
|
|
245
|
+
export function boostedWeight(weight, bps) {
|
|
246
|
+
const grown = (BigInt(weight) * (10_000n + BigInt(bps))) / 10_000n;
|
|
247
|
+
return grown > MOST_WEIGHT ? MOST_WEIGHT : grown;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Box `b`'s weights for a player who passes every one of its odds rules at their most (order.rs): the rules' boosts on
|
|
252
|
+
* one outcome add up, and stop at the largest `u32`; each outcome's weight is then boosted (`boostedWeight`). A boost
|
|
253
|
+
* per point counts its `upTo` points. BigInt weights, outcome by outcome.
|
|
254
|
+
*/
|
|
255
|
+
export function boostedWeights(b) {
|
|
256
|
+
const bps = (b.outcomes ?? []).map(() => 0n);
|
|
257
|
+
for (const o of b.odds ?? []) {
|
|
258
|
+
if (!(o.outcome < bps.length)) continue;
|
|
259
|
+
const add = o.add ?? {};
|
|
260
|
+
const each =
|
|
261
|
+
add.addPercent !== undefined
|
|
262
|
+
? BigInt(Math.round(Number(add.addPercent) * 100))
|
|
263
|
+
: BigInt(Math.round(Number(add.perPointPercent ?? 0) * 100)) * BigInt(add.upTo ?? 0);
|
|
264
|
+
const sum = bps[o.outcome] + each;
|
|
265
|
+
bps[o.outcome] = sum > MOST_WEIGHT ? MOST_WEIGHT : sum;
|
|
266
|
+
}
|
|
267
|
+
return weights(b).map((w, i) => boostedWeight(Math.round(w), bps[i]));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* What places of tokens paid from one pool get when it holds `holds` and they win more (lottery.rs `WhenShort`), as a
|
|
272
|
+
* pull pays them: `runs` are [{ each, places }] in place order (base units, BigInt), `whenShort` the setting they all
|
|
273
|
+
* say. Each run gets [{ each, sent }]. "inOrder": whole prizes, first to last, while what is left covers one;
|
|
274
|
+
* "inProportion": every place the same share of its prize, rounded down; "nothing": no place, and the pool keeps it.
|
|
275
|
+
* Prizes of 1,000, 500 and 500 from a pool holding 1,700: 1,000, 500 and 0; 850, 425 and 425; or nothing.
|
|
276
|
+
*/
|
|
277
|
+
export function shortPays(runs, holds, whenShort = 'inOrder') {
|
|
278
|
+
const owed = runs.reduce((n, r) => n + BigInt(r.each) * BigInt(r.places), 0n);
|
|
279
|
+
let left = BigInt(holds);
|
|
280
|
+
return runs.map(r => {
|
|
281
|
+
const amount = BigInt(r.each);
|
|
282
|
+
const each =
|
|
283
|
+
BigInt(holds) >= owed
|
|
284
|
+
? amount
|
|
285
|
+
: whenShort === 'inProportion'
|
|
286
|
+
? (amount * BigInt(holds)) / owed
|
|
287
|
+
: whenShort === 'nothing'
|
|
288
|
+
? 0n
|
|
289
|
+
: amount;
|
|
290
|
+
const whole = left / (each > 0n ? each : 1n);
|
|
291
|
+
const sent = each * (BigInt(r.places) < whole ? BigInt(r.places) : whole);
|
|
292
|
+
left -= sent;
|
|
293
|
+
return { each, sent };
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* What a purchase of `count` tickets at `at` weighs in a lottery that weighs tickets by time (lottery.rs
|
|
299
|
+
* `time_weighted`): each ticket the seconds from it to the close of sales (`drawsAt` less `closesBefore`), at least 1.
|
|
300
|
+
* Off, a ticket weighs one. BigInt.
|
|
301
|
+
*/
|
|
302
|
+
export function purchaseWeight(count, at, drawsAt, closesBefore = 0, weighted = true) {
|
|
303
|
+
if (!weighted) return BigInt(count);
|
|
304
|
+
const left = BigInt(drawsAt) - BigInt(closesBefore) - BigInt(at);
|
|
305
|
+
return BigInt(count) * (left > 1n ? left : 1n);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* The entry that wins each place of a round (lottery.rs `ticket_of_place`): place `k` is at `first + k × (total /
|
|
310
|
+
* winners)`, rounding the end, and its winner is the entry whose slice of the draw holds that point. `entries` are each
|
|
311
|
+
* purchase's weight in order (BigInt), `first` the answer's point below the total. The entry's index for each place.
|
|
312
|
+
*/
|
|
313
|
+
export function placeWinners(entries, winners, first) {
|
|
314
|
+
const total = entries.reduce((n, w) => n + BigInt(w), 0n);
|
|
315
|
+
if (total === 0n) return [];
|
|
316
|
+
const step = total / BigInt(winners) > 0n ? total / BigInt(winners) : 1n;
|
|
317
|
+
const ends = [];
|
|
318
|
+
entries.reduce((n, w, i) => ((ends[i] = n + BigInt(w)), ends[i]), 0n);
|
|
319
|
+
return Array.from({ length: winners }, (_, k) => {
|
|
320
|
+
const point = (BigInt(first) + BigInt(k) * step) % total;
|
|
321
|
+
return ends.findIndex(end => point < end);
|
|
322
|
+
});
|
|
204
323
|
}
|
|
205
324
|
|
|
206
325
|
/**
|
|
@@ -709,7 +828,7 @@ export function lifeLines(r, m) {
|
|
|
709
828
|
/** A lottery's place as { percent, prize }: a number is a percent of what the round pays from, and no prize. */
|
|
710
829
|
export const placeOf = p =>
|
|
711
830
|
typeof p === 'object' && p !== null
|
|
712
|
-
? { percent: p.percent ?? 0, prize: prizeLine(p.prize), times: p.times ?? 1 }
|
|
831
|
+
? { percent: p.percent ?? 0, prize: prizeLine(p.prize), times: p.times ?? 1, whenShort: p.whenShort }
|
|
713
832
|
: { percent: p, prize: null, times: 1 };
|
|
714
833
|
|
|
715
834
|
/** "pool:house" → { kind: 'pool', key: 'house' }; "burn" → { kind: 'burn' }. */
|