mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
Use seedrandom to back PseudoRandom.ts (#1828)
## Description: The previous implementation had a bug that biased numbers away from 0, so random.chance(1500+) would always return false. This caused trains to not spawn at all when their spawn rate was sufficiently low. We should be using a library instead of implementing it from scratch anyways. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan
This commit is contained in:
Generated
+15
@@ -28,6 +28,7 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"nanoid": "^3.3.6",
|
||||
"obscenity": "^0.4.3",
|
||||
"seedrandom": "^3.0.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
@@ -53,6 +54,7 @@
|
||||
"@types/msgpack5": "^3.4.6",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/pg": "^8.11.11",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@types/systeminformation": "^3.23.1",
|
||||
"@types/ws": "^8.5.11",
|
||||
@@ -6926,6 +6928,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/seedrandom": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.8.tgz",
|
||||
"integrity": "sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "0.17.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
|
||||
@@ -17608,6 +17617,12 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/seedrandom": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
|
||||
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/select-hose": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@types/msgpack5": "^3.4.6",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/pg": "^8.11.11",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@types/systeminformation": "^3.23.1",
|
||||
"@types/ws": "^8.5.11",
|
||||
@@ -118,6 +119,7 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"nanoid": "^3.3.6",
|
||||
"obscenity": "^0.4.3",
|
||||
"seedrandom": "^3.0.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
|
||||
+21
-103
@@ -1,123 +1,45 @@
|
||||
export class PseudoRandom {
|
||||
// Internal state (two 32-bit integers)
|
||||
private state0: number;
|
||||
private state1: number;
|
||||
import seedrandom from "seedrandom";
|
||||
|
||||
// Keep these variables to maintain the exact same interface
|
||||
private m: number = 0x80000000; // 2**31
|
||||
private a: number = 1103515245;
|
||||
private c: number = 12345;
|
||||
private state: number;
|
||||
export class PseudoRandom {
|
||||
private rng: seedrandom.PRNG;
|
||||
|
||||
private static readonly POW36_8 = Math.pow(36, 8); // Pre-compute 36^8
|
||||
private static readonly INV_2_32 = 1 / 4294967296; // 1 / 2^32 for float conversion
|
||||
|
||||
constructor(seed: number) {
|
||||
// Initialize the XorShift state with seed
|
||||
this.state0 = seed | 0; // Force to 32-bit integer with bitwise OR
|
||||
this.state1 = 0x6e2d786c; // Fixed value as second seed (arbitrary prime)
|
||||
|
||||
// Ensure non-zero state
|
||||
if (this.state0 === 0) this.state0 = 1;
|
||||
|
||||
// Also set the LCG state variable to maintain interface
|
||||
this.state = seed % this.m;
|
||||
if (this.state < 0) this.state += this.m;
|
||||
|
||||
// Warm up the generator to improve initial distribution
|
||||
for (let i = 0; i < 20; i++) {
|
||||
this._nextIntInternal();
|
||||
}
|
||||
this.rng = seedrandom(String(seed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function that implements XorShift algorithm
|
||||
* @returns A 32-bit integer
|
||||
*/
|
||||
private _nextIntInternal(): number {
|
||||
// Get current state
|
||||
let s1 = this.state0;
|
||||
const s0 = this.state1;
|
||||
|
||||
// Update state using XorShift algorithm (all operations are bitwise)
|
||||
this.state0 = s0;
|
||||
s1 ^= s1 << 23;
|
||||
s1 ^= s1 >>> 17;
|
||||
s1 ^= s0;
|
||||
s1 ^= s0 >>> 26;
|
||||
this.state1 = s1;
|
||||
|
||||
// Generate output (force 32-bit integer)
|
||||
return (this.state0 + this.state1) | 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized version that directly returns unsigned 32-bit integer
|
||||
*/
|
||||
private _nextUInt32(): number {
|
||||
return this._nextIntInternal() >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next pseudorandom number.
|
||||
* @returns A number between 0 (inclusive) and 1 (exclusive).
|
||||
*/
|
||||
// Generates the next pseudorandom number between 0 and 1.
|
||||
next(): number {
|
||||
// Get a 32-bit integer and convert to [0,1) range
|
||||
// Using >>> 0 to get unsigned interpretation (positive number)
|
||||
const int = this._nextUInt32();
|
||||
|
||||
// Update the state variable to maintain compatibility with original interface
|
||||
this.state = int % this.m;
|
||||
|
||||
// Convert to [0,1) range - using division for same interface
|
||||
return this.state / this.m;
|
||||
return this.rng();
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized version for internal use - directly converts to [0,1) without state update
|
||||
*/
|
||||
private _nextFloat(): number {
|
||||
return this._nextUInt32() * PseudoRandom.INV_2_32;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random integer between min (inclusive) and max (exclusive).
|
||||
*/
|
||||
// Generates a random integer between min (inclusive) and max (exclusive).
|
||||
nextInt(min: number, max: number): number {
|
||||
// keep max exclusive, min inclusive – round down to get an int
|
||||
return Math.floor(this._nextFloat() * (max - min)) + min;
|
||||
return Math.floor(this.rng() * (max - min)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random float between min (inclusive) and max (exclusive).
|
||||
*/
|
||||
// Generates a random float between min (inclusive) and max (exclusive).
|
||||
nextFloat(min: number, max: number): number {
|
||||
return this._nextFloat() * (max - min) + min;
|
||||
return this.rng() * (max - min) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random ID (8 characters, alphanumeric).
|
||||
*/
|
||||
// Generates a random ID (8 characters, alphanumeric).
|
||||
nextID(): string {
|
||||
return Math.floor(this._nextFloat() * PseudoRandom.POW36_8) // 36^8 possibilities
|
||||
.toString(36) // Convert to base36 (0-9 and a-z)
|
||||
.padStart(8, "0"); // Ensure 8 chars by padding with zeros
|
||||
return Math.floor(this.rng() * PseudoRandom.POW36_8)
|
||||
.toString(36)
|
||||
.padStart(8, "0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a random element from an array.
|
||||
*/
|
||||
// Selects a random element from an array.
|
||||
randElement<T>(arr: T[]): T {
|
||||
if (arr.length === 0) {
|
||||
throw new Error("array must not be empty");
|
||||
}
|
||||
return arr[Math.floor(this._nextFloat() * arr.length)];
|
||||
return arr[Math.floor(this.rng() * arr.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a random element from a set.
|
||||
*/
|
||||
// Selects a random element from a set.
|
||||
randFromSet<T>(set: Set<T>): T {
|
||||
const size = set.size;
|
||||
if (size === 0) {
|
||||
@@ -137,20 +59,16 @@ export class PseudoRandom {
|
||||
throw new Error("Unexpected error selecting element from set");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true with probability 1/odds.
|
||||
*/
|
||||
// Returns true with probability 1/odds.
|
||||
chance(odds: number): boolean {
|
||||
return Math.floor(this._nextFloat() * odds) === 0;
|
||||
return Math.floor(this.rng() * odds) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a shuffled copy of the array using Fisher-Yates algorithm.
|
||||
*/
|
||||
// Returns a shuffled copy of the array using Fisher-Yates algorithm.
|
||||
shuffleArray<T>(array: T[]): T[] {
|
||||
const result = [...array];
|
||||
for (let i = result.length - 1; i >= 0; i--) {
|
||||
const j = Math.floor(this._nextFloat() * (i + 1));
|
||||
const j = Math.floor(this.rng() * (i + 1));
|
||||
[result[i], result[j]] = [result[j], result[i]];
|
||||
}
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user