@trioforge/random 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +90 -2
- package/out/index.d.ts +24 -0
- package/out/init.luau +301 -0
- package/package.json +44 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @trioforge/random
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#6](https://github.com/Trio-Forge-Studio/packages/pull/6) [`dac820b`](https://github.com/Trio-Forge-Studio/packages/commit/dac820b44abb72c663f694c6125de44e1a1a8fcd) Thanks [@Hezaerd](https://github.com/Hezaerd)! - The source moved to [Trio-Forge-Studio/packages](https://github.com/Trio-Forge-Studio/packages), so the package's repository and issue links point there. The package now ships its `CHANGELOG.md`. The code is the same as the previous version.
|
|
8
|
+
|
|
9
|
+
## 0.1.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- First release. `Rng` is a seeded xoshiro128\*\* generator in plain Luau, so every client and server draws the same sequence from the same seed. It draws integers, floats, chances and normal values, and it picks by weight, shuffles and samples. A state can be saved, restored, cloned and forked. ([Trio-Forge-Studio/random#1](https://github.com/Trio-Forge-Studio/random/pull/1))
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Trio Forge Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,91 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @trioforge/random
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Seeded random numbers for roblox-ts. Every client and every server draws the same sequence from the same seed, today and after any engine update.
|
|
4
|
+
|
|
5
|
+
Roblox's `Random` doesn't promise that. Its algorithm and `Random:Shuffle` belong to the engine and can change between versions, which matters as soon as a server and its clients must agree on a draw. This package runs [xoshiro128\*\*](https://prng.di.unimi.it/) in plain Luau instead, and checks it against the algorithm's reference output.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @trioforge/random
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
roblox-ts only looks for packages in the scopes your project lists, so add the `@trioforge` scope in two places.
|
|
14
|
+
|
|
15
|
+
1. In `tsconfig.json`, add it to `typeRoots`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
"typeRoots": ["node_modules/@rbxts", "node_modules/@trioforge"]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
2. In your Rojo project, map it beside `@rbxts` under `rbxts_include/node_modules`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
"node_modules": {
|
|
25
|
+
"$className": "Folder",
|
|
26
|
+
"@rbxts": { "$path": "node_modules/@rbxts" },
|
|
27
|
+
"@trioforge": { "$path": "node_modules/@trioforge" }
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Example
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { Rng } from "@trioforge/random";
|
|
35
|
+
|
|
36
|
+
const rng = new Rng("chalet", 12345);
|
|
37
|
+
|
|
38
|
+
print(rng.int(1, 6));
|
|
39
|
+
// → 5
|
|
40
|
+
print(rng.pick(["lamp", "vase", "clock"]));
|
|
41
|
+
// → vase
|
|
42
|
+
print(rng.shuffle([1, 2, 3, 4, 5]).join(" "));
|
|
43
|
+
// → 5 4 1 2 3
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Seeds
|
|
47
|
+
|
|
48
|
+
`new Rng(...seed)` takes any mix of strings and integers and hashes them into a 128-bit state. The same parts in the same order always give the same stream. Different parts give unrelated streams, so a seed can name what it's for:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { Rng } from "@trioforge/random";
|
|
52
|
+
|
|
53
|
+
const houseSeed = 2024;
|
|
54
|
+
const furniture = new Rng("furniture", houseSeed);
|
|
55
|
+
const weather = new Rng("weather", houseSeed);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The two streams above don't move together, and drawing from one never changes the other. Seed numbers must be integers within ±2^53. `new Rng()` with no parts seeds itself from the clock and `math.random`, for draws nobody needs to repeat.
|
|
59
|
+
|
|
60
|
+
`fork()` takes four draws from a stream and seeds a new one from them. Hand a fork to a system that draws an unknown number of times, and the parent's sequence after it stays the same whatever the fork does.
|
|
61
|
+
|
|
62
|
+
`state()` returns the four 32-bit words of the stream's position. `Rng.fromState(state)` resumes from them, so a server can save a stream or send it to clients. `clone()` copies a stream at its current position.
|
|
63
|
+
|
|
64
|
+
## Draws
|
|
65
|
+
|
|
66
|
+
| Method | Returns |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `uint32()` | An integer from 0 to 2^32 − 1, the generator's raw output |
|
|
69
|
+
| `float()` | A number in [0, 1) with 53 random bits |
|
|
70
|
+
| `float(min, max)` | A number in [min, max) |
|
|
71
|
+
| `int(min, max)` | An integer in [min, max], both inclusive, without modulo bias |
|
|
72
|
+
| `chance(probability)` | `true` with the given probability |
|
|
73
|
+
| `normal(mean?, deviation?)` | A normally distributed number, by default with mean 0 and deviation 1 |
|
|
74
|
+
| `pick(items)` | One item |
|
|
75
|
+
| `weightedIndex(weights)` | An index, chosen in proportion to its weight |
|
|
76
|
+
| `weighted(items, weight)` | An item, chosen in proportion to `weight(item, index)` |
|
|
77
|
+
| `shuffle(items)` | A shuffled copy. The array you pass stays as it was |
|
|
78
|
+
| `sample(items, count)` | `count` distinct items, in the order they were drawn |
|
|
79
|
+
|
|
80
|
+
Indices are 0-based, as everywhere in roblox-ts. Weights must be finite and non-negative, with a positive total. A zero weight is never chosen.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { Rng } from "@trioforge/random";
|
|
84
|
+
|
|
85
|
+
const rng = new Rng("loot", 7);
|
|
86
|
+
const rarities = ["common", "rare", "epic"];
|
|
87
|
+
print(rarities[rng.weightedIndex([80, 18, 2])]);
|
|
88
|
+
// → common
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Each method's sequence for a given state is part of the package's contract. A change that would draw differently from the same seed is a breaking change.
|
package/out/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type SeedPart = string | number;
|
|
2
|
+
export type RngState = readonly [number, number, number, number];
|
|
3
|
+
export declare class Rng {
|
|
4
|
+
private a;
|
|
5
|
+
private b;
|
|
6
|
+
private c;
|
|
7
|
+
private d;
|
|
8
|
+
constructor(...seed: SeedPart[]);
|
|
9
|
+
static fromState(state: RngState): Rng;
|
|
10
|
+
state(): RngState;
|
|
11
|
+
clone(): Rng;
|
|
12
|
+
fork(): Rng;
|
|
13
|
+
uint32(): number;
|
|
14
|
+
float(): number;
|
|
15
|
+
float(min: number, max: number): number;
|
|
16
|
+
int(min: number, max: number): number;
|
|
17
|
+
chance(probability: number): boolean;
|
|
18
|
+
normal(mean?: number, deviation?: number): number;
|
|
19
|
+
pick<T extends defined>(items: readonly T[]): T;
|
|
20
|
+
weightedIndex(weights: readonly number[]): number;
|
|
21
|
+
weighted<T extends defined>(items: readonly T[], weight: (item: T, index: number) => number): T;
|
|
22
|
+
shuffle<T extends defined>(items: readonly T[]): T[];
|
|
23
|
+
sample<T extends defined>(items: readonly T[], count: number): T[];
|
|
24
|
+
}
|
package/out/init.luau
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local WORD = 2 ^ 32
|
|
3
|
+
local DOUBLE = 2 ^ 53
|
|
4
|
+
-- Luau has no 32-bit multiply, and a full product of two words overflows a double's 53 bits, so it splits one side.
|
|
5
|
+
local function imul(a, b)
|
|
6
|
+
local low = a * (b % 65536)
|
|
7
|
+
local high = ((a * math.floor(b / 65536)) % WORD) * 65536
|
|
8
|
+
return (low + high) % WORD
|
|
9
|
+
end
|
|
10
|
+
local function fmix(h)
|
|
11
|
+
h = imul(bit32.bxor(h, bit32.rshift(h, 16)), 0x85ebca6b)
|
|
12
|
+
h = imul(bit32.bxor(h, bit32.rshift(h, 13)), 0xc2b2ae35)
|
|
13
|
+
return bit32.bxor(h, bit32.rshift(h, 16))
|
|
14
|
+
end
|
|
15
|
+
local STRING = 0x5354524e
|
|
16
|
+
local NUMBER = 0x4e554d42
|
|
17
|
+
local function words(parts)
|
|
18
|
+
local result = { #parts }
|
|
19
|
+
for _, part in parts do
|
|
20
|
+
if type(part) == "string" then
|
|
21
|
+
local _arg1 = #part
|
|
22
|
+
-- ▼ Array.push ▼
|
|
23
|
+
table.insert(result, STRING)
|
|
24
|
+
table.insert(result, _arg1)
|
|
25
|
+
-- ▲ Array.push ▲
|
|
26
|
+
for index = 1, #part, 4 do
|
|
27
|
+
local a, b, c, d = string.byte(part, index, index + 3)
|
|
28
|
+
local _condition = a
|
|
29
|
+
if _condition == nil then
|
|
30
|
+
_condition = 0
|
|
31
|
+
end
|
|
32
|
+
local _condition_1 = b
|
|
33
|
+
if _condition_1 == nil then
|
|
34
|
+
_condition_1 = 0
|
|
35
|
+
end
|
|
36
|
+
local _exp = _condition + _condition_1 * 0x100
|
|
37
|
+
local _condition_2 = c
|
|
38
|
+
if _condition_2 == nil then
|
|
39
|
+
_condition_2 = 0
|
|
40
|
+
end
|
|
41
|
+
local _exp_1 = _exp + _condition_2 * 0x10000
|
|
42
|
+
local _condition_3 = d
|
|
43
|
+
if _condition_3 == nil then
|
|
44
|
+
_condition_3 = 0
|
|
45
|
+
end
|
|
46
|
+
table.insert(result, _exp_1 + _condition_3 * 0x1000000)
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
if part ~= part or part % 1 ~= 0 or math.abs(part) > DOUBLE then
|
|
50
|
+
error(`a seed number must be an integer within ±2^53, got {part}`, 3)
|
|
51
|
+
end
|
|
52
|
+
local _arg1 = part % WORD
|
|
53
|
+
local _arg2 = math.floor(part / WORD) % WORD
|
|
54
|
+
-- ▼ Array.push ▼
|
|
55
|
+
table.insert(result, NUMBER)
|
|
56
|
+
table.insert(result, _arg1)
|
|
57
|
+
table.insert(result, _arg2)
|
|
58
|
+
-- ▲ Array.push ▲
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
return result
|
|
62
|
+
end
|
|
63
|
+
local LANES = { 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a }
|
|
64
|
+
local function hashState(input)
|
|
65
|
+
-- ▼ ReadonlyArray.map ▼
|
|
66
|
+
local _newValue = table.create(#LANES)
|
|
67
|
+
local _callback = function(lane)
|
|
68
|
+
local h = lane
|
|
69
|
+
for _, word in input do
|
|
70
|
+
h = fmix((imul(bit32.bxor(h, word), 0x9e3779b1) + lane) % WORD)
|
|
71
|
+
end
|
|
72
|
+
return fmix(h)
|
|
73
|
+
end
|
|
74
|
+
for _k, _v in LANES do
|
|
75
|
+
_newValue[_k] = _callback(_v, _k - 1, LANES)
|
|
76
|
+
end
|
|
77
|
+
-- ▲ ReadonlyArray.map ▲
|
|
78
|
+
local state = _newValue
|
|
79
|
+
-- ▼ ReadonlyArray.every ▼
|
|
80
|
+
local _result = true
|
|
81
|
+
local _callback_1 = function(word)
|
|
82
|
+
return word == 0
|
|
83
|
+
end
|
|
84
|
+
for _k, _v in state do
|
|
85
|
+
if not _callback_1(_v, _k - 1, state) then
|
|
86
|
+
_result = false
|
|
87
|
+
break
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
-- ▲ ReadonlyArray.every ▲
|
|
91
|
+
if _result then
|
|
92
|
+
state[1] = 1
|
|
93
|
+
end
|
|
94
|
+
return { state[1], state[2], state[3], state[4] }
|
|
95
|
+
end
|
|
96
|
+
local function entropy()
|
|
97
|
+
return { math.random(0, 0xffff), math.random(0, 0xffff), math.random(0, 0xffff), math.floor(os.clock() * 1e6) % WORD }
|
|
98
|
+
end
|
|
99
|
+
local Rng
|
|
100
|
+
do
|
|
101
|
+
Rng = setmetatable({}, {
|
|
102
|
+
__tostring = function()
|
|
103
|
+
return "Rng"
|
|
104
|
+
end,
|
|
105
|
+
})
|
|
106
|
+
Rng.__index = Rng
|
|
107
|
+
function Rng.new(...)
|
|
108
|
+
local self = setmetatable({}, Rng)
|
|
109
|
+
return self:constructor(...) or self
|
|
110
|
+
end
|
|
111
|
+
function Rng:constructor(...)
|
|
112
|
+
local seed = { ... }
|
|
113
|
+
local _binding = hashState(if #seed > 0 then words(seed) else entropy())
|
|
114
|
+
self.a = _binding[1]
|
|
115
|
+
self.b = _binding[2]
|
|
116
|
+
self.c = _binding[3]
|
|
117
|
+
self.d = _binding[4]
|
|
118
|
+
end
|
|
119
|
+
function Rng:fromState(state)
|
|
120
|
+
local rng = Rng.new(0)
|
|
121
|
+
for _, word in state do
|
|
122
|
+
if word % 1 ~= 0 or word < 0 or word >= WORD then
|
|
123
|
+
error(`a state word must be a uint32, got {word}`, 2)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
-- ▼ ReadonlyArray.every ▼
|
|
127
|
+
local _result = true
|
|
128
|
+
local _callback = function(word)
|
|
129
|
+
return word == 0
|
|
130
|
+
end
|
|
131
|
+
for _k, _v in state do
|
|
132
|
+
if not _callback(_v, _k - 1, state) then
|
|
133
|
+
_result = false
|
|
134
|
+
break
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
-- ▲ ReadonlyArray.every ▲
|
|
138
|
+
if _result then
|
|
139
|
+
error("a state can't be all zeroes", 2)
|
|
140
|
+
end
|
|
141
|
+
local _binding = state
|
|
142
|
+
rng.a = _binding[1]
|
|
143
|
+
rng.b = _binding[2]
|
|
144
|
+
rng.c = _binding[3]
|
|
145
|
+
rng.d = _binding[4]
|
|
146
|
+
return rng
|
|
147
|
+
end
|
|
148
|
+
function Rng:state()
|
|
149
|
+
return { self.a, self.b, self.c, self.d }
|
|
150
|
+
end
|
|
151
|
+
function Rng:clone()
|
|
152
|
+
return Rng:fromState(self:state())
|
|
153
|
+
end
|
|
154
|
+
function Rng:fork()
|
|
155
|
+
return Rng.new(self:uint32(), self:uint32(), self:uint32(), self:uint32())
|
|
156
|
+
end
|
|
157
|
+
function Rng:uint32()
|
|
158
|
+
local result = (bit32.lrotate((self.b * 5) % WORD, 7) * 9) % WORD
|
|
159
|
+
local t = bit32.lshift(self.b, 9)
|
|
160
|
+
self.c = bit32.bxor(self.c, self.a)
|
|
161
|
+
self.d = bit32.bxor(self.d, self.b)
|
|
162
|
+
self.b = bit32.bxor(self.b, self.c)
|
|
163
|
+
self.a = bit32.bxor(self.a, self.d)
|
|
164
|
+
self.c = bit32.bxor(self.c, t)
|
|
165
|
+
self.d = bit32.lrotate(self.d, 11)
|
|
166
|
+
return result
|
|
167
|
+
end
|
|
168
|
+
function Rng:float(min, max)
|
|
169
|
+
local unit = (bit32.rshift(self:uint32(), 5) * 2 ^ 26 + bit32.rshift(self:uint32(), 6)) / DOUBLE
|
|
170
|
+
if min == nil or max == nil then
|
|
171
|
+
return unit
|
|
172
|
+
end
|
|
173
|
+
if not (min <= max) then
|
|
174
|
+
error(`float needs min <= max, got {min} and {max}`, 2)
|
|
175
|
+
end
|
|
176
|
+
return min + unit * (max - min)
|
|
177
|
+
end
|
|
178
|
+
function Rng:int(min, max)
|
|
179
|
+
if min % 1 ~= 0 or max % 1 ~= 0 then
|
|
180
|
+
error(`int needs integer bounds, got {min} and {max}`, 2)
|
|
181
|
+
end
|
|
182
|
+
local span = max - min + 1
|
|
183
|
+
if span < 1 then
|
|
184
|
+
error(`int needs min <= max, got {min} and {max}`, 2)
|
|
185
|
+
end
|
|
186
|
+
if span > WORD then
|
|
187
|
+
error(`int spans at most 2^32 values, got {span}`, 2)
|
|
188
|
+
end
|
|
189
|
+
local limit = WORD - (WORD % span)
|
|
190
|
+
local value = self:uint32()
|
|
191
|
+
while value >= limit do
|
|
192
|
+
value = self:uint32()
|
|
193
|
+
end
|
|
194
|
+
return min + (value % span)
|
|
195
|
+
end
|
|
196
|
+
function Rng:chance(probability)
|
|
197
|
+
return self:float() < probability
|
|
198
|
+
end
|
|
199
|
+
function Rng:normal(mean, deviation)
|
|
200
|
+
if mean == nil then
|
|
201
|
+
mean = 0
|
|
202
|
+
end
|
|
203
|
+
if deviation == nil then
|
|
204
|
+
deviation = 1
|
|
205
|
+
end
|
|
206
|
+
local radius = math.sqrt(-2 * math.log(1 - self:float()))
|
|
207
|
+
return mean + deviation * radius * math.cos(2 * math.pi * self:float())
|
|
208
|
+
end
|
|
209
|
+
function Rng:pick(items)
|
|
210
|
+
if #items == 0 then
|
|
211
|
+
error("can't pick from an empty array", 2)
|
|
212
|
+
end
|
|
213
|
+
return items[self:int(0, #items - 1) + 1]
|
|
214
|
+
end
|
|
215
|
+
function Rng:weightedIndex(weights)
|
|
216
|
+
local total = 0
|
|
217
|
+
for _, weight in weights do
|
|
218
|
+
if not (weight >= 0 and weight < math.huge) then
|
|
219
|
+
error(`a weight must be finite and non-negative, got {weight}`, 2)
|
|
220
|
+
end
|
|
221
|
+
total += weight
|
|
222
|
+
end
|
|
223
|
+
if total <= 0 then
|
|
224
|
+
error("weights must add up to more than zero", 2)
|
|
225
|
+
end
|
|
226
|
+
local roll = self:float() * total
|
|
227
|
+
local last = 0
|
|
228
|
+
for index = 0, #weights - 1 do
|
|
229
|
+
if weights[index + 1] <= 0 then
|
|
230
|
+
continue
|
|
231
|
+
end
|
|
232
|
+
last = index
|
|
233
|
+
roll -= weights[index + 1]
|
|
234
|
+
if roll < 0 then
|
|
235
|
+
return index
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
-- Rounding can leave a sliver of the roll past the last bucket.
|
|
239
|
+
return last
|
|
240
|
+
end
|
|
241
|
+
function Rng:weighted(items, weight)
|
|
242
|
+
local _exp = items
|
|
243
|
+
local _self = self
|
|
244
|
+
local _items = items
|
|
245
|
+
local _weight = weight
|
|
246
|
+
-- ▼ ReadonlyArray.map ▼
|
|
247
|
+
local _newValue = table.create(#_items)
|
|
248
|
+
for _k, _v in _items do
|
|
249
|
+
_newValue[_k] = _weight(_v, _k - 1, _items)
|
|
250
|
+
end
|
|
251
|
+
-- ▲ ReadonlyArray.map ▲
|
|
252
|
+
return _exp[_self:weightedIndex(_newValue) + 1]
|
|
253
|
+
end
|
|
254
|
+
function Rng:shuffle(items)
|
|
255
|
+
local _array = {}
|
|
256
|
+
local _length = #_array
|
|
257
|
+
table.move(items, 1, #items, _length + 1, _array)
|
|
258
|
+
local result = _array
|
|
259
|
+
for index = #result - 1, 1, -1 do
|
|
260
|
+
local other = self:int(0, index)
|
|
261
|
+
local _index = index + 1
|
|
262
|
+
local _index_1 = other + 1
|
|
263
|
+
result[_index], result[_index_1] = result[other + 1], result[index + 1]
|
|
264
|
+
end
|
|
265
|
+
return result
|
|
266
|
+
end
|
|
267
|
+
function Rng:sample(items, count)
|
|
268
|
+
if count % 1 ~= 0 or count < 0 or count > #items then
|
|
269
|
+
error(`sample needs a count from 0 to {#items}, got {count}`, 2)
|
|
270
|
+
end
|
|
271
|
+
local _array = {}
|
|
272
|
+
local _length = #_array
|
|
273
|
+
table.move(items, 1, #items, _length + 1, _array)
|
|
274
|
+
local pool = _array
|
|
275
|
+
local result = {}
|
|
276
|
+
do
|
|
277
|
+
local index = 0
|
|
278
|
+
local _shouldIncrement = false
|
|
279
|
+
while true do
|
|
280
|
+
if _shouldIncrement then
|
|
281
|
+
index += 1
|
|
282
|
+
else
|
|
283
|
+
_shouldIncrement = true
|
|
284
|
+
end
|
|
285
|
+
if not (index < count) then
|
|
286
|
+
break
|
|
287
|
+
end
|
|
288
|
+
local other = self:int(index, #pool - 1)
|
|
289
|
+
local _index = index + 1
|
|
290
|
+
local _index_1 = other + 1
|
|
291
|
+
pool[_index], pool[_index_1] = pool[other + 1], pool[index + 1]
|
|
292
|
+
local _arg0 = pool[index + 1]
|
|
293
|
+
table.insert(result, _arg0)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
return result
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
return {
|
|
300
|
+
Rng = Rng,
|
|
301
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trioforge/random",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Seeded, deterministic random numbers for roblox-ts that every client and server draws the same",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"roblox",
|
|
7
|
+
"roblox-ts",
|
|
8
|
+
"random",
|
|
9
|
+
"rng",
|
|
10
|
+
"prng",
|
|
11
|
+
"seed",
|
|
12
|
+
"xoshiro"
|
|
13
|
+
],
|
|
14
|
+
"bugs": "https://github.com/Trio-Forge-Studio/packages/issues",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/Trio-Forge-Studio/packages.git",
|
|
18
|
+
"directory": "packages/random"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "out/init.luau",
|
|
23
|
+
"types": "out/index.d.ts",
|
|
24
|
+
"files": [
|
|
25
|
+
"out",
|
|
26
|
+
"!**/*.tsbuildinfo",
|
|
27
|
+
"README.md",
|
|
28
|
+
"CHANGELOG.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@rbxts/compiler-types": "3.0.0-types.0",
|
|
36
|
+
"@rbxts/types": "^1.0.958",
|
|
37
|
+
"@types/node": "^24.19.2",
|
|
38
|
+
"roblox-ts": "^3.0.0",
|
|
39
|
+
"typescript": "5.5.3",
|
|
40
|
+
"vite-plus": "1.3.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "rbxtsc --type package",
|
|
44
|
+
"test": "vp test",
|
|
45
|
+
"check": "tsc -p test && vp test"
|
|
46
|
+
}
|
|
6
47
|
}
|