@xgamesfun/solana 0.20.1 → 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 +95 -52
- package/bin/xsolana.mjs +23 -8
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +4 -12
- package/examples/duel-arena.json +1 -4
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/fair-presale.json +1 -3
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +15 -6
- 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/merge-2048.json +5 -5
- package/examples/nft-raffle.json +1 -0
- package/examples/pack-league.json +108 -0
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +20 -7
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +4 -15
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/examples/stride-sneakers.json +164 -0
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +122 -52
- 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 +290 -73
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +24 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +35 -7
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +124 -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 +44 -16
- 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 +653 -131
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +267 -83
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2406 -2176
- package/js/client/index.js +1 -0
- package/js/client/ix.js +3059 -664
- package/js/client/pda.js +39 -33
- package/js/client/policy.js +52 -24
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +414 -16
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +4153 -725
- package/js/economy/cost.mjs +403 -64
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +466 -111
- package/js/economy/guide.mjs +296 -72
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +83 -19
- package/js/economy/model.mjs +462 -124
- package/js/economy/plan.mjs +1306 -219
- package/js/economy/schema.json +390 -484
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +883 -223
- 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 +307 -88
- package/js/play/index.js +2864 -619
- package/js/play/keys.js +193 -0
- 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 +39 -21
- package/skills/xsolana-economy/references/after-launch.md +58 -38
- package/skills/xsolana-economy/references/apps.md +97 -50
- package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
- package/skills/xsolana-economy/references/telling-the-person.md +61 -17
- package/types/chain/core.d.ts +4 -2
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +217 -56
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +602 -549
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3884 -247
- package/types/client/pda.d.ts +22 -16
- package/types/client/policy.d.ts +39 -14
- package/types/client/random.d.ts +26 -0
- package/types/client/sizes.d.ts +93 -5
- package/types/economy/check.d.mts +5 -10
- package/types/economy/cost.d.mts +2 -1
- package/types/economy/guide.d.mts +5 -0
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +151 -38
- package/types/economy/plan.d.mts +237 -194
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +453 -53
- package/types/play/keys.d.ts +51 -0
- 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/client/pda.js
CHANGED
|
@@ -16,18 +16,17 @@ export const BUBBLEGUM_PROGRAM = 'BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY';
|
|
|
16
16
|
export const COMPRESSION_PROGRAM = 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW';
|
|
17
17
|
export const NOOP_PROGRAM = 'mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3';
|
|
18
18
|
|
|
19
|
-
const seed =
|
|
20
|
-
const key =
|
|
21
|
-
const u8 =
|
|
22
|
-
const u16 =
|
|
23
|
-
const u32 =
|
|
24
|
-
const u64 =
|
|
25
|
-
const i64 =
|
|
19
|
+
const seed = s => utf8(s);
|
|
20
|
+
const key = a => addressBytes(a);
|
|
21
|
+
const u8 = n => uintLE(n, 1);
|
|
22
|
+
const u16 = n => uintLE(n, 2);
|
|
23
|
+
const u32 = n => uintLE(n, 4);
|
|
24
|
+
const u64 = n => uintLE(n, 8);
|
|
25
|
+
const i64 = n => intLE(n, 8);
|
|
26
26
|
|
|
27
27
|
/** The PDA of these seeds under the loot program, with its bump. */
|
|
28
28
|
export const lootAddress = (seeds, program = LOOT_PROGRAM) => findProgramAddress(seeds, program);
|
|
29
29
|
const at = (...seeds) => lootAddress(seeds)[0];
|
|
30
|
-
const keyReceipt = (grant, incarnation, voucherKey) => at(seed('code'), key(grant), u64(incarnation), key(voucherKey));
|
|
31
30
|
|
|
32
31
|
export const pda = {
|
|
33
32
|
platform: () => at(seed('platform')),
|
|
@@ -35,12 +34,16 @@ export const pda = {
|
|
|
35
34
|
* new owner keeps the address. */
|
|
36
35
|
project: (creator, id = 0) => at(seed('project'), key(creator), u64(id)),
|
|
37
36
|
/** The project's fee box: where the platform's fees on its transactions wait. */
|
|
38
|
-
fees:
|
|
39
|
-
pool: (project, index) => at(seed('pool'), key(project),
|
|
37
|
+
fees: project => at(seed('fees'), key(project)),
|
|
38
|
+
pool: (project, index) => at(seed('pool'), key(project), u16(index)),
|
|
40
39
|
item: (project, id) => at(seed('item'), key(project), u32(id)),
|
|
41
40
|
box: (project, id) => at(seed('box'), key(project), u32(id)),
|
|
42
41
|
/** NFTs the project holds to give away as prizes: a box entry of a stash gives one of them at random. */
|
|
43
42
|
stash: (project, id) => at(seed('stash'), key(project), u32(id)),
|
|
43
|
+
/** A table of weighted rows the project writes and seals (a card among thousands, a trait's values), or commits by a Merkle root. */
|
|
44
|
+
table: (project, id) => at(seed('table'), key(project), u32(id)),
|
|
45
|
+
/** What the project keeps about a wallet (its player's profile: XP, level, rank). */
|
|
46
|
+
profile: (project, wallet) => at(seed('profile'), key(project), key(wallet)),
|
|
44
47
|
schedule: (project, id) => at(seed('schedule'), key(project), u32(id)),
|
|
45
48
|
lottery: (project, id) => at(seed('lottery'), key(project), u32(id)),
|
|
46
49
|
recipe: (project, id) => at(seed('recipe'), key(project), u32(id)),
|
|
@@ -53,48 +56,51 @@ export const pda = {
|
|
|
53
56
|
/** Every collectible the program mints of its own accord (an order's, a prize's, an auction's lot): its address
|
|
54
57
|
* names its item and its serial, minted once at most. */
|
|
55
58
|
itemAsset: (item, serial) => at(seed('asset'), key(item), u32(serial)),
|
|
56
|
-
/** A contest's pot
|
|
57
|
-
pot:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
/** One
|
|
61
|
-
|
|
62
|
-
/** A
|
|
63
|
-
|
|
59
|
+
/** A contest's pot: `["pot", contest]`. */
|
|
60
|
+
pot: record => at(seed('pot'), key(record)),
|
|
61
|
+
/** A lottery's pot `id` (below `MAX_POTS`), kept from round to round: `["pot", lottery, id]`. */
|
|
62
|
+
lotteryPot: (lottery, id) => at(seed('pot'), key(lottery), u8(id)),
|
|
63
|
+
/** One round of a lottery: its record, opened by the round's first purchase (`["round", lottery, incarnation, round]`). */
|
|
64
|
+
round: (lottery, incarnation, round) => at(seed('round'), key(lottery), u64(incarnation), u32(round)),
|
|
65
|
+
/** A lottery round's own token account of `mint`, which the round's record owns. */
|
|
66
|
+
roundTokens: (round, mint) => at(seed('round-tokens'), key(round), key(mint)),
|
|
67
|
+
/** A contest's list of entries, one record a player (page 0). */
|
|
68
|
+
entries: (contest, incarnation, page = 0) => at(seed('entries'), key(contest), u64(incarnation), u8(page)),
|
|
64
69
|
/** A hold names its NFT and its end (Unix seconds): a release sent late never finds a later hold of the NFT. */
|
|
65
70
|
hold: (asset, freeAt) => at(seed('hold'), key(asset), i64(freeAt)),
|
|
66
71
|
/** An offer: an id is its maker's own. Its money waits in the project's vault for its mint (`vault`). */
|
|
67
72
|
offer: (project, maker, id) => at(seed('offer'), key(project), key(maker), u32(id)),
|
|
68
73
|
/** A lottery's own stash: the NFTs its places promise (`Prize::Held`), which leave only as its prizes. */
|
|
69
|
-
lotteryStash:
|
|
74
|
+
lotteryStash: lottery => at(seed('lottery_stash'), key(lottery)),
|
|
70
75
|
/** A contest's prize split by stake, set aside when it is decided: `["prize", contest]`. */
|
|
71
|
-
prizeVault:
|
|
76
|
+
prizeVault: contest => at(seed('prize'), key(contest)),
|
|
72
77
|
/** A sealed schedule's own vault, which only its runs pay out of: `["vest", schedule]`. */
|
|
73
|
-
vest:
|
|
78
|
+
vest: schedule => at(seed('vest'), key(schedule)),
|
|
74
79
|
/** A listing of an NFT for sale or for rent: one per NFT a project holds, so its address names the NFT. */
|
|
75
80
|
listing: (project, asset) => at(seed('listing'), key(project), key(asset)),
|
|
76
|
-
staked:
|
|
77
|
-
rewards:
|
|
81
|
+
staked: farm => at(seed('staked'), key(farm)),
|
|
82
|
+
rewards: farm => at(seed('rewards'), key(farm)),
|
|
83
|
+
/** A farm's record of when its seats stop earning, for a farm that keeps one (`Farm.keeps_ends`). */
|
|
84
|
+
ends: farm => at(seed('ends'), key(farm)),
|
|
78
85
|
/** A seat: a player's in a farm of tokens, a collectible's (its asset) in a farm of collectibles. */
|
|
79
86
|
seat: (farm, by) => at(seed('seat'), key(farm), key(by)),
|
|
80
87
|
/** What the project writes about an NFT it has no authority over (its Sheet): `nft` is a Core asset, a Token Metadata mint or a compressed NFT's asset id. */
|
|
81
88
|
sheet: (project, nft) => at(seed('sheet'), key(project), key(nft)),
|
|
82
89
|
/** A compressed NFT as last proven (`prove_leaf`), read while the leaf is unchanged and its root is still in the tree's change log. */
|
|
83
|
-
leaf:
|
|
90
|
+
leaf: asset => at(seed('leaf'), key(asset)),
|
|
84
91
|
/** The root of the traits the project vouches for, under its own `key` (a collection's address, by convention). */
|
|
85
92
|
traits: (project, key_) => at(seed('traits'), key(project), key(key_)),
|
|
86
93
|
/** Bubblegum's config of a tree: `[tree]`. */
|
|
87
|
-
treeConfig:
|
|
94
|
+
treeConfig: tree => findProgramAddress([key(tree)], BUBBLEGUM_PROGRAM)[0],
|
|
88
95
|
/** A compressed NFT's asset id: Bubblegum's `["asset", tree, nonce]`, `nonce` the count minted before it. */
|
|
89
96
|
compressedAsset: (tree, nonce) => findProgramAddress([seed('asset'), key(tree), u64(nonce)], BUBBLEGUM_PROGRAM)[0],
|
|
90
|
-
/** A wallet's
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
code: keyReceipt,
|
|
97
|
+
/** A wallet's counts in a project: how many times it bought from each box, crafted each recipe or held tickets in
|
|
98
|
+
* a lottery's round that limits it (`decodeCounts`). */
|
|
99
|
+
counts: (project, wallet) => at(seed('counts'), key(project), key(wallet)),
|
|
100
|
+
/** Shard `shard` of a grant's limit records: what each wallet, person, badge and voucher key claimed. */
|
|
101
|
+
claims: (grant, incarnation, shard) => at(seed('claims'), key(grant), u64(incarnation), u8(shard)),
|
|
102
|
+
/** Shard `shard` of the rounds a grant paid. */
|
|
103
|
+
rounds: (grant, incarnation, shard) => at(seed('rounds'), key(grant), u64(incarnation), u8(shard)),
|
|
98
104
|
/** The program's identity, which signs its requests to the oracle. */
|
|
99
105
|
identity: () => at(seed('identity')),
|
|
100
106
|
/** The identity the oracle signs callbacks with: the VRF program's PDA for this program. */
|
package/js/client/policy.js
CHANGED
|
@@ -3,28 +3,46 @@
|
|
|
3
3
|
import IDL from './idl/loot.js';
|
|
4
4
|
|
|
5
5
|
const constant = name => BigInt(IDL.constants.find(value => value.name === name).value);
|
|
6
|
-
|
|
7
|
-
export const
|
|
8
|
-
|
|
6
|
+
/** How long the oracle has to answer before a roll or a draw may be asked again, and a pending order refunded (seconds). */
|
|
7
|
+
export const STALE_SECONDS = constant('STALE_SECONDS');
|
|
8
|
+
/** How long after a lottery's first ask the next ask cancels its round, and an asked order holding a payment or an NFT
|
|
9
|
+
* may be refunded (seconds). */
|
|
10
|
+
export const LOTTERY_TIMEOUT_SECONDS = constant('LOTTERY_TIMEOUT_SECONDS');
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* The earliest
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* The earliest an order can be refunded, as `{ slot, at }` (the program wants a later slot than `slot` and a later
|
|
14
|
+
* second than `at`), or null when it never can be as it stands: a failed order from the next slot; a paid order the
|
|
15
|
+
* oracle never answered 10 minutes after it was asked (`STALE_SECONDS`); an asked roll holding a payment or an NFT 7 days
|
|
16
|
+
* after its first ask. A rolled order has an outcome to carry out, and is settled. A waiting roll, or an asked one holding
|
|
17
|
+
* nothing, is asked (again): `rollAskable`.
|
|
14
18
|
*/
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
-
if (order.status === 'Failed')
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return (deadline > U64_MAX ? U64_MAX : deadline) + 1n;
|
|
19
|
+
export function orderRefundAt(order) {
|
|
20
|
+
const slot = BigInt(order.requested_slot);
|
|
21
|
+
if (order.status === 'Failed') return { slot, at: null };
|
|
22
|
+
if (order.status === 'Pending') return { slot, at: BigInt(order.asked_at) + STALE_SECONDS };
|
|
23
|
+
if (order.status === 'Asked' && (BigInt(order.paid ?? 0) > 0n || order.held))
|
|
24
|
+
return { slot: null, at: BigInt(order.first_asked_at) + LOTTERY_TIMEOUT_SECONDS };
|
|
25
|
+
if (['Rolled', 'Waiting', 'Asked'].includes(order.status)) return null;
|
|
26
|
+
throw new Error('Unknown order status.');
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
/** Whether
|
|
27
|
-
export
|
|
29
|
+
/** Whether an order can be refunded at `slot` and `now` (Unix seconds, the chain's clock): `orderRefundAt`. */
|
|
30
|
+
export function orderRefundable(order, slot, now) {
|
|
31
|
+
const due = orderRefundAt(order);
|
|
32
|
+
if (!due) return false;
|
|
33
|
+
return (due.slot === null || BigInt(slot) > due.slot) && (due.at === null || BigInt(now) > due.at);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether its player or the project's keys may ask the oracle for a mission's or a recipe's roll at `slot` and `now`
|
|
38
|
+
* (Unix seconds, the chain's clock): never asked, or never answered within `STALE_SECONDS`. Once asked, a later ask comes
|
|
39
|
+
* in a later slot than the last (`SameSlot`: never the same seed twice), so a roll that found nothing in stock and waits
|
|
40
|
+
* again is asked from the next slot on.
|
|
41
|
+
*/
|
|
42
|
+
export const rollAskable = (order, slot, now) =>
|
|
43
|
+
(order.status === 'Waiting' &&
|
|
44
|
+
(BigInt(order.asked_first ?? 0) === 0n || BigInt(slot) > BigInt(order.requested_slot))) ||
|
|
45
|
+
(order.status === 'Asked' && BigInt(now) > BigInt(order.asked_at) + STALE_SECONDS);
|
|
28
46
|
|
|
29
47
|
/**
|
|
30
48
|
* Whether a contest is off at `now` (Unix seconds), as the program reads its verdict: called off, its result
|
|
@@ -34,15 +52,25 @@ export function contestOff(contest, now) {
|
|
|
34
52
|
if (contest.verdict === 'Off') return true;
|
|
35
53
|
if (contest.verdict !== 'Open') return false;
|
|
36
54
|
const total = contest.sides.reduce((sum, side) => sum + BigInt(side), 0n);
|
|
37
|
-
return
|
|
55
|
+
return (
|
|
56
|
+
BigInt(now) > BigInt(contest.result_by) ||
|
|
57
|
+
(BigInt(now) >= BigInt(contest.closes_at) && total < BigInt(contest.min_pot ?? 0))
|
|
58
|
+
);
|
|
38
59
|
}
|
|
39
60
|
|
|
40
|
-
/** Whether a lottery's draw was asked for and the oracle has not answered within
|
|
41
|
-
|
|
61
|
+
/** Whether a lottery round's draw (`round`: its decoded record) was asked for and the oracle has not answered within
|
|
62
|
+
* `STALE_SECONDS` by `now` (Unix seconds, the chain's clock): anyone may ask again. */
|
|
63
|
+
export const lotteryDrawStuck = (round, now) =>
|
|
64
|
+
round?.status === 'Drawing' && BigInt(now) > BigInt(round.asked_at) + STALE_SECONDS;
|
|
42
65
|
|
|
43
66
|
/**
|
|
44
|
-
* Whether a lottery's draw was asked for and the oracle has not answered within
|
|
45
|
-
* ask (asking again doesn't move it)
|
|
46
|
-
*
|
|
67
|
+
* Whether a lottery round's draw (`round`: its decoded record) was asked for and the oracle has not answered within
|
|
68
|
+
* `LOTTERY_TIMEOUT_SECONDS` of the first ask (asking again doesn't move it) by `now` (Unix seconds, the chain's clock):
|
|
69
|
+
* the next ask of that round (`drawLotteryIx` with its address) cancels it, and every entry is refunded. A round nobody
|
|
70
|
+
* asked to draw is never cancelled.
|
|
47
71
|
*/
|
|
48
|
-
export const lotteryCancellable = (
|
|
72
|
+
export const lotteryCancellable = (round, now) =>
|
|
73
|
+
round?.status === 'Drawing' && BigInt(now) > BigInt(round.first_asked_at) + LOTTERY_TIMEOUT_SECONDS;
|
|
74
|
+
|
|
75
|
+
/** A farm seat's `earns_until` when nothing ends its earning (i64::MAX): no tier term and no fuel it reads. */
|
|
76
|
+
export const NO_END = (1n << 63n) - 1n;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The loot program's rolling module (`src/random`), mirrored: every random choice it makes is drawn from a stream of
|
|
2
|
+
// the oracle's answer, by a path that names what is drawn, with one unbiased sampler. A page shows from an order's
|
|
3
|
+
// answer what each draw gave (and a fresh unit's range traits) exactly as the program drew them. Held to the
|
|
4
|
+
// program's own vectors (`js/test/fixtures/roll-vectors.json`, from gamesfun-loot's `tests/fixtures`).
|
|
5
|
+
import { concat, uintLE } from '../chain/bytes.js';
|
|
6
|
+
import { sha256 } from '../chain/sha256.js';
|
|
7
|
+
|
|
8
|
+
/** What a path's first word names (`random::tag`): an entry of a box, a stash's NFT, a lottery's ticket, a fresh trait. */
|
|
9
|
+
export const TAG = Object.freeze({ ENTRY: 1, STASH: 2, LOTTERY: 3, TRAIT: 4 });
|
|
10
|
+
/** The most words a path holds. */
|
|
11
|
+
export const MAX_PATH = 8;
|
|
12
|
+
const U64 = (1n << 64n) - 1n;
|
|
13
|
+
const PREFIX = new TextEncoder().encode('loot-roll-v1');
|
|
14
|
+
|
|
15
|
+
/** A stream of the oracle's 32-byte `answer` at `path` (whole numbers): seed = sha256("loot-roll-v1" | answer | path as
|
|
16
|
+
* u32 little-endian words); its nth number the first 8 bytes, little-endian, of sha256(seed | n as u64 little-endian). */
|
|
17
|
+
export function stream(answer, path = []) {
|
|
18
|
+
const words = path.slice(0, MAX_PATH).map(w => uintLE(w, 4));
|
|
19
|
+
const seed = sha256(concat(PREFIX, Uint8Array.from(answer), ...words));
|
|
20
|
+
let counter = 0n;
|
|
21
|
+
return {
|
|
22
|
+
next() {
|
|
23
|
+
const block = sha256(concat(seed, uintLE(counter, 8)));
|
|
24
|
+
counter += 1n;
|
|
25
|
+
return block.subarray(0, 8).reduceRight((n, b) => (n << 8n) | BigInt(b), 0n);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A number from 0 to `n` - 1, every one as likely: a draw past the last whole multiple of `n` is drawn again. null for
|
|
31
|
+
* `n` 0. */
|
|
32
|
+
export function uniform(s, n) {
|
|
33
|
+
n = BigInt(n);
|
|
34
|
+
if (n === 0n) return null;
|
|
35
|
+
const limit = U64 - (((1n << 64n) - n) % n);
|
|
36
|
+
for (;;) {
|
|
37
|
+
const roll = s.next();
|
|
38
|
+
if (roll <= limit) return roll % n;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The row `at` lands on, the weights walked in order. null past the last. */
|
|
43
|
+
export function walk(weights, at) {
|
|
44
|
+
let left = BigInt(at);
|
|
45
|
+
for (const [i, w] of weights.entries()) {
|
|
46
|
+
if (left < BigInt(w)) return i;
|
|
47
|
+
left -= BigInt(w);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A row by weight (a weight of 0 never drawn); null when none weighs anything, or they sum past 64 bits. */
|
|
53
|
+
export function weighted(s, weights) {
|
|
54
|
+
const total = weights.reduce((n, w) => n + BigInt(w), 0n);
|
|
55
|
+
if (total > U64) return null;
|
|
56
|
+
const at = uniform(s, total);
|
|
57
|
+
return at === null ? null : walk(weights, at);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A number from `min` to `max`, both included, every one as likely (either order). */
|
|
61
|
+
export function range(s, min, max) {
|
|
62
|
+
let [lo, hi] = [BigInt(min), BigInt(max)];
|
|
63
|
+
if (lo > hi) [lo, hi] = [hi, lo];
|
|
64
|
+
const n = hi - lo + 1n;
|
|
65
|
+
return n > U64 ? s.next() : lo + uniform(s, n);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A fresh unit's range trait `t` (its place among the item's traits) as the program draws it at the mint: from the
|
|
69
|
+
* order's `answer`, `at` the unit's place in the order's `won`. */
|
|
70
|
+
export const rangeTrait = (answer, at, t, min, max) => range(stream(answer, [TAG.TRAIT, at, t]), min, max);
|