feat: TestRNG 클래스 추가 및 관련 테스트 케이스 작성
This commit is contained in:
@@ -8,3 +8,4 @@ export * from './util/convertBytesLikeToUint8Array.js';
|
|||||||
export * from './util/LiteHashDRBG.js';
|
export * from './util/LiteHashDRBG.js';
|
||||||
export * from './util/RNG.js';
|
export * from './util/RNG.js';
|
||||||
export * from './util/RandUtil.js';
|
export * from './util/RandUtil.js';
|
||||||
|
export * from './util/TestRNG.js';
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import type { RNG } from './RNG.js';
|
||||||
|
|
||||||
|
const maxSafeInt = Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
const clamp01 = (value: number): number => {
|
||||||
|
if (value < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (value > 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 테스트에서 0/1만 고정으로 뽑기 위한 RNG
|
||||||
|
export class ConstantRNG implements RNG {
|
||||||
|
private readonly bit: 0 | 1;
|
||||||
|
|
||||||
|
public constructor(bit: 0 | 1) {
|
||||||
|
this.bit = bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMaxInt(): number {
|
||||||
|
return maxSafeInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBytes(bytes: number): Uint8Array<ArrayBuffer> {
|
||||||
|
if (bytes <= 0) {
|
||||||
|
throw new Error('bytes must be positive');
|
||||||
|
}
|
||||||
|
const result = new Uint8Array(bytes);
|
||||||
|
result.fill(this.bit === 0 ? 0x00 : 0xff);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBits(bits: number): Uint8Array<ArrayBuffer> {
|
||||||
|
if (bits <= 0) {
|
||||||
|
throw new Error('bits must be positive');
|
||||||
|
}
|
||||||
|
const bytes = (bits + 7) >> 3;
|
||||||
|
const headBits = bits & 0x7;
|
||||||
|
const result = this.nextBytes(bytes);
|
||||||
|
|
||||||
|
if (headBits === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result[bytes - 1]! &= 0xff >> (8 - headBits);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextInt(max?: number): number {
|
||||||
|
if (max === undefined || max === maxSafeInt) {
|
||||||
|
return this.bit === 0 ? 0 : maxSafeInt;
|
||||||
|
}
|
||||||
|
if (max > maxSafeInt) {
|
||||||
|
throw new Error('Over max int');
|
||||||
|
}
|
||||||
|
if (max === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (max < 0) {
|
||||||
|
return -this.nextInt(-max);
|
||||||
|
}
|
||||||
|
return this.bit === 0 ? 0 : max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextFloat1(): number {
|
||||||
|
return this.bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 중간값 고정 + bool은 0/1 교대로 뽑는 RNG
|
||||||
|
export class MidpointRNG implements RNG {
|
||||||
|
private bitState: 0 | 1;
|
||||||
|
|
||||||
|
public constructor(startBit: 0 | 1 = 0) {
|
||||||
|
this.bitState = startBit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMaxInt(): number {
|
||||||
|
return maxSafeInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextBitRaw(): 0 | 1 {
|
||||||
|
const value = this.bitState;
|
||||||
|
this.bitState = value === 0 ? 1 : 0;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBytes(bytes: number): Uint8Array<ArrayBuffer> {
|
||||||
|
if (bytes <= 0) {
|
||||||
|
throw new Error('bytes must be positive');
|
||||||
|
}
|
||||||
|
return this.nextBits(bytes * 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBits(bits: number): Uint8Array<ArrayBuffer> {
|
||||||
|
if (bits <= 0) {
|
||||||
|
throw new Error('bits must be positive');
|
||||||
|
}
|
||||||
|
const bytes = (bits + 7) >> 3;
|
||||||
|
const result = new Uint8Array(bytes);
|
||||||
|
|
||||||
|
for (let bitIdx = 0; bitIdx < bits; bitIdx += 1) {
|
||||||
|
if (this.nextBitRaw() === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const byteIdx = bitIdx >> 3;
|
||||||
|
const offset = bitIdx & 0x7;
|
||||||
|
result[byteIdx]! |= 1 << offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextInt(max?: number): number {
|
||||||
|
if (max === undefined || max === maxSafeInt) {
|
||||||
|
return Math.floor(maxSafeInt / 2);
|
||||||
|
}
|
||||||
|
if (max > maxSafeInt) {
|
||||||
|
throw new Error('Over max int');
|
||||||
|
}
|
||||||
|
if (max === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (max < 0) {
|
||||||
|
return -this.nextInt(-max);
|
||||||
|
}
|
||||||
|
return Math.floor(max / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextFloat1(): number {
|
||||||
|
return 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사인파 기반으로 주기/진폭을 조절하는 RNG
|
||||||
|
export class SineRNG implements RNG {
|
||||||
|
private step = 0;
|
||||||
|
private readonly period: number;
|
||||||
|
private readonly amplitude: number;
|
||||||
|
private readonly phase: number;
|
||||||
|
|
||||||
|
public constructor(period = 32, amplitude = 0.5, phase = 0) {
|
||||||
|
if (period <= 0) {
|
||||||
|
throw new Error('period must be positive');
|
||||||
|
}
|
||||||
|
this.period = period;
|
||||||
|
this.amplitude = amplitude;
|
||||||
|
this.phase = phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMaxInt(): number {
|
||||||
|
return maxSafeInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextWaveFloat(): number {
|
||||||
|
const radians = this.phase + (this.step * 2 * Math.PI) / this.period;
|
||||||
|
const value = 0.5 + this.amplitude * Math.sin(radians);
|
||||||
|
this.step += 1;
|
||||||
|
return clamp01(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBytes(bytes: number): Uint8Array<ArrayBuffer> {
|
||||||
|
if (bytes <= 0) {
|
||||||
|
throw new Error('bytes must be positive');
|
||||||
|
}
|
||||||
|
const result = new Uint8Array(bytes);
|
||||||
|
for (let idx = 0; idx < bytes; idx += 1) {
|
||||||
|
const value = Math.floor(this.nextWaveFloat() * 256);
|
||||||
|
result[idx] = value >= 256 ? 255 : value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBits(bits: number): Uint8Array<ArrayBuffer> {
|
||||||
|
if (bits <= 0) {
|
||||||
|
throw new Error('bits must be positive');
|
||||||
|
}
|
||||||
|
const bytes = (bits + 7) >> 3;
|
||||||
|
const headBits = bits & 0x7;
|
||||||
|
const result = this.nextBytes(bytes);
|
||||||
|
|
||||||
|
if (headBits === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result[bytes - 1]! &= 0xff >> (8 - headBits);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextInt(max?: number): number {
|
||||||
|
if (max === undefined || max === maxSafeInt) {
|
||||||
|
const value = Math.floor(this.nextWaveFloat() * (maxSafeInt + 1));
|
||||||
|
return value > maxSafeInt ? maxSafeInt : value;
|
||||||
|
}
|
||||||
|
if (max > maxSafeInt) {
|
||||||
|
throw new Error('Over max int');
|
||||||
|
}
|
||||||
|
if (max === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (max < 0) {
|
||||||
|
return -this.nextInt(-max);
|
||||||
|
}
|
||||||
|
const value = Math.floor(this.nextWaveFloat() * (max + 1));
|
||||||
|
return value > max ? max : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextFloat1(): number {
|
||||||
|
return this.nextWaveFloat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 지정한 수열을 반복 재생하는 테스트용 RNG
|
||||||
|
export class SequenceRNG implements RNG {
|
||||||
|
private readonly sequence: number[];
|
||||||
|
private idx = 0;
|
||||||
|
|
||||||
|
public constructor(sequence: number[]) {
|
||||||
|
if (sequence.length === 0) {
|
||||||
|
throw new Error('sequence must not be empty');
|
||||||
|
}
|
||||||
|
this.sequence = sequence.map(clamp01);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMaxInt(): number {
|
||||||
|
return maxSafeInt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextValue(): number {
|
||||||
|
const value = this.sequence[this.idx]!;
|
||||||
|
this.idx = (this.idx + 1) % this.sequence.length;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBytes(bytes: number): Uint8Array<ArrayBuffer> {
|
||||||
|
if (bytes <= 0) {
|
||||||
|
throw new Error('bytes must be positive');
|
||||||
|
}
|
||||||
|
const result = new Uint8Array(bytes);
|
||||||
|
for (let idx = 0; idx < bytes; idx += 1) {
|
||||||
|
const value = Math.floor(this.nextValue() * 256);
|
||||||
|
result[idx] = value >= 256 ? 255 : value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextBits(bits: number): Uint8Array<ArrayBuffer> {
|
||||||
|
if (bits <= 0) {
|
||||||
|
throw new Error('bits must be positive');
|
||||||
|
}
|
||||||
|
const bytes = (bits + 7) >> 3;
|
||||||
|
const headBits = bits & 0x7;
|
||||||
|
const result = this.nextBytes(bytes);
|
||||||
|
if (headBits === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result[bytes - 1]! &= 0xff >> (8 - headBits);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextInt(max?: number): number {
|
||||||
|
if (max === undefined || max === maxSafeInt) {
|
||||||
|
const value = Math.floor(this.nextValue() * (maxSafeInt + 1));
|
||||||
|
return value > maxSafeInt ? maxSafeInt : value;
|
||||||
|
}
|
||||||
|
if (max > maxSafeInt) {
|
||||||
|
throw new Error('Over max int');
|
||||||
|
}
|
||||||
|
if (max === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (max < 0) {
|
||||||
|
return -this.nextInt(-max);
|
||||||
|
}
|
||||||
|
const value = Math.floor(this.nextValue() * (max + 1));
|
||||||
|
return value > max ? max : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextFloat1(): number {
|
||||||
|
return this.nextValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { ConstantRNG, MidpointRNG, SequenceRNG, SineRNG } from '../src/util/TestRNG.js';
|
||||||
|
|
||||||
|
const toArray = (bytes: Uint8Array): number[] => Array.from(bytes);
|
||||||
|
|
||||||
|
describe('TestRNG:Constant', () => {
|
||||||
|
it('returns fixed 0', () => {
|
||||||
|
const rng = new ConstantRNG(0);
|
||||||
|
expect(rng.nextFloat1()).toBe(0);
|
||||||
|
expect(rng.nextInt(10)).toBe(0);
|
||||||
|
expect(toArray(rng.nextBytes(3))).toEqual([0, 0, 0]);
|
||||||
|
expect(toArray(rng.nextBits(3))).toEqual([0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns fixed 1', () => {
|
||||||
|
const rng = new ConstantRNG(1);
|
||||||
|
expect(rng.nextFloat1()).toBe(1);
|
||||||
|
expect(rng.nextInt(10)).toBe(10);
|
||||||
|
expect(toArray(rng.nextBytes(2))).toEqual([255, 255]);
|
||||||
|
expect(toArray(rng.nextBits(3))).toEqual([7]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TestRNG:Midpoint', () => {
|
||||||
|
it('returns midpoint for int/float', () => {
|
||||||
|
const rng = new MidpointRNG();
|
||||||
|
expect(rng.nextFloat1()).toBe(0.5);
|
||||||
|
expect(rng.nextInt(9)).toBe(4);
|
||||||
|
expect(rng.nextInt(10)).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('alternates bits', () => {
|
||||||
|
const rng = new MidpointRNG();
|
||||||
|
expect(toArray(rng.nextBits(4))).toEqual([10]);
|
||||||
|
expect(toArray(rng.nextBits(4))).toEqual([10]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TestRNG:Sine', () => {
|
||||||
|
it('follows sine wave with period/amplitude', () => {
|
||||||
|
const rng = new SineRNG(4, 0.5, 0);
|
||||||
|
expect(rng.nextFloat1()).toBeCloseTo(0.5, 8);
|
||||||
|
expect(rng.nextFloat1()).toBeCloseTo(1, 8);
|
||||||
|
expect(rng.nextFloat1()).toBeCloseTo(0.5, 8);
|
||||||
|
expect(rng.nextFloat1()).toBeCloseTo(0, 8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps float to int range', () => {
|
||||||
|
const rng = new SineRNG(4, 0.5, 0);
|
||||||
|
expect(rng.nextInt(9)).toBe(5);
|
||||||
|
expect(rng.nextInt(9)).toBe(9);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TestRNG:Sequence', () => {
|
||||||
|
it('cycles fixed sequence', () => {
|
||||||
|
const rng = new SequenceRNG([0, 0.25, 0.5, 0.75, 1]);
|
||||||
|
expect(rng.nextFloat1()).toBe(0);
|
||||||
|
expect(rng.nextFloat1()).toBe(0.25);
|
||||||
|
expect(rng.nextFloat1()).toBe(0.5);
|
||||||
|
expect(rng.nextFloat1()).toBe(0.75);
|
||||||
|
expect(rng.nextFloat1()).toBe(1);
|
||||||
|
expect(rng.nextFloat1()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts sequence to bytes and ints', () => {
|
||||||
|
const rng = new SequenceRNG([0, 0.5, 1]);
|
||||||
|
expect(toArray(rng.nextBytes(3))).toEqual([0, 128, 255]);
|
||||||
|
expect(rng.nextInt(8)).toBe(0);
|
||||||
|
expect(rng.nextInt(8)).toBe(4);
|
||||||
|
expect(rng.nextInt(8)).toBe(8);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user