@xgamesfun/solana 0.24.0 → 0.25.0
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 +94 -51
- package/bin/xsolana.mjs +23 -8
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +2 -5
- package/examples/duel-arena.json +1 -4
- package/examples/fair-presale.json +1 -3
- package/examples/hero-trials.json +14 -2
- package/examples/holder-den.json +38 -0
- package/examples/holders-drop.json +105 -0
- package/examples/ideas.json +788 -788
- package/examples/match-predictions.json +24 -0
- package/examples/nft-raffle.json +1 -0
- package/examples/pack-league.json +108 -0
- package/examples/partner-vault.json +19 -3
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +3 -7
- package/examples/stride-sneakers.json +164 -0
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +117 -48
- package/js/approve/ed25519.mjs +5 -1
- package/js/approve/live-check.mjs +83 -26
- package/js/approve/page.html +217 -78
- package/js/approve/page.js +43 -15
- package/js/approve/server.mjs +276 -74
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +15 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +21 -5
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +118 -36
- package/js/chain/mint.js +6 -2
- package/js/chain/pda.js +2 -2
- package/js/chain/programs.js +6 -6
- package/js/chain/rpc.js +40 -14
- package/js/chain/token.js +13 -2
- package/js/chain/token2022.js +55 -16
- package/js/chain/tx.js +32 -22
- package/js/client/accounts.js +270 -80
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +165 -40
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +104 -16
- package/js/client/ix.js +2121 -457
- package/js/client/pda.js +20 -18
- package/js/client/policy.js +12 -5
- package/js/client/random.js +1 -1
- package/js/client/sizes.js +220 -65
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +3873 -690
- package/js/economy/cost.mjs +380 -56
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +461 -110
- package/js/economy/guide.mjs +282 -59
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +60 -19
- package/js/economy/model.mjs +320 -97
- package/js/economy/plan.mjs +1022 -203
- package/js/economy/schema.json +127 -369
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +771 -178
- package/js/economy/txsize.mjs +15 -3
- package/js/economy/validate.mjs +61 -24
- package/js/mcp/server.mjs +42 -14
- package/js/mcp/tools.mjs +293 -80
- package/js/play/index.js +1960 -461
- package/js/play/keys.js +35 -9
- package/js/results/index.js +223 -30
- package/js/results/server.js +230 -0
- package/js/results/wire.js +64 -16
- package/package.json +6 -4
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +30 -16
- package/skills/xsolana-economy/references/after-launch.md +30 -20
- package/skills/xsolana-economy/references/apps.md +47 -36
- package/skills/xsolana-economy/references/sentence-to-spec.md +148 -129
- package/skills/xsolana-economy/references/telling-the-person.md +9 -6
- package/types/chain/core.d.ts +1 -1
- package/types/chain/lookup.d.ts +1 -1
- package/types/client/accounts.d.ts +24 -3
- package/types/client/grant.d.ts +2 -2
- package/types/client/idl/loot.d.ts +22 -0
- package/types/client/ix.d.ts +54 -45
- package/types/client/pda.d.ts +3 -1
- package/types/economy/guide.d.mts +5 -0
- package/types/economy/model.d.mts +48 -3
- package/types/economy/plan.d.mts +44 -2
- package/types/play/index.d.ts +8 -19
- package/types/results/index.d.ts +7 -6
- package/types/results/server.d.ts +126 -0
- package/types/results/wire.d.ts +1 -1
package/js/economy/simulate.mjs
CHANGED
|
@@ -11,13 +11,47 @@
|
|
|
11
11
|
// the rest from then (`stockWalk`). It is a guide, not a promise: the full simulation (js/sim) runs the real program.
|
|
12
12
|
import { entryWidth, LIMIT_RECORD_BYTES, ROUND_RECORD_BYTES } from '../client/index.js';
|
|
13
13
|
import { boxOrderBytes, roundRentOf } from './plan.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
boxBuys,
|
|
16
|
+
COLLECTIBLE_RENT as COLLECTIBLE_RENT_LAMPORTS,
|
|
17
|
+
drawsOf,
|
|
18
|
+
settleUnits,
|
|
19
|
+
holdsRent,
|
|
20
|
+
outcomeParts,
|
|
21
|
+
SETTLE_RENT_UNIT,
|
|
22
|
+
FEES,
|
|
23
|
+
LAMPORTS_PER_SOL,
|
|
24
|
+
ORACLE_FEE,
|
|
25
|
+
RENT_PER_BYTE,
|
|
26
|
+
SIGNATURE_FEE,
|
|
27
|
+
destination,
|
|
28
|
+
listOf,
|
|
29
|
+
keepsPot,
|
|
30
|
+
lotterySplit,
|
|
31
|
+
model,
|
|
32
|
+
placeOf,
|
|
33
|
+
prizeLine,
|
|
34
|
+
prizePool,
|
|
35
|
+
prizeToken,
|
|
36
|
+
farmEndWords,
|
|
37
|
+
lateRunWords,
|
|
38
|
+
prizeWords,
|
|
39
|
+
releaseWords,
|
|
40
|
+
sharesOf,
|
|
41
|
+
toBps,
|
|
42
|
+
toSeconds,
|
|
43
|
+
toTime,
|
|
44
|
+
weights,
|
|
45
|
+
whenOutWords,
|
|
46
|
+
} from './model.mjs';
|
|
15
47
|
|
|
16
48
|
const DAY = 86_400;
|
|
17
49
|
const DEFAULT_PER_DAY = 100;
|
|
50
|
+
/** How many wallets the simulation's crowd is, next to one: what a person with many wallets could take from a grant. */
|
|
51
|
+
const MANY_WALLETS = 100;
|
|
18
52
|
/** Bids a day on an auction the scenario doesn't name (`auction:<key>`); a grant it doesn't name is claimed by nobody. */
|
|
19
53
|
const DEFAULT_BIDS = 10;
|
|
20
|
-
const sol =
|
|
54
|
+
const sol = lamports => Number(lamports) / Number(LAMPORTS_PER_SOL);
|
|
21
55
|
const PLAYER_FEE = sol(FEES.player);
|
|
22
56
|
const PROJECT_FEE = sol(FEES.project);
|
|
23
57
|
const NETWORK_FEE = sol(SIGNATURE_FEE);
|
|
@@ -25,7 +59,15 @@ const NETWORK_FEE = sol(SIGNATURE_FEE);
|
|
|
25
59
|
const ORACLE = sol(ORACLE_FEE);
|
|
26
60
|
/** A lottery purchase's entry in its round: the bytes the buyer adds (not refunded: they go to the treasury when the
|
|
27
61
|
* round closes), with a wallet kept for each share the buyer's own entry names (lottery.rs `entry_width`). */
|
|
28
|
-
const ticketLine =
|
|
62
|
+
const ticketLine = l =>
|
|
63
|
+
sol(
|
|
64
|
+
BigInt(
|
|
65
|
+
entryWidth(
|
|
66
|
+
lotterySplit(l).some(x => String(x.to).startsWith('chosen')),
|
|
67
|
+
Boolean(l.ticketNft),
|
|
68
|
+
),
|
|
69
|
+
) * RENT_PER_BYTE,
|
|
70
|
+
);
|
|
29
71
|
/** A collectible's Core account, paid from the SOL the order holds: its rent and Core's creation fee (model.mjs). */
|
|
30
72
|
const COLLECTIBLE_RENT = sol(COLLECTIBLE_RENT_LAMPORTS);
|
|
31
73
|
/** What a collectible a recipe burns gives back: its rent less one byte's (Core leaves a 1-byte account that keeps
|
|
@@ -33,12 +75,18 @@ const COLLECTIBLE_RENT = sol(COLLECTIBLE_RENT_LAMPORTS);
|
|
|
33
75
|
const BURN_REFUND = 0.00096;
|
|
34
76
|
/** What a grant's claim adds to its lists: the wallet's limit record (its first claim) and a record of the round paid
|
|
35
77
|
* (none for an open grant's claim). Back to the owner when the shards close after the grant. */
|
|
36
|
-
const claimRecordsRent =
|
|
78
|
+
const claimRecordsRent = g =>
|
|
79
|
+
sol(
|
|
80
|
+
BigInt(
|
|
81
|
+
LIMIT_RECORD_BYTES +
|
|
82
|
+
((g.vouchers ?? []).length || (g.keys ?? []).length || (g.list ?? []).length ? ROUND_RECORD_BYTES : 0),
|
|
83
|
+
) * RENT_PER_BYTE,
|
|
84
|
+
);
|
|
37
85
|
/** A wallet's token account, which a grant that pays tokens opens from its funds when the winner has none. */
|
|
38
86
|
const TOKEN_ACCOUNT_RENT = sol((165n + 128n) * RENT_PER_BYTE);
|
|
39
87
|
/** An order of box `b`'s rent (`orderBytes`), returned at settle or refund: an open's (`opened`, sized as `open_box` sizes it), or a roll's made inside another call. */
|
|
40
88
|
const orderRent = (b, m, opened = false) => sol((BigInt(boxOrderBytes(b, m, 1, { opened })) + 128n) * RENT_PER_BYTE);
|
|
41
|
-
const num =
|
|
89
|
+
const num = v => (v === undefined || v === null ? 0 : Number(v));
|
|
42
90
|
export const round = (x, digits = 9) => (Number.isFinite(x) ? Number(x.toFixed(digits)) : x);
|
|
43
91
|
|
|
44
92
|
/**
|
|
@@ -48,7 +96,19 @@ export const round = (x, digits = 9) => (Number.isFinite(x) ? Number(x.toFixed(d
|
|
|
48
96
|
* `heldIs`: the SOL an order holds and its account, a ticket purchase's record) and, for SOL, the wrapped-SOL account a
|
|
49
97
|
* purchase opens and closes again. A price that comes back (`priceBack`: a no-loss ticket) is held, not spent.
|
|
50
98
|
*/
|
|
51
|
-
function realCostOf({
|
|
99
|
+
function realCostOf({
|
|
100
|
+
price,
|
|
101
|
+
token,
|
|
102
|
+
isSol,
|
|
103
|
+
oracle = 0,
|
|
104
|
+
signatures = 1,
|
|
105
|
+
rent = 0,
|
|
106
|
+
rentIs = '',
|
|
107
|
+
held = 0,
|
|
108
|
+
heldIs = '',
|
|
109
|
+
priceBack = false,
|
|
110
|
+
atLeast = false,
|
|
111
|
+
}) {
|
|
52
112
|
const extrasSol = PLAYER_FEE + oracle + signatures * NETWORK_FEE + rent;
|
|
53
113
|
const hold = extrasSol - rent + held + (isSol ? price + TOKEN_ACCOUNT_RENT : 0);
|
|
54
114
|
const spent = isSol && !priceBack ? price : 0;
|
|
@@ -107,7 +167,12 @@ export function salePrices(part, perDay, days, fallStartsAfter = 0, budget = wal
|
|
|
107
167
|
if (!part.fall && !growth && step > 0) {
|
|
108
168
|
const below = Math.min(count - 1, Math.max(0, Math.ceil((PRICE_CAP - listed) / step) - 1));
|
|
109
169
|
const total = listed * (1 + below) + (step * below * (below + 1)) / 2 + PRICE_CAP * (count - 1 - below);
|
|
110
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
count,
|
|
172
|
+
average: total / count,
|
|
173
|
+
first: listed,
|
|
174
|
+
last: count === 1 ? listed : below === count - 1 ? listed + below * step : PRICE_CAP,
|
|
175
|
+
};
|
|
111
176
|
}
|
|
112
177
|
// On the sale clock (the default) a sale restarts the countdown, so after the first sale each sale comes `gap` after
|
|
113
178
|
// the last, and one closer than a step of the fall never lowers it. On a shared clock the steps keep their own time.
|
|
@@ -119,7 +184,10 @@ export function salePrices(part, perDay, days, fallStartsAfter = 0, budget = wal
|
|
|
119
184
|
let first;
|
|
120
185
|
let paid = listed;
|
|
121
186
|
for (let k = 0; k < count; k++) {
|
|
122
|
-
if (budget.left-- <= 0)
|
|
187
|
+
if (budget.left-- <= 0)
|
|
188
|
+
throw new Error(
|
|
189
|
+
`The scenario has ${count.toLocaleString('en-US')} sales of “${part.name ?? part.key}”, whose price rises or falls, and the quick simulation walks at most ${MAX_WALK.toLocaleString('en-US')} sales of prices that still move, for all boxes and items together. Give the scenario fewer days or fewer a day.`,
|
|
190
|
+
);
|
|
123
191
|
const at = (k + 0.5) * gap;
|
|
124
192
|
paid = price;
|
|
125
193
|
const steps = part.fall && every && at > since ? Math.floor((at - since) / every) : 0;
|
|
@@ -146,17 +214,44 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
146
214
|
const scenarioDays = days > 0 ? days : 1;
|
|
147
215
|
const budget = walkBudget();
|
|
148
216
|
const perDay = (kind, key) => num(m.scenario.perDay?.[`${kind}:${key}`] ?? DEFAULT_PER_DAY);
|
|
149
|
-
const symbol =
|
|
217
|
+
const symbol = key => m.tokens.get(key)?.symbol ?? key;
|
|
150
218
|
|
|
151
219
|
// Per pool: fixed money in and out per day, and a rate per day for what takes a share of what it holds.
|
|
152
|
-
const pools = new Map(
|
|
153
|
-
|
|
154
|
-
|
|
220
|
+
const pools = new Map(
|
|
221
|
+
m.pools.map(p => [
|
|
222
|
+
p.key,
|
|
223
|
+
{ key: p.key, currency: p.currency, seed: num(p.seed), in: [], out: [], rate: [], variance: 0 },
|
|
224
|
+
]),
|
|
225
|
+
);
|
|
226
|
+
// Each lottery's pot is an account of its own, counted here as a pool named "lottery:<key>" (one that pays each round
|
|
227
|
+
// from its own ticket money keeps none).
|
|
228
|
+
for (const l of m.lotteries.filter(keepsPot))
|
|
229
|
+
pools.set(`lottery:${l.key}`, {
|
|
230
|
+
key: `lottery:${l.key}`,
|
|
231
|
+
pot: l.name,
|
|
232
|
+
currency: l.currency ?? game,
|
|
233
|
+
seed: num(l.seed),
|
|
234
|
+
in: [],
|
|
235
|
+
out: [],
|
|
236
|
+
rate: [],
|
|
237
|
+
variance: 0,
|
|
238
|
+
});
|
|
155
239
|
// Each farm's rewards vault is an account of its own too, counted as a pool named "farm:<key>".
|
|
156
|
-
for (const f of m.farms)
|
|
240
|
+
for (const f of m.farms)
|
|
241
|
+
pools.set(`farm:${f.key}`, {
|
|
242
|
+
key: `farm:${f.key}`,
|
|
243
|
+
farm: f.name,
|
|
244
|
+
currency: f.rewardsToken,
|
|
245
|
+
seed: num(f.seed),
|
|
246
|
+
in: [],
|
|
247
|
+
out: [],
|
|
248
|
+
rate: [],
|
|
249
|
+
variance: 0,
|
|
250
|
+
});
|
|
157
251
|
const flowIn = (pool, by, amount) => amount && pools.get(pool)?.in.push({ by, perDay: amount });
|
|
158
252
|
const flowOut = (pool, by, amount) => amount && pools.get(pool)?.out.push({ by, perDay: amount });
|
|
159
|
-
const rateOut = (pool, by, perDayShare, to) =>
|
|
253
|
+
const rateOut = (pool, by, perDayShare, to) =>
|
|
254
|
+
perDayShare && pools.get(pool)?.rate.push({ by, perDay: perDayShare, ...(to ? { to } : {}) });
|
|
160
255
|
|
|
161
256
|
const players = { pay: {}, get: {}, fees: 0, network: 0, rent: 0, oracle: 0 };
|
|
162
257
|
const creator = { treasury: {}, fees: 0, network: 0, oracle: 0 };
|
|
@@ -173,7 +268,7 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
173
268
|
const prizeTakes = [];
|
|
174
269
|
/** The SOL a prize's order holds while it waits (its rent), back to the winner at settle. Whoever settles it pays a
|
|
175
270
|
* collectible's rent. */
|
|
176
|
-
const orderHeld =
|
|
271
|
+
const orderHeld = box => orderRent(m.box.get(box), m);
|
|
177
272
|
/**
|
|
178
273
|
* A route gives prize line `line` `times` a day (prize.rs), `payer` ({ rent, oracle }) paying what it opens: a
|
|
179
274
|
* collectible's rent, the winner's token account (the bound, not the average), a roll's order (back to the winner at
|
|
@@ -189,7 +284,7 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
189
284
|
if (worth !== undefined) add(players.get, game, times * num(worth));
|
|
190
285
|
} else if (line.gives === 'tokens') {
|
|
191
286
|
each = TOKEN_ACCOUNT_RENT;
|
|
192
|
-
const token = prizePool(line) ? m.pool.get(prizePool(line))?.currency : line.currency ?? game;
|
|
287
|
+
const token = prizePool(line) ? m.pool.get(prizePool(line))?.currency : (line.currency ?? game);
|
|
193
288
|
if (prizePool(line)) flowOut(prizePool(line), by, times * num(line.amount));
|
|
194
289
|
add(players.get, token, times * num(line.amount));
|
|
195
290
|
} else if (line.gives === 'nft') {
|
|
@@ -203,18 +298,26 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
203
298
|
return each;
|
|
204
299
|
};
|
|
205
300
|
/** Seconds from going live to a fall's start. */
|
|
206
|
-
const fallStart =
|
|
301
|
+
const fallStart = part =>
|
|
302
|
+
part.fall?.start && part.fall.start !== 'publish' ? Math.max(0, toTime(part.fall.start, now) - now) : 0;
|
|
207
303
|
/** What a rising or falling price really comes to, in words. */
|
|
208
304
|
const priceNote = (part, sold) => {
|
|
209
305
|
if (!part.rise && !part.fall) return;
|
|
210
306
|
const shared = part.fall?.clock === 'shared';
|
|
211
|
-
const how =
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
307
|
+
const how =
|
|
308
|
+
part.rise && part.fall
|
|
309
|
+
? `rises with each sale and falls with time${shared ? ' on a clock of its own, sales or not' : ', the countdown restarting at each sale'}`
|
|
310
|
+
: part.rise
|
|
311
|
+
? 'rises with each sale'
|
|
312
|
+
: shared
|
|
313
|
+
? 'falls on a clock of its own, sales or not (a sale keeps the price at what was paid, and the next step still comes on time)'
|
|
314
|
+
: 'falls while nobody buys (a sale keeps the price at what was paid and restarts the countdown)';
|
|
216
315
|
const said = `“${part.name}”’s price ${how}: over the scenario’s ${sold.count} sales it averages ${round(sold.average, 6)} (the first ${round(sold.first, 6)}, the last ${round(sold.last, 6)}), and the money below counts that.`;
|
|
217
|
-
notes.push(
|
|
316
|
+
notes.push(
|
|
317
|
+
sold.last >= 10 * num(part.price)
|
|
318
|
+
? `${said} It ends ${sold.last >= PRICE_CAP ? 'beyond any price' : `${round(sold.last / num(part.price), 1)} times`} where it started: players stop buying long before that. A smaller growth keeps it in reach.`
|
|
319
|
+
: said,
|
|
320
|
+
);
|
|
218
321
|
};
|
|
219
322
|
|
|
220
323
|
/** Sends `amount` of `token` a day to one destination (model `destination`): a pool, a lottery's pot, a farm's rewards vault, a burn, the treasury or an account. */
|
|
@@ -224,7 +327,14 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
224
327
|
else if (d.kind === 'farm') flowIn(`farm:${d.key}`, by, amount);
|
|
225
328
|
else if (d.kind === 'burn') add(burned, token, amount);
|
|
226
329
|
else if (d.kind === 'treasury') add(creator.treasury, token, amount);
|
|
227
|
-
else if (d.kind === 'chosen')
|
|
330
|
+
else if (d.kind === 'chosen')
|
|
331
|
+
add(
|
|
332
|
+
accounts,
|
|
333
|
+
d.key
|
|
334
|
+
? `the holder of the ${m.item.get(d.key)?.name ?? d.key} the buyer names`
|
|
335
|
+
: `the wallet the ${String(by).startsWith('payment:') ? 'payer' : 'buyer'} names`,
|
|
336
|
+
amount,
|
|
337
|
+
);
|
|
228
338
|
else add(accounts, `${d.kind}:${d.key}`, amount);
|
|
229
339
|
};
|
|
230
340
|
/** Where `amount` of `token` goes under `split`, per day: its shares (`sendTo`), and the rest to the treasury (or into `restTo`, a pool). */
|
|
@@ -256,14 +366,15 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
256
366
|
const draws = drawsOf(b);
|
|
257
367
|
// What each stock holds at the start, and what one draw of each outcome takes from it.
|
|
258
368
|
const left = new Map();
|
|
259
|
-
const stockOf =
|
|
369
|
+
const stockOf = x => {
|
|
260
370
|
const supply = Number(m.item.get(x.item)?.supply ?? 0);
|
|
261
371
|
if (x.gives === 'item' && supply > 0 && !b.stockOdds) return [`item:${x.item}`, supply];
|
|
262
|
-
if (x.gives === 'stash' && m.stash.get(x.stash))
|
|
372
|
+
if (x.gives === 'stash' && m.stash.get(x.stash))
|
|
373
|
+
return [`stash:${x.stash}`, (m.stash.get(x.stash).nfts ?? []).length];
|
|
263
374
|
if (x.gives === 'nft') return [`nft:${x.nft}`, 1];
|
|
264
375
|
return null;
|
|
265
376
|
};
|
|
266
|
-
const needs = (b.outcomes ?? []).map(
|
|
377
|
+
const needs = (b.outcomes ?? []).map(o => {
|
|
267
378
|
const need = new Map();
|
|
268
379
|
for (const [key, holds] of outcomeParts(o).map(stockOf).filter(Boolean)) {
|
|
269
380
|
left.set(key, holds);
|
|
@@ -273,49 +384,89 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
273
384
|
});
|
|
274
385
|
if (!left.size || !(opens > 0)) return null;
|
|
275
386
|
const stops = b.whenOut !== 'drawLeft';
|
|
276
|
-
const
|
|
277
|
-
|
|
387
|
+
const out = i => ({
|
|
388
|
+
outcome: i,
|
|
389
|
+
gives: (b.outcomes[i].gives === 'bundle'
|
|
390
|
+
? (b.outcomes[i].prizes ?? []).map(x => x.item ?? x.stash ?? x.gives)
|
|
391
|
+
: [b.outcomes[i].item ?? b.outcomes[i].stash ?? b.outcomes[i].gives]
|
|
392
|
+
).join(' + '),
|
|
393
|
+
});
|
|
394
|
+
if (stops) {
|
|
395
|
+
// A box that stops selling covers each purchase by stock (order.rs `covers`): each item, stash or named NFT must
|
|
396
|
+
// hold the most any outcome takes of it in one draw, times the draws of a purchase; it stops once one can't.
|
|
397
|
+
const most = new Map();
|
|
398
|
+
for (const need of needs) for (const [k, n] of need) most.set(k, Math.max(most.get(k) ?? 0, n));
|
|
399
|
+
const short = [...most].filter(([k, n]) => left.get(k) < n * draws).map(([k]) => k);
|
|
400
|
+
const by = keys =>
|
|
401
|
+
w
|
|
402
|
+
.map((_, i) => i)
|
|
403
|
+
.filter(i => keys.some(k => needs[i].has(k)))
|
|
404
|
+
.map(out);
|
|
405
|
+
if (short.length) return { whenOut: 'stopSales', stopsSellingInDays: 0, by: by(short) };
|
|
406
|
+
const sum = w.reduce((n, x) => n + x, 0);
|
|
407
|
+
if (!sum) return null;
|
|
408
|
+
const rate = new Map();
|
|
409
|
+
w.forEach((x, i) => needs[i].forEach((n, k) => rate.set(k, (rate.get(k) ?? 0) + (opens * draws * x * n) / sum)));
|
|
410
|
+
const next = [...rate]
|
|
411
|
+
.filter(([, r]) => r > 0)
|
|
412
|
+
.map(([k, r]) => ({ k, t: (left.get(k) - most.get(k) * draws + 1) / r }))
|
|
413
|
+
.sort((a, z) => a.t - z.t)[0];
|
|
414
|
+
return next && next.t <= days
|
|
415
|
+
? { whenOut: 'stopSales', stopsSellingInDays: round(next.t, 4), by: by([next.k]) }
|
|
416
|
+
: null;
|
|
417
|
+
}
|
|
418
|
+
const there = i => [...needs[i]].every(([k, n]) => left.get(k) >= n);
|
|
419
|
+
const chances = alive => {
|
|
278
420
|
const sum = alive.reduce((n, i) => n + w[i], 0);
|
|
279
421
|
return w.map((x, i) => (sum && alive.includes(i) ? round((x / sum) * 100, 6) : 0));
|
|
280
422
|
};
|
|
281
423
|
let alive = w.map((_, i) => i).filter(there);
|
|
282
424
|
let day = 0;
|
|
283
425
|
const runsOut = [];
|
|
284
|
-
const
|
|
285
|
-
const gone = w.map((_, i) => i).filter((i) => !alive.includes(i));
|
|
426
|
+
const gone = w.map((_, i) => i).filter(i => !alive.includes(i));
|
|
286
427
|
if (gone.length) {
|
|
287
|
-
if (stops) return { whenOut: 'stopSales', stopsSellingInDays: 0, by: gone.map(out) };
|
|
288
428
|
runsOut.push({ inDays: 0, outcomes: gone.map(out), chancesAfter: chances(alive) });
|
|
289
429
|
}
|
|
290
430
|
while (alive.length) {
|
|
291
431
|
const sum = alive.reduce((n, i) => n + w[i], 0);
|
|
292
432
|
// Each stock's take a day, by the outcomes still there.
|
|
293
433
|
const rate = new Map();
|
|
294
|
-
for (const i of alive)
|
|
434
|
+
for (const i of alive)
|
|
435
|
+
for (const [k, n] of needs[i]) rate.set(k, (rate.get(k) ?? 0) + (opens * draws * w[i] * n) / sum);
|
|
295
436
|
const next = [...rate].map(([k, r]) => ({ k, t: left.get(k) / r })).sort((a, z) => a.t - z.t)[0];
|
|
296
437
|
if (!next || day + next.t > days) break;
|
|
297
438
|
day += next.t;
|
|
298
439
|
for (const [k, r] of rate) left.set(k, Math.max(0, left.get(k) - r * next.t));
|
|
299
440
|
left.set(next.k, 0);
|
|
300
|
-
const ended = alive.filter(
|
|
441
|
+
const ended = alive.filter(i => !there(i));
|
|
301
442
|
alive = alive.filter(there);
|
|
302
|
-
if (stops) return { whenOut: 'stopSales', stopsSellingInDays: round(day, 4), by: ended.map(out) };
|
|
303
443
|
runsOut.push({ inDays: round(day, 4), outcomes: ended.map(out), chancesAfter: chances(alive) });
|
|
304
444
|
}
|
|
305
|
-
|
|
306
|
-
|
|
445
|
+
return runsOut.length
|
|
446
|
+
? { whenOut: 'drawLeft', runsOut, ...(alive.length ? {} : { stopsSellingInDays: runsOut.at(-1).inDays }) }
|
|
447
|
+
: null;
|
|
307
448
|
};
|
|
308
449
|
/** The walk of a box's stock in words (`stockWalk`). */
|
|
309
450
|
const stockNote = (b, walk) => {
|
|
310
|
-
const named =
|
|
451
|
+
const named = list => list.map(x => `outcome ${x.outcome} (${x.gives})`).join(', ');
|
|
311
452
|
if (walk.whenOut === 'stopSales')
|
|
312
|
-
return `“${b.name}” stops selling ${walk.stopsSellingInDays ? `in about ${walk.stopsSellingInDays} days` : 'from the start'}, when ${named(walk.by)} runs out (its own opens alone): it isn’t sold while
|
|
313
|
-
const steps = walk.runsOut.map(
|
|
453
|
+
return `“${b.name}” stops selling ${walk.stopsSellingInDays ? `in about ${walk.stopsSellingInDays} days` : 'from the start'}, when ${named(walk.by)} runs out (its own opens alone): it isn’t sold while an item, a stash or a named NFT couldn’t cover, at the most any outcome takes of it in one draw, the draws of a purchase and of those still waiting for the oracle ("whenOut": "stopSales"), so every roll is on the odds as written. Restock it, or edit the box, to sell again; "drawLeft" would keep selling, the rest’s chances rising.`;
|
|
454
|
+
const steps = walk.runsOut.map(
|
|
455
|
+
r =>
|
|
456
|
+
`${named(r.outcomes)} ${r.inDays ? `in about ${r.inDays} days` : 'from the start'}${
|
|
457
|
+
r.chancesAfter.some(c => c > 0)
|
|
458
|
+
? `, then ${r.chancesAfter
|
|
459
|
+
.map((c, i) => (c > 0 ? `outcome ${i} ${round(c, 2)}%` : null))
|
|
460
|
+
.filter(Boolean)
|
|
461
|
+
.join(', ')}`
|
|
462
|
+
: ''
|
|
463
|
+
}`,
|
|
464
|
+
);
|
|
314
465
|
return `“${b.name}” keeps selling as its outcomes run out ("whenOut": "drawLeft"; its own opens alone): ${steps.join('; ')}${walk.stopsSellingInDays !== undefined ? `; with nothing left, it stops selling` : ''}. Each roll draws among what is left by the written weights, so a player can empty a common outcome to make a rare one likelier; "stopSales" keeps the odds as written.`;
|
|
315
466
|
};
|
|
316
467
|
|
|
317
468
|
// ---- boxes
|
|
318
|
-
const boxes = m.boxes.map(
|
|
469
|
+
const boxes = m.boxes.map(b => {
|
|
319
470
|
const token = b.currency ?? game;
|
|
320
471
|
const opens = perDay('box', b.key);
|
|
321
472
|
// A price that rises or falls is counted at what the scenario's opens really pay, on average.
|
|
@@ -351,7 +502,9 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
351
502
|
} else if (o.gives === 'stash' || o.gives === 'nft') {
|
|
352
503
|
// A stash's NFT has whatever worth its collection has: the simulation counts how fast it is given away.
|
|
353
504
|
Object.assign(row, { stash: o.stash, worth: null, ...(o.nft ? { nft: o.nft } : {}) });
|
|
354
|
-
unpriced.push(
|
|
505
|
+
unpriced.push(
|
|
506
|
+
o.gives === 'nft' ? `the NFT ${String(o.nft).slice(0, 4)}… of “${o.stash}”` : `the NFTs of “${o.stash}”`,
|
|
507
|
+
);
|
|
355
508
|
// A box switched off is opened by nobody: only the prizes that roll it take from the stash.
|
|
356
509
|
if (b.active !== false) stashTakes.push({ stash: o.stash, perDay: opens * p, by: `box:${b.key}` });
|
|
357
510
|
} else if (o.gives === 'roll') {
|
|
@@ -364,15 +517,15 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
364
517
|
};
|
|
365
518
|
const outcomes = (b.outcomes ?? []).map((o, i) => {
|
|
366
519
|
const p = w[i] / total;
|
|
367
|
-
const parts = outcomeParts(o).map(
|
|
520
|
+
const parts = outcomeParts(o).map(part => partRow(part, p));
|
|
368
521
|
if (o.gives === 'bundle') return { gives: 'bundle', chance: round(p * 100, 6), prizes: parts };
|
|
369
522
|
return parts[0] ?? { gives: o.gives, chance: round(p * 100, 6) };
|
|
370
523
|
});
|
|
371
524
|
// Every part of every outcome, each with its outcome's chance.
|
|
372
|
-
const partsOf = outcomes.flatMap(
|
|
525
|
+
const partsOf = outcomes.flatMap(o => (o.gives === 'bundle' ? o.prizes : [o]));
|
|
373
526
|
// How much the prizes swing from day to day, per pool: opens × (E[prize²] − E[prize]²).
|
|
374
|
-
for (const key of new Set(partsOf.filter(
|
|
375
|
-
const rows = partsOf.filter(
|
|
527
|
+
for (const key of new Set(partsOf.filter(o => o.gives === 'pay').map(o => o.pool))) {
|
|
528
|
+
const rows = partsOf.filter(o => o.gives === 'pay' && o.pool === key);
|
|
376
529
|
const mean = rows.reduce((n, o) => n + (o.chance / 100) * o.amount, 0);
|
|
377
530
|
const square = rows.reduce((n, o) => n + (o.chance / 100) * o.amount ** 2, 0);
|
|
378
531
|
if (pools.has(key)) pools.get(key).variance += opens * (square - mean ** 2);
|
|
@@ -388,14 +541,27 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
388
541
|
const signatures = bought ? 1 : 2;
|
|
389
542
|
playerAction(opens, signatures);
|
|
390
543
|
if (!bought) players.oracle += opens * ORACLE;
|
|
391
|
-
const collectibles =
|
|
544
|
+
const collectibles =
|
|
545
|
+
drawsOf(b) *
|
|
546
|
+
partsOf
|
|
547
|
+
.filter(o => holdsRent(o, m) && m.item.get(o.item)?.kind !== 'normal')
|
|
548
|
+
.reduce((n, o) => n + o.chance / 100, 0);
|
|
392
549
|
players.rent += opens * collectibles * COLLECTIBLE_RENT;
|
|
393
550
|
const known = !unpriced.length && !Object.keys(other).length;
|
|
394
551
|
// What one open really takes from the player's wallet: the price, the platform's fee, the oracle's and the network's.
|
|
395
552
|
const isSol = Boolean(m.tokens.get(token)?.wsol);
|
|
396
553
|
const oracle = bought ? 0 : ORACLE;
|
|
397
554
|
// A collectible's rent comes out of the SOL the open lends its settle key: on average, the chance of one.
|
|
398
|
-
const realCost = realCostOf({
|
|
555
|
+
const realCost = realCostOf({
|
|
556
|
+
price,
|
|
557
|
+
token,
|
|
558
|
+
isSol,
|
|
559
|
+
oracle,
|
|
560
|
+
signatures,
|
|
561
|
+
rent: collectibles * COLLECTIBLE_RENT,
|
|
562
|
+
rentIs: collectibles < 1 ? 'the collectible’s rent, when it gives one' : 'the collectible’s rent',
|
|
563
|
+
held: bought ? 0 : sol(SETTLE_RENT_UNIT) * settleUnits(b, m) + orderRent(b, m, true),
|
|
564
|
+
});
|
|
399
565
|
const extrasSol = realCost.extrasSol;
|
|
400
566
|
priceNote(b, sold);
|
|
401
567
|
// What it gives that runs out, as the box says (`whenOut`): it stops selling, or draws among what is left.
|
|
@@ -418,7 +584,18 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
418
584
|
oracleFee: oracle,
|
|
419
585
|
realCost,
|
|
420
586
|
// What it gives can run out, and what the box does then (`whenOut`, `whenOutWords`).
|
|
421
|
-
...(!bought &&
|
|
587
|
+
...(!bought &&
|
|
588
|
+
outcomes
|
|
589
|
+
.flatMap(o => (o.gives === 'bundle' ? o.prizes : [o]))
|
|
590
|
+
.some(
|
|
591
|
+
o =>
|
|
592
|
+
o.gives === 'pay' ||
|
|
593
|
+
o.gives === 'stash' ||
|
|
594
|
+
o.gives === 'nft' ||
|
|
595
|
+
(o.gives === 'item' && Number(m.item.get(o.item)?.supply ?? 0) > 0),
|
|
596
|
+
)
|
|
597
|
+
? { limitedStock: true }
|
|
598
|
+
: {}),
|
|
422
599
|
...(!bought ? { whenOut: b.whenOut === 'drawLeft' ? 'drawLeft' : 'stopSales' } : {}),
|
|
423
600
|
...(walk ? { stock: walk } : {}),
|
|
424
601
|
...(isSol && known && price ? { returnOfRealCostPercent: round((value / (price + extrasSol)) * 100, 4) } : {}),
|
|
@@ -427,8 +604,8 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
427
604
|
|
|
428
605
|
// ---- items sold in the shop
|
|
429
606
|
const items = m.shop
|
|
430
|
-
.map(
|
|
431
|
-
.map(
|
|
607
|
+
.map(b => ({ ...m.item.get(b.shopOf), shopBox: b.key }))
|
|
608
|
+
.map(it => {
|
|
432
609
|
const sales = perDay('item', it.key);
|
|
433
610
|
const sold = salePrices(it, sales, days, fallStart(it), budget);
|
|
434
611
|
const price = sold.average;
|
|
@@ -442,32 +619,73 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
442
619
|
if (collectible) players.rent += sales * COLLECTIBLE_RENT;
|
|
443
620
|
const listed = num(it.price);
|
|
444
621
|
const rises = it.rise ? listed * (num(it.rise.growthPercent) / 100) + num(it.rise.step) : 0;
|
|
445
|
-
const realCost = realCostOf({
|
|
446
|
-
|
|
622
|
+
const realCost = realCostOf({
|
|
623
|
+
price: listed,
|
|
624
|
+
token: game,
|
|
625
|
+
isSol: Boolean(m.tokens.get(game)?.wsol),
|
|
626
|
+
signatures: 1,
|
|
627
|
+
rent: collectible ? COLLECTIBLE_RENT : 0,
|
|
628
|
+
rentIs: 'the collectible’s rent',
|
|
629
|
+
held: 0,
|
|
630
|
+
});
|
|
631
|
+
return {
|
|
632
|
+
key: it.key,
|
|
633
|
+
name: it.name,
|
|
634
|
+
price: listed,
|
|
635
|
+
...(price !== listed ? { averagePrice: round(price) } : {}),
|
|
636
|
+
token: game,
|
|
637
|
+
salesPerDay: sales,
|
|
638
|
+
...(rises ? { nextPrice: round(listed + rises) } : {}),
|
|
639
|
+
...(it.supply && sales > 0 ? { soldOutInDays: round(it.supply / sales, 2) } : {}),
|
|
640
|
+
realCost,
|
|
641
|
+
};
|
|
447
642
|
});
|
|
448
643
|
|
|
449
644
|
// ---- payments
|
|
450
|
-
const payments = m.payments.map(
|
|
645
|
+
const payments = m.payments.map(p => {
|
|
451
646
|
const token = p.currency ?? game;
|
|
452
647
|
const count = perDay('payment', p.key);
|
|
453
648
|
carryOut(p.split, token, count * num(p.amount), `payment:${p.key}`);
|
|
454
649
|
add(players.pay, token, count * num(p.amount));
|
|
455
650
|
playerAction(count);
|
|
456
|
-
const realCost = realCostOf({
|
|
457
|
-
|
|
651
|
+
const realCost = realCostOf({
|
|
652
|
+
price: num(p.amount),
|
|
653
|
+
token,
|
|
654
|
+
isSol: Boolean(m.tokens.get(token)?.wsol),
|
|
655
|
+
atLeast: Boolean(p.any),
|
|
656
|
+
});
|
|
657
|
+
return {
|
|
658
|
+
key: p.key,
|
|
659
|
+
name: p.name,
|
|
660
|
+
amount: num(p.amount),
|
|
661
|
+
...(p.any ? { atLeast: true } : {}),
|
|
662
|
+
token,
|
|
663
|
+
perDay: count,
|
|
664
|
+
realCost,
|
|
665
|
+
};
|
|
458
666
|
});
|
|
459
667
|
|
|
460
668
|
// ---- recipes
|
|
461
|
-
const recipes = m.recipes.map(
|
|
669
|
+
const recipes = m.recipes.map(r => {
|
|
462
670
|
const crafts = perDay('recipe', r.key);
|
|
463
671
|
const takes = [];
|
|
464
672
|
const gives = [];
|
|
465
673
|
for (const line of r.in ?? []) {
|
|
466
674
|
if (line.takes === 'burn') players.rent -= crafts * (line.count ?? 1) * BURN_REFUND;
|
|
467
675
|
if (line.takes !== 'pay') {
|
|
468
|
-
const what =
|
|
469
|
-
|
|
470
|
-
|
|
676
|
+
const what =
|
|
677
|
+
line.takes === 'show' || line.takes === 'trade'
|
|
678
|
+
? `an NFT of ${line.collection ? `${line.collection.slice(0, 4)}…` : 'the collection'}`
|
|
679
|
+
: line.item;
|
|
680
|
+
const traded = t =>
|
|
681
|
+
!t || t === 'burn'
|
|
682
|
+
? ' (traded in, burned)'
|
|
683
|
+
: t.stash
|
|
684
|
+
? ` (traded in, into “${t.stash}”)`
|
|
685
|
+
: ' (traded in, given on)';
|
|
686
|
+
takes.push(
|
|
687
|
+
`${line.takes === 'burn' ? `${line.count ?? 1} × ` : ''}${what}${line.takes === 'burn' ? ' (burned)' : line.takes === 'trade' ? traded(line.then) : line.takes === 'renew' ? ' (renewed)' : line.takes === 'staked' ? ` (kept, and staked in “${m.farm.get(line.farm)?.name ?? line.farm}”)` : ' (kept)'}`,
|
|
688
|
+
);
|
|
471
689
|
continue;
|
|
472
690
|
}
|
|
473
691
|
const token = line.currency ?? game;
|
|
@@ -476,9 +694,24 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
476
694
|
sendTo(destination(line.to), token, crafts * num(line.amount), `recipe:${r.key}`);
|
|
477
695
|
}
|
|
478
696
|
for (const x of r.raises ?? []) {
|
|
479
|
-
if (!x.fromNow && typeof x.by === 'string')
|
|
480
|
-
|
|
481
|
-
|
|
697
|
+
if (!x.fromNow && typeof x.by === 'string')
|
|
698
|
+
gives.push(
|
|
699
|
+
String(x.by).startsWith('-')
|
|
700
|
+
? `${x.key} ${String(x.by).slice(1)} sooner on collectible ${x.place}`
|
|
701
|
+
: `${x.key} ${x.by} later on collectible ${x.place}`,
|
|
702
|
+
);
|
|
703
|
+
else if (x.fromNow)
|
|
704
|
+
gives.push(
|
|
705
|
+
String(x.by).startsWith('-')
|
|
706
|
+
? `${x.key} ${String(x.by).slice(1)} sooner on collectible ${x.place}`
|
|
707
|
+
: `${x.key} ${x.by} later on collectible ${x.place}, from now when it is past (at most ${x.upTo} ahead)`,
|
|
708
|
+
);
|
|
709
|
+
else
|
|
710
|
+
gives.push(
|
|
711
|
+
x.by > 0
|
|
712
|
+
? `+${x.by} ${x.key} on collectible ${x.place} (up to ${x.upTo})`
|
|
713
|
+
: `${x.by} ${x.key} on collectible ${x.place} (never below 0)`,
|
|
714
|
+
);
|
|
482
715
|
}
|
|
483
716
|
for (const line of r.out ?? []) {
|
|
484
717
|
if (line.gives === 'item') {
|
|
@@ -506,15 +739,17 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
506
739
|
|
|
507
740
|
// ---- schedules
|
|
508
741
|
const schedules = m.schedules
|
|
509
|
-
.filter(
|
|
510
|
-
.map(
|
|
742
|
+
.filter(s => s.active !== false)
|
|
743
|
+
.map(s => {
|
|
511
744
|
const every = toSeconds(s.every) || DAY;
|
|
512
745
|
const runs = DAY / every;
|
|
513
746
|
creator.network += runs * NETWORK_FEE; // run_schedule is free of the program's fee; whoever sends it pays the network
|
|
514
747
|
if (s.does === 'issue') {
|
|
515
748
|
const amount = num(s.amount);
|
|
516
749
|
flowIn(destination(s.to ?? '').kind === 'farm' ? s.to : s.pool, `schedule:${s.key}`, runs * amount);
|
|
517
|
-
const halving = s.halveEvery
|
|
750
|
+
const halving = s.halveEvery
|
|
751
|
+
? `, halving every ${s.halveEvery} runs (every ${round((s.halveEvery * every) / DAY, 2)} days)`
|
|
752
|
+
: '';
|
|
518
753
|
// Halving every h runs: h runs at the amount, h at half, … (2 × h × amount in all, with no end); with
|
|
519
754
|
// maxRuns, the full halvings it reaches and the runs after the last.
|
|
520
755
|
const halvings = s.halveEvery && s.maxRuns ? Math.floor(s.maxRuns / s.halveEvery) : 0;
|
|
@@ -525,14 +760,28 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
525
760
|
: s.maxRuns
|
|
526
761
|
? amount * s.maxRuns
|
|
527
762
|
: Infinity;
|
|
528
|
-
return {
|
|
763
|
+
return {
|
|
764
|
+
key: s.key,
|
|
765
|
+
does: 'issue',
|
|
766
|
+
perDay: round(runs * amount),
|
|
767
|
+
token: m.pool.get(s.pool)?.currency,
|
|
768
|
+
says: `mints ${round(runs * amount)} ${symbol(m.pool.get(s.pool)?.currency)} a day into ${s.pool}${halving}`,
|
|
769
|
+
mintsInAll: Number.isFinite(totalEver) ? round(totalEver) : null,
|
|
770
|
+
};
|
|
529
771
|
}
|
|
530
772
|
if (s.sealed) {
|
|
531
773
|
// A sealed schedule pays from its own vault: what it was funded with, run by run, the last run the rest.
|
|
532
774
|
const t = s.currency ?? m.currencyKey;
|
|
533
|
-
const each =
|
|
775
|
+
const each =
|
|
776
|
+
s.percent === undefined ? `${num(s.amount)} ${symbol(t)}` : `${s.percent}% of what its vault holds`;
|
|
534
777
|
if (s.percent === undefined) sendTo(destination(s.to), t, runs * num(s.amount), `schedule:${s.key}`);
|
|
535
|
-
return {
|
|
778
|
+
return {
|
|
779
|
+
key: s.key,
|
|
780
|
+
does: 'pay',
|
|
781
|
+
sealed: true,
|
|
782
|
+
perDay: s.percent === undefined ? round(runs * num(s.amount)) : 0,
|
|
783
|
+
says: `sealed: pays ${s.to} ${each} every ${round(every / 3600, 3)} h for ${s.maxRuns} runs from a vault of its own${s.fund ? ` (${num(s.fund)} ${symbol(t)} put in at go-live${s.fundFrom === 'mint' ? ', minted' : s.fundFrom?.startsWith('pool:') ? ` from ${s.fundFrom.slice(5)}` : ' from the owner’s wallet'})` : ''}; the last run pays whatever is left, and nobody, the owner included, can change it, stop it or take its tokens back`,
|
|
784
|
+
};
|
|
536
785
|
}
|
|
537
786
|
const from = s.does === 'burn' ? s.pool : s.from;
|
|
538
787
|
const sym = symbol(m.pool.get(from)?.currency);
|
|
@@ -544,7 +793,12 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
544
793
|
else if (s.does === 'burn') add(burned, m.pool.get(from)?.currency, runs * amount);
|
|
545
794
|
else sendTo(destination(s.to), m.pool.get(from)?.currency, runs * amount, `schedule:${s.key}`);
|
|
546
795
|
const where = s.does === 'burn' ? 'burns' : s.does === 'move' ? `moves to ${s.to}` : `pays ${s.to}`;
|
|
547
|
-
return {
|
|
796
|
+
return {
|
|
797
|
+
key: s.key,
|
|
798
|
+
does: s.does,
|
|
799
|
+
perDay: round(runs * amount),
|
|
800
|
+
says: `${where} ${amount} ${sym} from ${from} every ${round(every / 3600, 3)} h`,
|
|
801
|
+
};
|
|
548
802
|
}
|
|
549
803
|
const share = (toBps(s.percent) ?? 0) / 10_000;
|
|
550
804
|
if (s.does === 'burn') {
|
|
@@ -558,7 +812,12 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
558
812
|
sharePrizes.push({ pool: s.from, rate: runs * share, account: s.to, by: `schedule:${s.key}` });
|
|
559
813
|
}
|
|
560
814
|
const where = s.does === 'burn' ? 'burns' : s.does === 'move' ? `moves to ${s.to}` : `pays ${s.to}`;
|
|
561
|
-
return {
|
|
815
|
+
return {
|
|
816
|
+
key: s.key,
|
|
817
|
+
does: s.does,
|
|
818
|
+
percentPerDay: round(runs * s.percent, 6),
|
|
819
|
+
says: `${where} ${s.percent}% of ${from} every ${round(every / 3600, 3)} h`,
|
|
820
|
+
};
|
|
562
821
|
});
|
|
563
822
|
|
|
564
823
|
// ---- stakes: everything that arrives in a farm’s rewards vault goes to its stakers
|
|
@@ -567,7 +826,7 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
567
826
|
// its draw and its pot happen once, spread over the scenario's days like an auction's sale.
|
|
568
827
|
/** Per lottery: whether it draws once, and the days its money a day is spread over. */
|
|
569
828
|
const lotteryRun = new Map();
|
|
570
|
-
const lotteries = m.lotteries.map(
|
|
829
|
+
const lotteries = m.lotteries.map(l => {
|
|
571
830
|
const once = !toSeconds(l.every);
|
|
572
831
|
const untilDraw = l.firstDraw ? toTime(l.firstDraw, now) - now : 0;
|
|
573
832
|
const every = once ? Math.min(untilDraw > 0 ? untilDraw : Infinity, scenarioDays * DAY) : toSeconds(l.every);
|
|
@@ -585,32 +844,82 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
585
844
|
// No loss: every ticket comes back to its buyer after the draw; the pot is only what is paid into it.
|
|
586
845
|
if (l.noLoss) add(players.get, ticketToken, tickets * price);
|
|
587
846
|
else if (l.ticketCurrency) add(burned, ticketToken, tickets * price);
|
|
588
|
-
else {
|
|
847
|
+
else if (keepsPot(l)) {
|
|
589
848
|
// The split takes its shares as it does a sale's; the rest goes into the pot (not the treasury).
|
|
590
849
|
carryOut(split, potToken, tickets * price, `lottery:${l.key}`, `lottery:${l.key}`);
|
|
850
|
+
} else {
|
|
851
|
+
// Paid from each round's own money: the split takes its shares, the places their percents of the rest, and
|
|
852
|
+
// what they leave goes to `rest` (below, once the places are known).
|
|
853
|
+
for (const share of sharesOf(split))
|
|
854
|
+
sendTo(share, potToken, (tickets * price * (share.bps ?? 0)) / 10_000, `lottery:${l.key}`);
|
|
591
855
|
}
|
|
592
856
|
playerAction(tickets);
|
|
593
857
|
// A lottery that holds SOL for its rounds (`roundRent`, on by default) pays each entry's bytes: the buyer pays none.
|
|
594
858
|
const entryRent = roundRentOf(l, m) > 0n ? 0 : ticketLine(l);
|
|
595
859
|
players.rent += tickets * entryRent;
|
|
596
860
|
const placed = (l.places ?? [100]).map(placeOf);
|
|
597
|
-
const places = placed.map(
|
|
861
|
+
const places = placed.map(p => Number(p.percent));
|
|
598
862
|
creator.network += (1 + places.length) * NETWORK_FEE * drawsPerDay; // the draw and each place's payment
|
|
599
863
|
// A place's prize, once a round, given by whoever pays the place (the game's keeper).
|
|
600
|
-
for (const p of placed.filter(
|
|
864
|
+
for (const p of placed.filter(p => p.prize)) givePrize(p.prize, drawsPerDay, `lottery:${l.key}`, creator);
|
|
601
865
|
creator.oracle += ORACLE * drawsPerDay; // the draw asks the oracle; whoever sends it (the game's keeper) pays
|
|
602
866
|
// What the places' prizes are worth a round in the pot's token: tokens of it, or an item with a value in it. A
|
|
603
867
|
// prize with no price (an NFT, a roll, an item with no value) leaves a ticket's return unsaid.
|
|
604
|
-
const worthOf =
|
|
605
|
-
if (line.gives === 'tokens')
|
|
606
|
-
|
|
868
|
+
const worthOf = line => {
|
|
869
|
+
if (line.gives === 'tokens')
|
|
870
|
+
return (prizePool(line) ? m.pool.get(prizePool(line))?.currency : (line.currency ?? game)) === potToken
|
|
871
|
+
? num(line.amount)
|
|
872
|
+
: null;
|
|
873
|
+
if (line.gives === 'item')
|
|
874
|
+
return m.item.get(line.item)?.value !== undefined && game === potToken
|
|
875
|
+
? num(m.item.get(line.item).value)
|
|
876
|
+
: null;
|
|
607
877
|
return null;
|
|
608
878
|
};
|
|
609
|
-
const prized = placed.filter(
|
|
879
|
+
const prized = placed.filter(p => prizeLine(p.prize)).map(p => worthOf(prizeLine(p.prize)));
|
|
610
880
|
// One ticket bought on its own: the fee and the record are per purchase, so a bundle of tickets shares them. The
|
|
611
881
|
// record's rent comes back once the round is done with it, and a no-loss ticket's price after the draw: both held.
|
|
612
|
-
const realCost = realCostOf({
|
|
613
|
-
|
|
882
|
+
const realCost = realCostOf({
|
|
883
|
+
price,
|
|
884
|
+
token: ticketToken,
|
|
885
|
+
isSol: Boolean(m.tokens.get(ticketToken)?.wsol),
|
|
886
|
+
rent: entryRent,
|
|
887
|
+
rentIs: 'the purchase’s entry in the round',
|
|
888
|
+
...(l.noLoss ? { heldIs: 'the ticket’s price' } : {}),
|
|
889
|
+
priceBack: Boolean(l.noLoss),
|
|
890
|
+
});
|
|
891
|
+
// Each round's own money a day, for its places and its `rest` (none reaches them when tickets are burned or NFTs).
|
|
892
|
+
const roundMoney = keepsPot(l) || l.noLoss || l.ticketCurrency ? 0 : tickets * price * (1 - cut);
|
|
893
|
+
if (!keepsPot(l))
|
|
894
|
+
sendTo(
|
|
895
|
+
{ ...destination(l.rest) },
|
|
896
|
+
potToken,
|
|
897
|
+
roundMoney * (1 - places.reduce((a, b) => a + b, 0) / 100),
|
|
898
|
+
`lottery:${l.key}`,
|
|
899
|
+
);
|
|
900
|
+
return {
|
|
901
|
+
key: l.key,
|
|
902
|
+
name: l.name,
|
|
903
|
+
price,
|
|
904
|
+
token: ticketToken,
|
|
905
|
+
potToken,
|
|
906
|
+
...(keepsPot(l) ? { pot: l.pot } : { rest: l.rest, roundMoneyPerDay: roundMoney }),
|
|
907
|
+
...(l.minSold ? { minSold: l.minSold } : {}),
|
|
908
|
+
ticketsPerDay: round(tickets, 6),
|
|
909
|
+
ticketsPerRound: round(perRound, 4),
|
|
910
|
+
roundDays: round(every / DAY, 4),
|
|
911
|
+
places,
|
|
912
|
+
placePrizes: placed.map(p => (p.prize ? prizeWords(p.prize, m) : null)),
|
|
913
|
+
prizeWorthPerRound: round(prized.reduce((n, w) => n + (w ?? 0), 0)),
|
|
914
|
+
...(prized.includes(null) ? { unpricedPrizes: true } : {}),
|
|
915
|
+
cutPercent: num(l.cutPercent),
|
|
916
|
+
splitPercent: round(cut * 100, 4),
|
|
917
|
+
drawsOnce: once,
|
|
918
|
+
roundRentSol: round(Number(roundRentOf(l, m)) / 1e9),
|
|
919
|
+
...(l.ticketNft ? { ticketNft: true } : {}),
|
|
920
|
+
...(l.noLoss ? { noLoss: true } : {}),
|
|
921
|
+
realCost,
|
|
922
|
+
};
|
|
614
923
|
});
|
|
615
924
|
|
|
616
925
|
// ---- decisions with a known rate
|
|
@@ -626,26 +935,37 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
626
935
|
rateOut(d.pool, `decision:${d.key}`, rate * ((toBps(d.percent) ?? 0) / 10_000));
|
|
627
936
|
if (d.does === 'payout') sharePrizes.push({ pool: d.pool, rate: rate * ((toBps(d.percent) ?? 0) / 10_000) });
|
|
628
937
|
else shareBurns.push({ pool: d.pool, rate: rate * ((toBps(d.percent) ?? 0) / 10_000) });
|
|
629
|
-
}
|
|
630
|
-
else flowOut(d.pool, `decision:${d.key}`, rate * num(d.amount));
|
|
938
|
+
} else flowOut(d.pool, `decision:${d.key}`, rate * num(d.amount));
|
|
631
939
|
const token = m.pool.get(d.pool)?.currency;
|
|
632
940
|
if (d.does === 'payout' && d.amount !== undefined) add(players.get, token, rate * num(d.amount));
|
|
633
941
|
if (d.does === 'burn' && d.amount !== undefined) add(burned, token, rate * num(d.amount));
|
|
634
942
|
}
|
|
635
943
|
}
|
|
636
|
-
const contests = m.contests.map(
|
|
944
|
+
const contests = m.contests.map(c => {
|
|
637
945
|
const rake = sharesOf(c.split).reduce((n, s) => n + (s.bps ?? 0), 0) / 100;
|
|
638
946
|
const opened = c.every ? DAY / toSeconds(c.every) : 0;
|
|
639
947
|
creator.fees += opened * 2 * PROJECT_FEE; // opened and decided
|
|
640
948
|
platform.project += opened * 2 * PROJECT_FEE;
|
|
641
|
-
const split = prizeLine(c.prize)?.split
|
|
642
|
-
|
|
949
|
+
const split = prizeLine(c.prize)?.split
|
|
950
|
+
? `, and ${prizeWords(c.prize, m)} (set aside whole when it is decided; short of it, the decision is refused; a part a winner gives up stays for the pool, not for later winners)`
|
|
951
|
+
: '';
|
|
952
|
+
return {
|
|
953
|
+
key: c.key,
|
|
954
|
+
name: c.name,
|
|
955
|
+
sides: c.sides,
|
|
956
|
+
rakePercent: rake,
|
|
957
|
+
winnersSharePercent: 100 - rake,
|
|
958
|
+
says: `the winning side shares ${100 - rake}% of the pot as it stands when the side is named, by what each put in${split}: each winner's part is written then, whoever claims first, and anything sent to the pot after it goes to nobody (burned at the close)`,
|
|
959
|
+
};
|
|
643
960
|
});
|
|
644
961
|
// Auctions: each bid tops the last by the least raise, so a run of N bids ends near reserve × (1 + raise)^(N − 1).
|
|
645
962
|
// The scenario counts bids a day (`auction:<key>`, DEFAULT_BIDS a day unless it says), while the auction runs.
|
|
646
|
-
const auctions = m.auctions.map(
|
|
963
|
+
const auctions = m.auctions.map(a => {
|
|
647
964
|
const runDays = toSeconds(a.runs) / DAY;
|
|
648
|
-
const bids = Math.max(
|
|
965
|
+
const bids = Math.max(
|
|
966
|
+
0,
|
|
967
|
+
Math.round(num(m.scenario.perDay?.[`auction:${a.key}`] ?? DEFAULT_BIDS) * Math.max(runDays, 1 / 24)),
|
|
968
|
+
);
|
|
649
969
|
const reserve = num(a.reserve);
|
|
650
970
|
const raise = (a.minRaise ?? 5) / 100;
|
|
651
971
|
const price = bids ? reserve * (1 + raise) ** (bids - 1) : 0;
|
|
@@ -669,7 +989,9 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
669
989
|
bids,
|
|
670
990
|
reserve: num(a.reserve ?? 1),
|
|
671
991
|
expectedPrice: null,
|
|
672
|
-
says: !bids
|
|
992
|
+
says: !bids
|
|
993
|
+
? 'no bids: the lot goes home'
|
|
994
|
+
: `about ${bids} bids over ${round(runDays)} days, each a bundle of up to three NFTs: the bundle with the most NFTs wins (at least ${num(a.reserve ?? 1)}), so it goes for NFTs, not money, and nothing is counted in what players pay; every topped bundle goes back to its bidder at once`,
|
|
673
995
|
};
|
|
674
996
|
}
|
|
675
997
|
return {
|
|
@@ -686,21 +1008,71 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
686
1008
|
};
|
|
687
1009
|
});
|
|
688
1010
|
for (const f of m.farms) playerAction(perDay('farm', f.key));
|
|
1011
|
+
/**
|
|
1012
|
+
* What many wallets could take from a grant a day, next to one: wallets are free, so per-wallet limits bound a person
|
|
1013
|
+
* only when the grant counts people (vouchers that name a person) or each claim must show something bought. `perDay`
|
|
1014
|
+
* caps it whatever happens. Leaves out a list (each entry is paid once) and codes that each pay a set number of times.
|
|
1015
|
+
*/
|
|
1016
|
+
const MANY = MANY_WALLETS;
|
|
1017
|
+
const manyWallets = g => {
|
|
1018
|
+
const l = g.limits ?? {};
|
|
1019
|
+
const vouched = (g.vouchers ?? []).length > 0 || (g.keys ?? []).length > 0;
|
|
1020
|
+
const listed = (g.list ?? []).length > 0;
|
|
1021
|
+
if ((listed && !vouched) || ((g.keys ?? []).length && l.perKey && !(g.vouchers ?? []).length)) return {};
|
|
1022
|
+
const who = vouched
|
|
1023
|
+
? 'its voucher signer vouches for'
|
|
1024
|
+
: g.gate
|
|
1025
|
+
? 'each showing what its gate asks'
|
|
1026
|
+
: '(free to make)';
|
|
1027
|
+
if (l.perWalletEach?.count) {
|
|
1028
|
+
const each = l.perWalletEach.count / Math.max((toSeconds(l.perWalletEach.every ?? '1d') || DAY) / DAY, 1);
|
|
1029
|
+
const many = Math.min(MANY * each, l.perDay || Infinity);
|
|
1030
|
+
return {
|
|
1031
|
+
many: {
|
|
1032
|
+
one: round(each),
|
|
1033
|
+
all: round(many),
|
|
1034
|
+
per: 'a day',
|
|
1035
|
+
who,
|
|
1036
|
+
capped: Boolean(l.perDay && MANY * each > l.perDay),
|
|
1037
|
+
},
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (!l.perWallet) return {};
|
|
1041
|
+
const many = Math.min(MANY * l.perWallet, l.total || Infinity);
|
|
1042
|
+
return {
|
|
1043
|
+
many: {
|
|
1044
|
+
one: l.perWallet,
|
|
1045
|
+
all: many,
|
|
1046
|
+
per: 'in all',
|
|
1047
|
+
who,
|
|
1048
|
+
capped: Boolean(l.total && MANY * l.perWallet > l.total),
|
|
1049
|
+
perDay: l.perDay ?? null,
|
|
1050
|
+
},
|
|
1051
|
+
};
|
|
1052
|
+
};
|
|
689
1053
|
// Grants: each claim's sender pays the player fee; its records' rent comes from the grant's funds (the owner's SOL, back when its shards close).
|
|
690
|
-
const grants = m.grants.map(
|
|
1054
|
+
const grants = m.grants.map(g => {
|
|
691
1055
|
const claims = num(m.scenario.perDay?.[`grant:${g.key}`]);
|
|
692
1056
|
playerAction(claims);
|
|
693
1057
|
creator.rent = (creator.rent ?? 0) + claims * claimRecordsRent(g);
|
|
694
1058
|
// Its prize, paid for by the grant's funds; a roll's order goes back to the winner at settle.
|
|
695
1059
|
const line = prizeLine(g.prize ?? g.gives);
|
|
696
1060
|
const each = givePrize(line, claims, `grant:${g.key}`, creator);
|
|
697
|
-
return {
|
|
1061
|
+
return {
|
|
1062
|
+
key: g.key,
|
|
1063
|
+
name: g.name,
|
|
1064
|
+
claimsPerDay: claims,
|
|
1065
|
+
gives: prizeWords(line, m),
|
|
1066
|
+
fundsPerClaimSol: round(claimRecordsRent(g) + each),
|
|
1067
|
+
...(line?.gives === 'roll' ? { orderHeldSol: round(orderHeld(line.box)) } : {}),
|
|
1068
|
+
...manyWallets(g),
|
|
1069
|
+
};
|
|
698
1070
|
});
|
|
699
1071
|
for (const h of m.holds) playerAction(perDay('hold', h.key));
|
|
700
1072
|
for (const c of m.contests) playerAction(perDay('contest', c.key));
|
|
701
1073
|
|
|
702
1074
|
// ---- the pools over time
|
|
703
|
-
const settle =
|
|
1075
|
+
const settle = p => {
|
|
704
1076
|
const fixedIn = p.in.reduce((n, f) => n + f.perDay, 0);
|
|
705
1077
|
const fixedOut = p.out.reduce((n, f) => n + f.perDay, 0);
|
|
706
1078
|
const rate = p.rate.reduce((n, f) => n + f.perDay, 0);
|
|
@@ -711,7 +1083,9 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
711
1083
|
const levels = new Map();
|
|
712
1084
|
for (let pass = 0; pass < 3; pass += 1)
|
|
713
1085
|
for (const p of pools.values()) {
|
|
714
|
-
const moved = [...pools.values()].flatMap(
|
|
1086
|
+
const moved = [...pools.values()].flatMap(q =>
|
|
1087
|
+
q.rate.filter(r => r.to === p.key).map(r => r.perDay * (levels.get(q.key) ?? q.seed)),
|
|
1088
|
+
);
|
|
715
1089
|
const { fixedIn, fixedOut, rate } = settle(p);
|
|
716
1090
|
const inflow = fixedIn + moved.reduce((a, b) => a + b, 0);
|
|
717
1091
|
const net = inflow - fixedOut;
|
|
@@ -725,32 +1099,53 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
725
1099
|
// or with `shared` for the whole farm (minted, or from the vault while it lasts). In a steady state the release pays
|
|
726
1100
|
// what arrives, a day at a time.
|
|
727
1101
|
/** A farm's ways out, in words (farm.rs): an early-exit fee, compounding; or null. */
|
|
728
|
-
const farmExtras =
|
|
729
|
-
const exits = (f.tiers ?? [])
|
|
1102
|
+
const farmExtras = f => {
|
|
1103
|
+
const exits = (f.tiers ?? [])
|
|
1104
|
+
.map((t, i) =>
|
|
1105
|
+
t.exitFee
|
|
1106
|
+
? `tier ${i + 1} may leave before its ${t.lock} lock by leaving ${t.exitFee}% of what it takes out (${f.exitFeeTo === 'burn' ? 'burned' : f.exitFeeTo === 'rewards' ? 'into its own rewards vault' : `to ${String(f.exitFeeTo).replace(/^pool:/, '')}`}), keeping the rewards it was credited`
|
|
1107
|
+
: null,
|
|
1108
|
+
)
|
|
1109
|
+
.filter(Boolean);
|
|
730
1110
|
const parts = [
|
|
731
1111
|
...exits,
|
|
732
|
-
f.stakes !== 'collectibles' &&
|
|
733
|
-
|
|
1112
|
+
f.stakes !== 'collectibles' &&
|
|
1113
|
+
f.rewardsToken === f.stakes &&
|
|
1114
|
+
'its rewards can be compounded into the stake without restarting the lock',
|
|
1115
|
+
(f.tiers ?? []).some(t => t.earnsFor) &&
|
|
1116
|
+
`a seat earns only for its tier’s term (${(f.tiers ?? []).map(t => t.earnsFor ?? 'no end').join(', ')}) after its lock started`,
|
|
734
1117
|
].filter(Boolean);
|
|
735
1118
|
return parts.length ? parts.join('; ') : null;
|
|
736
1119
|
};
|
|
737
1120
|
/** What a seat of `f` can give up (farm.rs `Farm::give`): a forfeit, a watched NFT gone. */
|
|
738
|
-
const giveBacks =
|
|
739
|
-
const farmRows = m.farms.map(
|
|
1121
|
+
const giveBacks = f => ['a forfeit', f.custody === 'watch' && 'a watched NFT sold'].filter(Boolean).join(', ');
|
|
1122
|
+
const farmRows = m.farms.map(f => {
|
|
740
1123
|
const pool = pools.get(`farm:${f.key}`);
|
|
741
1124
|
const staked = num(m.scenario.staked?.[f.key]);
|
|
742
1125
|
const perUnit = f.rate ? num(f.rate.perDay) : null;
|
|
743
1126
|
const wanted = !f.rate ? Infinity : f.rate.shared ? (staked ? perUnit : 0) : perUnit * staked;
|
|
744
1127
|
const free = pool ? Math.max(0, pool.inflow - pool.out.reduce((n, x) => n + x.perDay, 0)) : 0;
|
|
745
|
-
const rewards = f.rate?.minted
|
|
1128
|
+
const rewards = f.rate?.minted
|
|
1129
|
+
? wanted
|
|
1130
|
+
: Math.min(wanted, f.rate ? free + (pool?.seed ?? 0) / Math.max(1, m.scenario.days ?? 30) : free);
|
|
746
1131
|
if (pool && !f.rate?.minted) pool.out.push({ by: `farm:${f.key}`, perDay: rewards });
|
|
747
1132
|
const sameToken = f.rewardsToken === f.stakes;
|
|
748
|
-
const tiers = (f.tiers ?? [{ lock: 0, multiplier: 1 }]).map(
|
|
1133
|
+
const tiers = (f.tiers ?? [{ lock: 0, multiplier: 1 }]).map(t => ({
|
|
1134
|
+
lock: t.lock,
|
|
1135
|
+
multiplier: t.multiplier ?? 1,
|
|
1136
|
+
...(t.earnsFor ? { earnsFor: t.earnsFor } : {}),
|
|
1137
|
+
...(prizeLine(t.prize) ? { prize: prizeWords(t.prize, m) } : {}),
|
|
1138
|
+
}));
|
|
749
1139
|
const unit = f.stakes === 'collectibles' ? 'collectible' : symbol(f.stakes);
|
|
750
1140
|
// Growth: the days a collectible at x1 needs for each level.
|
|
751
|
-
const growth = listOf(f.growth).map(
|
|
1141
|
+
const growth = listOf(f.growth).map(g => {
|
|
752
1142
|
const levels = (g.levels ?? []).map((points, i) => ({ level: i + 1, points, days: round(points / g.perDay, 2) }));
|
|
753
|
-
return {
|
|
1143
|
+
return {
|
|
1144
|
+
attribute: g.attribute ?? 'xp',
|
|
1145
|
+
perDay: g.perDay,
|
|
1146
|
+
...(levels.length ? { level: g.level ?? 'level', levels } : {}),
|
|
1147
|
+
streak: Boolean(g.streak),
|
|
1148
|
+
};
|
|
754
1149
|
});
|
|
755
1150
|
const perSeat = !f.rate ? null : f.rate.shared ? (staked ? perUnit / staked : null) : perUnit;
|
|
756
1151
|
return {
|
|
@@ -759,9 +1154,22 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
759
1154
|
stakes: f.stakes,
|
|
760
1155
|
rewardsToken: f.rewardsToken,
|
|
761
1156
|
rewardsPerDay: round(rewards),
|
|
762
|
-
...(f.rate
|
|
1157
|
+
...(f.rate
|
|
1158
|
+
? {
|
|
1159
|
+
rate: {
|
|
1160
|
+
perDay: perUnit,
|
|
1161
|
+
minted: Boolean(f.rate.minted),
|
|
1162
|
+
...(f.rate.shared ? { shared: true } : {}),
|
|
1163
|
+
...(f.rate.halveEvery ? { halveEvery: f.rate.halveEvery } : {}),
|
|
1164
|
+
},
|
|
1165
|
+
}
|
|
1166
|
+
: {}),
|
|
763
1167
|
tiers,
|
|
764
|
-
...(staked
|
|
1168
|
+
...(staked
|
|
1169
|
+
? { staked, perUnitPerDay: round(f.rate ? perSeat : rewards / staked), unit }
|
|
1170
|
+
: f.rate && !f.rate.shared
|
|
1171
|
+
? { perUnitPerDay: perUnit, unit }
|
|
1172
|
+
: {}),
|
|
765
1173
|
...(staked && sameToken ? { yearlyPercent: round(((rewards * 365) / staked) * 100, 4) } : {}),
|
|
766
1174
|
...(growth.length ? { growth } : {}),
|
|
767
1175
|
...(f.runs ? { runs: f.runs } : {}),
|
|
@@ -776,30 +1184,44 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
776
1184
|
});
|
|
777
1185
|
for (const row of farmRows) add(players.get, row.rewardsToken, row.rewardsPerDay);
|
|
778
1186
|
// A farm that ends inside the scenario takes no stake from its end on (FarmOver), and pays nothing after it.
|
|
779
|
-
for (const f of m.farms.filter(
|
|
780
|
-
notes.push(
|
|
1187
|
+
for (const f of m.farms.filter(x => x.runs && toSeconds(x.runs) < days * DAY))
|
|
1188
|
+
notes.push(
|
|
1189
|
+
`“${f.name ?? f.key}” runs ${f.runs}, less than the scenario’s ${days} days: its figures are a day while it runs; from its end it takes no stake and pays nothing.`,
|
|
1190
|
+
);
|
|
781
1191
|
|
|
782
1192
|
// Lotteries pay what came in, once the pot is in its steady state; one that draws once pays its seed and all that
|
|
783
1193
|
// came in, at its one draw.
|
|
784
1194
|
for (const l of lotteries) {
|
|
785
1195
|
const pool = pools.get(`lottery:${l.key}`);
|
|
786
|
-
const inflow = pool ? pool.inflow : 0;
|
|
787
1196
|
const run = lotteryRun.get(l.key);
|
|
788
1197
|
const placesTotal = l.places.reduce((a, b) => a + b, 0) / 100;
|
|
1198
|
+
// A pot pays what came in once it is steady; a round paid from its own money pays its places' percents of it, the
|
|
1199
|
+
// rest gone to `rest` already.
|
|
1200
|
+
const inflow = pool ? pool.inflow : (l.roundMoneyPerDay ?? 0) * placesTotal;
|
|
789
1201
|
pool?.out.push({ by: `lottery:${l.key}`, perDay: inflow });
|
|
790
|
-
const potBeforeDraw =
|
|
791
|
-
|
|
1202
|
+
const potBeforeDraw = !pool
|
|
1203
|
+
? (l.roundMoneyPerDay ?? 0) * (run.once ? run.spreadDays : l.roundDays)
|
|
1204
|
+
: run.once
|
|
1205
|
+
? run.seed + inflow * run.spreadDays
|
|
1206
|
+
: placesTotal
|
|
1207
|
+
? (inflow * l.roundDays) / placesTotal
|
|
1208
|
+
: 0;
|
|
1209
|
+
const paid = run.once || !pool ? potBeforeDraw * placesTotal : inflow * l.roundDays;
|
|
792
1210
|
l.potBeforeDraw = round(potBeforeDraw);
|
|
1211
|
+
if (!pool) l.roundMoneyPerDay = round(l.roundMoneyPerDay ?? 0);
|
|
793
1212
|
l.paidPerRound = round(paid);
|
|
794
|
-
l.prizes = l.places.map(
|
|
1213
|
+
l.prizes = l.places.map(pct => round((potBeforeDraw * pct) / 100));
|
|
795
1214
|
const perTicket = l.ticketsPerRound ? (paid + l.prizeWorthPerRound) / l.ticketsPerRound : null;
|
|
796
1215
|
l.expectedPerTicket = perTicket === null ? null : round(perTicket);
|
|
797
1216
|
// A no-loss ticket comes back whole, so it returns its price and its share of the pot on top.
|
|
798
|
-
l.returnPercent =
|
|
1217
|
+
l.returnPercent =
|
|
1218
|
+
perTicket !== null && l.price && l.token === l.potToken && !l.unpricedPrizes
|
|
1219
|
+
? round((perTicket / l.price) * 100 + (l.noLoss ? 100 : 0), 4)
|
|
1220
|
+
: null;
|
|
799
1221
|
add(players.get, l.potToken, run.once ? paid / run.spreadDays : inflow);
|
|
800
1222
|
}
|
|
801
1223
|
|
|
802
|
-
const poolRows = [...pools.values()].map(
|
|
1224
|
+
const poolRows = [...pools.values()].map(p => {
|
|
803
1225
|
const { fixedOut, rate } = settle(p);
|
|
804
1226
|
const inflow = p.inflow ?? 0;
|
|
805
1227
|
const net = inflow - fixedOut;
|
|
@@ -811,8 +1233,11 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
811
1233
|
seed: p.seed,
|
|
812
1234
|
inPerDay: round(inflow),
|
|
813
1235
|
outPerDay: round(fixedOut + rate * (levels.get(p.key) ?? p.seed)),
|
|
814
|
-
ins: p.in.map(
|
|
815
|
-
outs: [
|
|
1236
|
+
ins: p.in.map(f => ({ by: f.by, perDay: round(f.perDay) })),
|
|
1237
|
+
outs: [
|
|
1238
|
+
...p.out.map(f => ({ by: f.by, perDay: round(f.perDay) })),
|
|
1239
|
+
...p.rate.map(f => ({ by: f.by, percentPerDay: round(f.perDay * 100, 6) })),
|
|
1240
|
+
],
|
|
816
1241
|
};
|
|
817
1242
|
if (rate > 0 && Math.abs(net) < 1e-12) {
|
|
818
1243
|
// Only a share goes out: it shrinks by that share for ever, and never quite empties.
|
|
@@ -844,21 +1269,26 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
844
1269
|
row.swingPerDay = round(Math.sqrt(p.variance), 6);
|
|
845
1270
|
const weekly = Math.sqrt(p.variance * 7);
|
|
846
1271
|
if (row.lasts !== 'runs dry' && p.seed < 3 * weekly)
|
|
847
|
-
notes.push(
|
|
1272
|
+
notes.push(
|
|
1273
|
+
`The pool “${p.key}” ${row.lasts === 'grows' ? 'takes in more than it pays on average' : row.lasts === 'settles' ? 'settles on average' : 'pays even on average'}, but luck moves it by about ±${round(row.swingPerDay, 4)} ${symbol(p.currency)} a day (±${round(weekly, 4)} a week), against a seed of ${p.seed}. While it can’t cover a fixed prize, a box that stops selling when out (the default) sells nothing, and one that draws among what is left never lands on that prize, so a bad week stalls sales or takes the big wins away until it refills. A seed of about ${round(3 * weekly, 2)} (three times a week’s swing) rides out almost any week.`,
|
|
1274
|
+
);
|
|
848
1275
|
}
|
|
849
|
-
if (row.lasts === 'runs dry')
|
|
1276
|
+
if (row.lasts === 'runs dry')
|
|
1277
|
+
notes.push(
|
|
1278
|
+
`The pool “${p.key}” pays out more than comes in: it lasts about ${row.lastsDays} days from its seed of ${p.seed} ${symbol(p.currency)}.`,
|
|
1279
|
+
);
|
|
850
1280
|
return row;
|
|
851
1281
|
});
|
|
852
1282
|
|
|
853
1283
|
for (const prize of sharePrizes) {
|
|
854
|
-
const row = poolRows.find(
|
|
1284
|
+
const row = poolRows.find(r => r.key === prize.pool);
|
|
855
1285
|
const level = row ? (row.settlesAt ?? row.seed) : 0;
|
|
856
1286
|
if (prize.account) sendTo(destination(prize.account), row?.token, prize.rate * level, prize.by);
|
|
857
1287
|
else add(players.get, row?.token, prize.rate * level);
|
|
858
1288
|
}
|
|
859
1289
|
// A share burned is burned at the pool's level, as a share paid out is.
|
|
860
1290
|
for (const b of shareBurns) {
|
|
861
|
-
const row = poolRows.find(
|
|
1291
|
+
const row = poolRows.find(r => r.key === b.pool);
|
|
862
1292
|
add(burned, row?.token, b.rate * (row ? (row.settlesAt ?? row.seed) : 0));
|
|
863
1293
|
}
|
|
864
1294
|
// Stashes: each roll that lands on one takes an NFT out; how long what it starts with lasts.
|
|
@@ -866,37 +1296,84 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
866
1296
|
const b = m.box.get(r.box);
|
|
867
1297
|
const w = b ? weights(b) : [];
|
|
868
1298
|
const total = w.reduce((a, x) => a + x, 0) || 1;
|
|
869
|
-
(b?.outcomes ?? []).forEach((o, i) =>
|
|
1299
|
+
(b?.outcomes ?? []).forEach((o, i) =>
|
|
1300
|
+
outcomeParts(o).forEach(
|
|
1301
|
+
x =>
|
|
1302
|
+
(x.gives === 'stash' || x.gives === 'nft') &&
|
|
1303
|
+
stashTakes.push({ stash: x.stash, perDay: (r.perDay * w[i]) / total, by: r.by }),
|
|
1304
|
+
),
|
|
1305
|
+
);
|
|
870
1306
|
}
|
|
871
1307
|
// What fills a stash besides its owner: NFTs paid as a price (a box's `then`, a recipe's trade-in, a lottery's ticket).
|
|
872
1308
|
const stashFills = [];
|
|
873
|
-
for (const b of m.boxes.filter(
|
|
1309
|
+
for (const b of m.boxes.filter(b => b.rollsOn && b.active !== false)) {
|
|
874
1310
|
const w = weights(b);
|
|
875
1311
|
const total = w.reduce((a, x) => a + x, 0) || 1;
|
|
876
|
-
(b.outcomes ?? []).forEach(
|
|
1312
|
+
(b.outcomes ?? []).forEach(
|
|
1313
|
+
(o, i) =>
|
|
1314
|
+
(o.then ?? b.then)?.stash &&
|
|
1315
|
+
stashFills.push({
|
|
1316
|
+
stash: (o.then ?? b.then).stash,
|
|
1317
|
+
perDay: (perDay('box', b.key) * w[i]) / total,
|
|
1318
|
+
by: `box:${b.key}`,
|
|
1319
|
+
}),
|
|
1320
|
+
);
|
|
877
1321
|
}
|
|
878
|
-
for (const r of m.recipes)
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1322
|
+
for (const r of m.recipes)
|
|
1323
|
+
for (const l of r.in ?? [])
|
|
1324
|
+
if (l.takes === 'trade' && l.then?.stash)
|
|
1325
|
+
stashFills.push({ stash: l.then.stash, perDay: perDay('recipe', r.key), by: `recipe:${r.key}` });
|
|
1326
|
+
for (const l of m.lotteries)
|
|
1327
|
+
if (l.ticketNft?.then?.stash)
|
|
1328
|
+
stashFills.push({ stash: l.ticketNft.then.stash, perDay: perDay('lottery', l.key), by: `lottery:${l.key}` });
|
|
1329
|
+
const stashes = m.stashes.map(s => {
|
|
1330
|
+
const takes = stashTakes.filter(t => t.stash === s.key);
|
|
1331
|
+
const fills = stashFills.filter(t => t.stash === s.key);
|
|
883
1332
|
const perDay = takes.reduce((n, t) => n + t.perDay, 0);
|
|
884
1333
|
const inPerDay = fills.reduce((n, t) => n + t.perDay, 0);
|
|
885
1334
|
const holds = (s.nfts ?? []).length;
|
|
886
1335
|
const net = perDay - inPerDay;
|
|
887
|
-
const filled =
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1336
|
+
const filled =
|
|
1337
|
+
inPerDay > 0
|
|
1338
|
+
? `, and takes in about ${round(inPerDay, 4)} a day (${[...new Set(fills.map(t => t.by))].join(', ')})`
|
|
1339
|
+
: '';
|
|
1340
|
+
if (perDay > 0 && net > 0)
|
|
1341
|
+
notes.push(
|
|
1342
|
+
`The stash “${s.name}” starts with ${holds} NFT${holds === 1 ? '' : 's'} and gives about ${round(perDay, 4)} a day (${[...new Set(takes.map(t => t.by))].join(', ')})${filled}: ${holds ? `empty in about ${round(holds / net, 2)} days` : 'empty from the start'}, after which a draw that lands on its outcome is not made, and gives back that draw’s worth, until more are put in.`,
|
|
1343
|
+
);
|
|
1344
|
+
else if (perDay > 0)
|
|
1345
|
+
notes.push(
|
|
1346
|
+
`The stash “${s.name}” starts with ${holds} NFT${holds === 1 ? '' : 's'}, gives about ${round(perDay, 4)} a day (${[...new Set(takes.map(t => t.by))].join(', ')})${filled}: it keeps up${holds ? '' : ', once the first come in (until then a draw that lands on its outcome is not made)'}.`,
|
|
1347
|
+
);
|
|
1348
|
+
return {
|
|
1349
|
+
key: s.key,
|
|
1350
|
+
name: s.name,
|
|
1351
|
+
holds,
|
|
1352
|
+
givenPerDay: round(perDay, 6),
|
|
1353
|
+
...(inPerDay > 0 ? { takenInPerDay: round(inPerDay, 6) } : {}),
|
|
1354
|
+
...(net > 0 ? { lastsDays: round(holds / net, 4) } : {}),
|
|
1355
|
+
};
|
|
891
1356
|
});
|
|
892
|
-
const contestRolls = m.contests.filter(
|
|
893
|
-
if (contestRolls.length)
|
|
1357
|
+
const contestRolls = m.contests.filter(c => prizeLine(c.prize)?.gives === 'roll');
|
|
1358
|
+
if (contestRolls.length)
|
|
1359
|
+
notes.push(
|
|
1360
|
+
`${contestRolls.map(c => `“${c.name}”`).join(', ')} give${contestRolls.length === 1 ? 's' : ''} every winning entry a roll: whoever claims it pays the roll’s order (about ${round(orderHeld(prizeLine(contestRolls[0].prize).box), 4)} SOL, back to the winner at settle), and whoever asks for the roll pays the oracle.`,
|
|
1361
|
+
);
|
|
894
1362
|
for (const t of m.tokens.values())
|
|
895
|
-
if (t.powers?.transferFeePercent > 0)
|
|
1363
|
+
if (t.powers?.transferFeePercent > 0)
|
|
1364
|
+
notes.push(
|
|
1365
|
+
`${t.symbol} keeps ${t.powers.transferFeePercent}% of every transfer: pools, prizes, stakes and the treasury receive that much less than shown here.`,
|
|
1366
|
+
);
|
|
896
1367
|
|
|
897
|
-
const totals =
|
|
1368
|
+
const totals = map => Object.fromEntries(Object.entries(map).map(([k, v]) => [k, round(v)]));
|
|
898
1369
|
return {
|
|
899
|
-
scenario: {
|
|
1370
|
+
scenario: {
|
|
1371
|
+
days,
|
|
1372
|
+
perDay: m.scenario.perDay ?? {},
|
|
1373
|
+
defaultPerDay: DEFAULT_PER_DAY,
|
|
1374
|
+
defaultBidsPerDay: DEFAULT_BIDS,
|
|
1375
|
+
defaultClaimsPerDay: 0,
|
|
1376
|
+
},
|
|
900
1377
|
boxes,
|
|
901
1378
|
items,
|
|
902
1379
|
payments,
|
|
@@ -910,24 +1387,50 @@ export function simulate(economy, { now = 0 } = {}) {
|
|
|
910
1387
|
stashes,
|
|
911
1388
|
pools: poolRows,
|
|
912
1389
|
perDay: {
|
|
913
|
-
players: {
|
|
914
|
-
|
|
1390
|
+
players: {
|
|
1391
|
+
pay: totals(players.pay),
|
|
1392
|
+
getBack: totals(players.get),
|
|
1393
|
+
playerFeesSol: round(players.fees),
|
|
1394
|
+
oracleFeesSol: round(players.oracle),
|
|
1395
|
+
networkFeesSol: round(players.network),
|
|
1396
|
+
rentSol: round(players.rent),
|
|
1397
|
+
},
|
|
1398
|
+
creator: {
|
|
1399
|
+
treasury: totals(creator.treasury),
|
|
1400
|
+
projectFeesSol: round(creator.fees),
|
|
1401
|
+
oracleFeesSol: round(creator.oracle),
|
|
1402
|
+
networkFeesSol: round(creator.network),
|
|
1403
|
+
grantRentSol: round(creator.rent ?? 0),
|
|
1404
|
+
},
|
|
915
1405
|
oracle: { totalSol: round(players.oracle + creator.oracle) },
|
|
916
|
-
platform: {
|
|
1406
|
+
platform: {
|
|
1407
|
+
playerFeesSol: round(platform.player),
|
|
1408
|
+
projectFeesSol: round(platform.project),
|
|
1409
|
+
totalSol: round(platform.player + platform.project),
|
|
1410
|
+
},
|
|
917
1411
|
burned: totals(burned),
|
|
918
1412
|
accounts: totals(accounts),
|
|
919
1413
|
},
|
|
920
|
-
fees: {
|
|
1414
|
+
fees: {
|
|
1415
|
+
playerSol: PLAYER_FEE,
|
|
1416
|
+
projectSol: PROJECT_FEE,
|
|
1417
|
+
oracleSol: ORACLE,
|
|
1418
|
+
networkPerSignatureSol: NETWORK_FEE,
|
|
1419
|
+
settleRentUnitSol: sol(SETTLE_RENT_UNIT),
|
|
1420
|
+
ticketLineRentSol: round(ticketLine({})),
|
|
1421
|
+
collectibleRentSol: COLLECTIBLE_RENT,
|
|
1422
|
+
burnRefundSol: BURN_REFUND,
|
|
1423
|
+
},
|
|
921
1424
|
notes,
|
|
922
1425
|
};
|
|
923
1426
|
}
|
|
924
1427
|
|
|
925
1428
|
/** What the scenario doesn't name and doesn't count at its default, in words: ["grant claims none", "auction bids 10 a day"]. */
|
|
926
1429
|
export function unnamedExceptions(sim) {
|
|
927
|
-
const named =
|
|
1430
|
+
const named = k => Object.hasOwn(sim.scenario.perDay ?? {}, k);
|
|
928
1431
|
return [
|
|
929
|
-
(sim.grants ?? []).some(
|
|
930
|
-
(sim.auctions ?? []).some(
|
|
1432
|
+
(sim.grants ?? []).some(g => !named(`grant:${g.key}`)) && 'grant claims none',
|
|
1433
|
+
(sim.auctions ?? []).some(a => !named(`auction:${a.key}`)) && `auction bids ${DEFAULT_BIDS} a day`,
|
|
931
1434
|
].filter(Boolean);
|
|
932
1435
|
}
|
|
933
1436
|
/** What the scenario assumes for what it doesn't name, in words: "100 a day (grant claims none, auction bids 10 a day)". */
|
|
@@ -938,10 +1441,11 @@ export function unnamedWords(sim) {
|
|
|
938
1441
|
|
|
939
1442
|
/** What becomes of a pool (a row of `simulate`'s pools) over the scenario's `days`, in words: "settles at about 40". */
|
|
940
1443
|
export function poolFate(p, days) {
|
|
941
|
-
const n =
|
|
1444
|
+
const n = x => String(round(x, 6));
|
|
942
1445
|
if (p.lasts === 'runs dry') return `runs dry in about ${n(p.lastsDays)} days`;
|
|
943
1446
|
if (p.lasts === 'settles') return `settles at about ${n(p.settlesAt)}`;
|
|
944
|
-
if (p.lasts === 'shrinks')
|
|
1447
|
+
if (p.lasts === 'shrinks')
|
|
1448
|
+
return `shrinks to about ${n(p.after)} after ${days} days (half gone every ${n(p.halfLifeDays)} days)`;
|
|
945
1449
|
if (p.lasts === 'grows') return `grows to about ${n(p.after)} after ${days} days`;
|
|
946
1450
|
return 'holds steady';
|
|
947
1451
|
}
|
|
@@ -952,68 +1456,157 @@ export function poolFate(p, days) {
|
|
|
952
1456
|
*/
|
|
953
1457
|
export function inAll(c) {
|
|
954
1458
|
if (!c) return '';
|
|
955
|
-
const n =
|
|
956
|
-
const parts = [
|
|
957
|
-
|
|
958
|
-
|
|
1459
|
+
const n = x => String(round(x, 6));
|
|
1460
|
+
const parts = [
|
|
1461
|
+
'the player fee',
|
|
1462
|
+
c.oracleSol ? 'the oracle' : '',
|
|
1463
|
+
'the network',
|
|
1464
|
+
c.rentSol ? (/comes back/.test(c.rentIs) ? c.rentIs : `${c.rentIs}, which never comes back`) : '',
|
|
1465
|
+
]
|
|
1466
|
+
.filter(Boolean)
|
|
1467
|
+
.join(', ');
|
|
1468
|
+
const head =
|
|
1469
|
+
c.totalSol !== undefined
|
|
1470
|
+
? `about ${n(c.totalSol)} SOL in all${c.atLeast ? ' at the least amount' : ''} (${c.priceBack ? '' : 'the price, '}${parts})`
|
|
1471
|
+
: `${n(c.extrasSol)} SOL on top of the price (${parts})`;
|
|
1472
|
+
return c.holdSol > (c.totalSol ?? c.extrasSol) + 1e-9
|
|
1473
|
+
? `${head}; the wallet needs about ${n(c.holdSol)} SOL when it signs, and the rest${c.heldIs ? ` (${c.heldIs})` : ''} comes back`
|
|
1474
|
+
: head;
|
|
959
1475
|
}
|
|
960
1476
|
|
|
961
1477
|
/** The simulation in plain words. */
|
|
962
1478
|
export function explain(sim, economy) {
|
|
963
1479
|
const m = model(economy);
|
|
964
|
-
const sym =
|
|
1480
|
+
const sym = key => m.tokens.get(key)?.symbol ?? key;
|
|
965
1481
|
const lines = [];
|
|
966
|
-
const n =
|
|
967
|
-
lines.push(
|
|
1482
|
+
const n = x => (Number.isFinite(x) ? String(round(x, 6)) : String(x));
|
|
1483
|
+
lines.push(
|
|
1484
|
+
`Assuming ${
|
|
1485
|
+
Object.entries(sim.scenario.perDay)
|
|
1486
|
+
.map(([k, v]) => `${v} × ${k}`)
|
|
1487
|
+
.join(', ') || 'nothing in particular'
|
|
1488
|
+
} a day (anything not named: ${unnamedWords(sim)}), over ${sim.scenario.days} days.`,
|
|
1489
|
+
);
|
|
968
1490
|
for (const b of sim.boxes) {
|
|
969
1491
|
const edge = b.houseEdgePercent !== null ? `; the house keeps ${n(b.houseEdgePercent)}% of each price` : '';
|
|
970
1492
|
const also = Object.entries(b.alsoPays).map(([t, v]) => `${n(v)} ${sym(t)}`);
|
|
971
|
-
const back =
|
|
1493
|
+
const back =
|
|
1494
|
+
b.returnPercent !== null
|
|
1495
|
+
? `${n(b.expectedBack)} ${sym(b.token)} on average (${n(b.returnPercent)}%)${also.length ? ` and ${also.join(', ')}` : ''}`
|
|
1496
|
+
: `${also.join(', ') || 'nothing'} on average`;
|
|
972
1497
|
const all = `; ${inAll(b.realCost)}`;
|
|
973
1498
|
lines.push(`Box “${b.name}”: costs ${n(b.price)} ${sym(b.token)} (${all.slice(2)}), gives back ${back}${edge}.`);
|
|
974
|
-
const part =
|
|
975
|
-
|
|
1499
|
+
const part = o =>
|
|
1500
|
+
o.gives === 'pay'
|
|
1501
|
+
? `${n(o.amount)} ${sym(o.token)} from ${o.pool}`
|
|
1502
|
+
: o.gives === 'mint'
|
|
1503
|
+
? `${n(o.amount)} ${sym(o.token)}, minted`
|
|
1504
|
+
: o.gives === 'stash'
|
|
1505
|
+
? `an NFT of the stash “${o.stash}”, at random`
|
|
1506
|
+
: o.gives === 'nft'
|
|
1507
|
+
? `the NFT ${String(o.nft).slice(0, 4)}… of “${o.stash}”`
|
|
1508
|
+
: o.gives === 'roll'
|
|
1509
|
+
? `a roll of “${o.box}”`
|
|
1510
|
+
: o.gives === 'item'
|
|
1511
|
+
? `${o.item}${o.worth !== null ? ` (worth ${n(o.worth)})` : ' (no value given)'}`
|
|
1512
|
+
: o.gives;
|
|
1513
|
+
for (const o of b.outcomes)
|
|
1514
|
+
lines.push(` ${n(o.chance)}%: ${o.gives === 'bundle' ? o.prizes.map(part).join(' + ') : part(o)}`);
|
|
976
1515
|
if (b.unpriced.length) lines.push(` Give ${b.unpriced.join(', ')} a "value" to count them in the return.`);
|
|
977
1516
|
if (b.limitedStock) lines.push(` ${whenOutWords(m.box.get(b.key))}.`);
|
|
978
1517
|
}
|
|
979
1518
|
for (const it of sim.items) lines.push(`Shop “${it.name}”: ${n(it.price)} ${sym(it.token)}; ${inAll(it.realCost)}.`);
|
|
980
|
-
for (const p of sim.payments)
|
|
1519
|
+
for (const p of sim.payments)
|
|
1520
|
+
lines.push(
|
|
1521
|
+
`Payment “${p.name}”: ${p.atLeast ? 'at least ' : ''}${n(p.amount)} ${sym(p.token)}; ${inAll(p.realCost)}.`,
|
|
1522
|
+
);
|
|
981
1523
|
for (const l of sim.lotteries) {
|
|
982
|
-
const rounds = l.drawsOnce
|
|
983
|
-
|
|
984
|
-
|
|
1524
|
+
const rounds = l.drawsOnce
|
|
1525
|
+
? `${n(l.ticketsPerRound)} tickets in its one round of ${n(l.roundDays)} days (it draws once)`
|
|
1526
|
+
: `${n(l.ticketsPerRound)} tickets a round of ${n(l.roundDays)} days`;
|
|
1527
|
+
const from =
|
|
1528
|
+
l.rest !== undefined
|
|
1529
|
+
? `each round pays from its own ticket money, about ${n(l.potBeforeDraw)} ${sym(l.potToken)}, what its places leave going to “${l.rest}”,`
|
|
1530
|
+
: `the pot reaches about ${n(l.potBeforeDraw)} ${sym(l.potToken)} and`;
|
|
1531
|
+
lines.push(
|
|
1532
|
+
`Lottery “${l.name}”: ${rounds}; ${from} pays ${l.prizes.map((v, i) => `${n(v)}${l.placePrizes?.[i] ? ` and ${l.placePrizes[i]}` : ''}`).join(' / ')}; ${l.ticketNft ? (l.noLoss ? 'a ticket (one NFT, back to its buyer after the draw)' : 'a ticket (one NFT, spent at once, never refunded)') : `a ${n(l.price)} ${sym(l.token)} ticket${l.noLoss ? ' (back to its buyer after the draw)' : ''}`} ${l.noLoss ? 'wins' : 'is worth'} ${n(l.expectedPerTicket)} ${sym(l.potToken)} on average${l.noLoss ? ' on top' : ''}${l.unpricedPrizes ? ', plus its chance at the places’ prizes (no price given)' : ''}${l.returnPercent !== null ? ` (${n(l.returnPercent)}%${l.noLoss ? ' back, the ticket included' : ''})` : ''}; one ticket bought alone: ${inAll(l.realCost)}.`,
|
|
1533
|
+
);
|
|
1534
|
+
if (l.minSold)
|
|
1535
|
+
lines.push(
|
|
1536
|
+
` Fewer than ${l.minSold} tickets sold by the draw: the round is cancelled and every ticket refunded.`,
|
|
1537
|
+
);
|
|
985
1538
|
}
|
|
986
1539
|
for (const f of sim.farms) {
|
|
987
|
-
lines.push(
|
|
1540
|
+
lines.push(
|
|
1541
|
+
`Farm “${f.name}”: ${n(f.rewardsPerDay)} ${sym(f.rewardsToken)} a day to everyone staked${f.perUnitPerDay !== undefined ? `, ${n(f.perUnitPerDay)} a day for each ${f.unit} staked` : ''}${f.yearlyPercent !== undefined ? ` (${n(f.yearlyPercent)}% a year)` : ''}; ${f.says}.`,
|
|
1542
|
+
);
|
|
988
1543
|
if (f.extras) lines.push(` ${f.extras[0].toUpperCase()}${f.extras.slice(1)}.`);
|
|
989
|
-
for (const g of f.growth ?? [])
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1544
|
+
for (const g of f.growth ?? [])
|
|
1545
|
+
lines.push(
|
|
1546
|
+
` Growth: ${g.perDay} ${g.attribute} a day at x1${g.levels ? `; ${g.levels.map(l => `level ${l.level} after ${n(l.days)} days`).join(', ')}` : ''}${g.streak ? ', back to 0 when it leaves' : ''}.`,
|
|
1547
|
+
);
|
|
1548
|
+
if (f.runs)
|
|
1549
|
+
lines.push(
|
|
1550
|
+
` Runs ${f.runs} from when it goes live: it takes no stake at its end or after it, and pays and grows nothing more${f.end ? `; ${f.end}` : ''}.`,
|
|
1551
|
+
);
|
|
1552
|
+
if (f.lockAttribute)
|
|
1553
|
+
lines.push(
|
|
1554
|
+
` Its lock is written on each collectible under “${f.lockAttribute}”: whatever moves that time lets it out sooner or keeps it longer.`,
|
|
1555
|
+
);
|
|
1556
|
+
if (f.upkeepAttribute)
|
|
1557
|
+
lines.push(
|
|
1558
|
+
` Upkeep: each collectible earns and grows only until the time under “${f.upkeepAttribute}”; the figures assume every one is kept topped up.`,
|
|
1559
|
+
);
|
|
1560
|
+
for (const t of f.tiers.filter(t => t.prize))
|
|
1561
|
+
lines.push(` Mission (lock ${t.lock}): ${t.prize} for each one that stays it (tokens grown by its boosts).`);
|
|
994
1562
|
}
|
|
995
|
-
for (const g of sim.grants)
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1563
|
+
for (const g of sim.grants)
|
|
1564
|
+
lines.push(
|
|
1565
|
+
`Grant “${g.name}”: ${n(g.claimsPerDay)} claims a day, each giving ${g.gives}; each claim costs its funds about ${n(g.fundsPerClaimSol)} SOL${g.orderHeldSol ? ` (${n(g.orderHeldSol)} of it the roll’s order, which goes to the winner when it is settled; whoever asks for the roll pays the oracle)` : ''}.${g.many ? ` One wallet may claim ${n(g.many.one)} ${g.many.per}; ${MANY_WALLETS} wallets ${g.many.who} could claim ${n(g.many.all)} ${g.many.per}${g.many.capped ? `, its "${g.many.per === 'a day' ? 'perDay' : 'total'}" the cap` : ''}${g.many.perDay ? `, at most ${n(g.many.perDay)} a day` : ''}.` : ''}`,
|
|
1566
|
+
);
|
|
1567
|
+
for (const s of sim.stashes ?? [])
|
|
1568
|
+
lines.push(
|
|
1569
|
+
`Stash “${s.name}”: ${s.holds} NFT${s.holds === 1 ? '' : 's'} to start${s.takenInPerDay ? `, about ${n(s.takenInPerDay)} a day come in` : ''}${s.lastsDays !== undefined ? `, given at about ${n(s.givenPerDay)} a day: lasts about ${n(s.lastsDays)} days` : s.givenPerDay ? `, given at about ${n(s.givenPerDay)} a day` : ''}.`,
|
|
1570
|
+
);
|
|
1571
|
+
for (const r of sim.recipes)
|
|
1572
|
+
lines.push(
|
|
1573
|
+
`Recipe “${r.name}”: ${r.takes.join(' + ') || 'nothing'} → ${r.gives.join(' + ') || 'nothing'}, ${n(r.craftsPerDay)} a day.`,
|
|
1574
|
+
);
|
|
1575
|
+
for (const s of sim.schedules)
|
|
1576
|
+
lines.push(`Schedule “${s.key}”: ${s.says}; ${lateRunWords(m.schedules.find(x => x.key === s.key) ?? {})}.`);
|
|
999
1577
|
for (const c of sim.contests) lines.push(`Contest “${c.name}”: ${c.says}.`);
|
|
1000
1578
|
for (const a of sim.auctions ?? []) lines.push(`Auction “${a.name}”: ${a.says}.`);
|
|
1001
|
-
for (const p of sim.pools)
|
|
1579
|
+
for (const p of sim.pools)
|
|
1580
|
+
lines.push(
|
|
1581
|
+
`${p.pot ? `The pot of “${p.pot}”` : p.farm ? `The rewards vault of “${p.farm}”` : `Pool “${p.key}”`}: ${n(p.inPerDay)} ${sym(p.token)} in and ${n(p.outPerDay)} out a day from a seed of ${n(p.seed)}; it ${poolFate(p, sim.scenario.days)}.`,
|
|
1582
|
+
);
|
|
1002
1583
|
const d = sim.perDay;
|
|
1003
|
-
const list =
|
|
1004
|
-
|
|
1584
|
+
const list = map =>
|
|
1585
|
+
Object.entries(map)
|
|
1586
|
+
.map(([k, v]) => `${n(v)} ${sym(k)}`)
|
|
1587
|
+
.join(', ') || 'nothing';
|
|
1588
|
+
lines.push(
|
|
1589
|
+
`Each day players pay ${list(d.players.pay)} and get back ${list(d.players.getBack)} on average (items at the value you gave them), plus ${n(d.players.playerFeesSol)} SOL in player fees, ${n(d.players.oracleFeesSol)} SOL to MagicBlock’s oracle and ${n(d.players.networkFeesSol)} SOL in network fees.`,
|
|
1590
|
+
);
|
|
1005
1591
|
if (d.creator.oracleFeesSol) {
|
|
1006
1592
|
// Every roll the simulation counts as the project's: a draw, a roll a lottery's place or a grant gives, the authority's.
|
|
1007
1593
|
const what = [
|
|
1008
1594
|
sim.lotteries.length && 'lottery draws',
|
|
1009
|
-
m.lotteries.some(
|
|
1010
|
-
|
|
1011
|
-
|
|
1595
|
+
m.lotteries.some(l => (l.places ?? []).some(p => placeOf(p).prize?.gives === 'roll')) &&
|
|
1596
|
+
'the rolls lottery places give',
|
|
1597
|
+
sim.grants.some(
|
|
1598
|
+
g => g.claimsPerDay > 0 && prizeLine(m.grant.get(g.key)?.prize ?? m.grant.get(g.key)?.gives)?.gives === 'roll',
|
|
1599
|
+
) && 'the rolls grants give',
|
|
1600
|
+
m.decisions.some(x => x.does === 'roll' && Number(x.perDay) > 0) && 'the authority’s rolls',
|
|
1012
1601
|
].filter(Boolean);
|
|
1013
|
-
const words = what.length > 1 ? `${what.slice(0, -1).join(', ')} and ${what.at(-1)}` : what[0] ?? 'its rolls';
|
|
1014
|
-
lines.push(
|
|
1602
|
+
const words = what.length > 1 ? `${what.slice(0, -1).join(', ')} and ${what.at(-1)}` : (what[0] ?? 'its rolls');
|
|
1603
|
+
lines.push(
|
|
1604
|
+
`The oracle also takes about ${n(d.creator.oracleFeesSol)} SOL a day for ${words}, from whoever asks for them: anyone may (a player's page, or a ticket bought once a draw is due, sends it).`,
|
|
1605
|
+
);
|
|
1015
1606
|
}
|
|
1016
|
-
lines.push(
|
|
1607
|
+
lines.push(
|
|
1608
|
+
`The treasury gets ${list(d.creator.treasury)} a day; burned: ${list(d.burned)}. The platform’s fees: ${n(d.platform.totalSol)} SOL a day.`,
|
|
1609
|
+
);
|
|
1017
1610
|
for (const note of sim.notes) lines.push(note);
|
|
1018
1611
|
return lines.join('\n');
|
|
1019
1612
|
}
|