Skip to content

Commit

Permalink
feat: Add PRNG
Browse files Browse the repository at this point in the history
  • Loading branch information
PartMan7 committed Oct 28, 2024
1 parent 88604d7 commit 6aaf8b8
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 14 deletions.
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"lint:fix": "eslint src --ext .ts --fix",
"notify-unpushed": "sh ./scripts/notify-unpushed.sh",
"prepare": "sh ./scripts/setup.sh",
"start": "ts-node --transpileOnly src/index.ts",
"start": "ts-node src/index.ts",
"test": "npm run lint && npm run tsc",
"tsc": "tsc"
},
Expand Down
35 changes: 22 additions & 13 deletions src/globals/prototypes.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export = {};
import { sample, useRNG, RNGSource } from 'utils/random';

declare global {
interface Array<T> {
random (): T;
random (amount: number): T[];
random (rng?: RNGSource): T;
sample (amount: number, rng?: RNGSource): T[];
remove (...toRemove: T[]): T[];
shuffle (): T[];
shuffle (rng?: RNGSource): T[];
filterMap<X> (cb: (element: T, index: number, thisArray: T[]) => X | undefined): X | undefined;
unique (): T[];
}
Expand All @@ -21,7 +21,7 @@ Object.defineProperties(Array.prototype, {
enumerable: false,
writable: false,
configurable: false,
value: function<T, X> (this: T[], callback: (element: T, index: number, thisArray: T[]) => X | undefined): X | undefined {
value: function<X, T = unknown> (this: T[], callback: (element: T, index: number, thisArray: T[]) => X | undefined): X | undefined {
for (let i = 0; i < this.length; i++) {
const result = callback(this[i], i, this);
if (result === undefined) continue;
Expand All @@ -33,7 +33,7 @@ Object.defineProperties(Array.prototype, {
enumerable: false,
writable: false,
configurable: false,
value: function (this: unknown[], ...terms) {
value: function <T = unknown> (this: T[], ...terms: T[]): boolean {
let out = true;
terms.forEach(term => {
if (this.indexOf(term) >= 0) this.splice(this.indexOf(term), 1);
Expand All @@ -46,12 +46,20 @@ Object.defineProperties(Array.prototype, {
enumerable: false,
writable: false,
configurable: false,
value: function (this: unknown[], amount: number) {
if (amount === undefined) return this[Math.floor(Math.random() * this.length)];
const sample = Array.from(this), out = [];
value: function <T = unknown> (this: T[], rng?: RNGSource): T {
return this[sample(this.length, useRNG(rng))];
}
},
sample: {
enumerable: false,
writable: false,
configurable: false,
value: function T<T = unknown> (this: T[], amount: number, rng?: RNGSource): T[] {
const RNG = useRNG(rng);
const sample = Array.from(this), out: T[] = [];
let i = 0;
while (sample.length && i++ < amount) {
const term = sample[Math.floor(Math.random() * sample.length)];
const term = sample[Math.floor(RNG() * sample.length)];
out.push(term);
sample.remove(term);
}
Expand All @@ -62,9 +70,10 @@ Object.defineProperties(Array.prototype, {
enumerable: false,
writable: false,
configurable: false,
value: function (this: unknown[]) {
value: function <T = unknown> (this: T[], rng?: RNGSource): T[] {
const RNG = useRNG(rng);
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const j = Math.floor(RNG() * (i + 1));
[this[i], this[j]] = [this[j], this[i]];
}
return Array.from(this);
Expand All @@ -74,7 +83,7 @@ Object.defineProperties(Array.prototype, {
enumerable: false,
writable: false,
configurable: false,
value: function (this: unknown[]) {
value: function <T = unknown> (this: T[]): T[] {
const output = [];
const cache = new Set();
for (let i = 0; i < this.length; i++) {
Expand Down
48 changes: 48 additions & 0 deletions src/utils/random.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export type RNGSource = null | undefined | number | (() => number);

function PRNG (seed: number): () => number {
// Mulberry32
// Shoutouts:
// https://github.com/bryc/code/blob/master/jshash/PRNGs.md#mulberry32
// https://gist.github.com/tommyettinger/46a874533244883189143505d203312c
let s = seed;
return function () {
s |= 0; s = s + 0x6D2B79F5 | 0;
let t = Math.imul(s ^ s >>> 15, 1 | s);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
const JSRNG = (): number => Math.random();

export function useRNG (rng?: RNGSource): () => number {
if (typeof rng === 'function') return rng;
if (typeof rng === 'number') return PRNG(rng);
return JSRNG;
}

function sample(input: null, rng?: RNGSource): number;
function sample(input: number, rng?: RNGSource): number;
function sample(input: [number, number], rng?: RNGSource): number;
function sample(input: Record<string, number>, rng?: RNGSource): string;
function sample (input: null | number | [number, number] | Record<string, number>, rng?: RNGSource): number | string {
const RNG = useRNG(rng);
if (!input) return RNG();
if (typeof input === 'number') return Math.floor(input * RNG());
if (Array.isArray(input)) return input[0] + Math.floor((input[1] - input[0]) * RNG());
if (typeof input === 'object') {
const thresholds = Object.entries(input).reduce((acc, [key, weight]) => {
const lastWeight = acc.length ? acc.at(-1)[1] : 0;
return [...acc, [key, lastWeight + weight]];
}, []);
const totalWeight = thresholds.at(-1)[1];
if (!thresholds.length) throw new Error('Called RNG on record set with no keys');
if (totalWeight <= 0) throw new Error('Called RNG on record set with invalid weights');
const lookup = totalWeight * RNG();
return thresholds.find(([, weight]) => lookup < weight)[0];
}
log(`Called sample() with input`, input);
return RNG();
}

export { sample };

0 comments on commit 6aaf8b8

Please sign in to comment.