feat(inprogress): 특정 시드값을 기반으로 하는 결정론적 난수 생성기
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
export interface RNG {
|
||||
|
||||
/**
|
||||
* nextInt()가 반환 가능한 최대값
|
||||
*/
|
||||
getMaxInt(): number;
|
||||
|
||||
nextBytes(bytes: number): ArrayBuffer;
|
||||
nextBits(bits: number): ArrayBuffer;
|
||||
|
||||
nextInt(max?: number): number;
|
||||
nextFloat1(): number;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { RNG } from './RNG';
|
||||
|
||||
export class RandUtils{
|
||||
constructor(protected rng: RNG){
|
||||
|
||||
}
|
||||
|
||||
public nextFloat1(): number{
|
||||
return this.rng.nextFloat1();
|
||||
}
|
||||
|
||||
public nextRange(min: number, max: number): number{
|
||||
const range = max - min;
|
||||
return this.nextFloat1() * (range) + min;
|
||||
}
|
||||
|
||||
public nextRangeInt(min: number, max: number): number{
|
||||
const range = max - min;
|
||||
return this.rng.nextInt(range) + min;
|
||||
}
|
||||
|
||||
public nextBit(): boolean{
|
||||
const view = new DataView(this.rng.nextBits(1));
|
||||
return view.getUint8(0) != 0;
|
||||
}
|
||||
|
||||
public nextBool(prob = 0.5): boolean{
|
||||
return this.nextFloat1() < prob;
|
||||
}
|
||||
|
||||
public shuffle<T>(srcArray: T[]): T[]{
|
||||
const cnt = srcArray.length;
|
||||
if(cnt > this.rng.getMaxInt()){
|
||||
throw 'Invalid random int range';
|
||||
}
|
||||
|
||||
const result: T[] = Array.from(srcArray);
|
||||
for(let srcIdx = 0; srcIdx < cnt; srcIdx += 1){
|
||||
const destIdx = this.rng.nextInt(cnt - srcIdx - 1) + srcIdx;
|
||||
[result[srcIdx], result[destIdx]] = [result[destIdx], result[srcIdx]];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//Object는 integer key에 예외가 있어 shuffleAssoc은 없음
|
||||
|
||||
public choice<T>(items: T[]|Record<string|number,T>|Set<T>): T{
|
||||
if(items instanceof Array){
|
||||
const idx = this.rng.nextInt(items.length - 1);
|
||||
return items[idx];
|
||||
}
|
||||
|
||||
if(items instanceof Set){
|
||||
return this.choice(Array.from(items.values()));
|
||||
}
|
||||
|
||||
return items[this.choice(Array.from(Object.keys(items)))];
|
||||
}
|
||||
|
||||
public choiceUsingWeight(items: Record<string|number, number>): string|number{
|
||||
let sum = 0;
|
||||
for(const value of Object.values(items)){
|
||||
if(value <= 0){
|
||||
continue;
|
||||
}
|
||||
sum += value;
|
||||
}
|
||||
|
||||
let rd = this.nextFloat1() * sum;
|
||||
|
||||
for(const [item, value] of Object.entries(items)){
|
||||
if(value <= 0){
|
||||
if(rd <= 0){
|
||||
return item;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(rd <= value){
|
||||
return item;
|
||||
}
|
||||
rd -= value;
|
||||
}
|
||||
|
||||
throw new Error('Unreacheable');
|
||||
}
|
||||
|
||||
public choiceUsingWeightPair<T>(items: [T, number][]): T{
|
||||
let sum = 0;
|
||||
for(const [, value] of items){
|
||||
if(value <= 0){
|
||||
continue;
|
||||
}
|
||||
sum += value;
|
||||
}
|
||||
|
||||
let rd = this.nextFloat1() * sum;
|
||||
|
||||
for(const [item, value] of items){
|
||||
if(value <= 0){
|
||||
if(rd <= 0){
|
||||
return item;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(rd <= value){
|
||||
return item;
|
||||
}
|
||||
rd -= value;
|
||||
}
|
||||
|
||||
throw new Error('Unreacheable');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace sammo;
|
||||
|
||||
//NOTE: JavaScript 버전과 일치
|
||||
const MAX_RNG_SUPPORT_BIT = 53;
|
||||
if (PHP_INT_SIZE * 8 < MAX_RNG_SUPPORT_BIT) {
|
||||
throw new \RangeException("PHP not support {$MAX_RNG_SUPPORT_BIT} bit integer");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reseed를 하지 않는 단순한 형태의 sha512 drbg
|
||||
* float: bit를 강제로 채움
|
||||
**/
|
||||
|
||||
|
||||
class LiteHashDRBG implements RNG
|
||||
{
|
||||
const MAX_INT = (1 << MAX_RNG_SUPPORT_BIT) - 1;
|
||||
const BUFFER_BYTE_SIZE = 512 / 8; //SHA512
|
||||
|
||||
protected string $buffer;
|
||||
protected int $bufferIdx;
|
||||
protected function __construct(protected string $seed, protected int $stateIdx = 0)
|
||||
{
|
||||
$this->genNextBlock();
|
||||
}
|
||||
|
||||
protected function genNextBlock(): void
|
||||
{
|
||||
$hq = $this->seed . pack('P', $this->stateIdx);
|
||||
$this->buffer = hash('sha512', $hq, true);
|
||||
$this->bufferIdx = 0;
|
||||
$this->stateIdx += 1;
|
||||
}
|
||||
|
||||
static function getMaxInt(): int
|
||||
{
|
||||
return self::MAX_INT;
|
||||
}
|
||||
|
||||
const INT_BIT_MASK_MAP = [
|
||||
0x1 => 1,
|
||||
0x3 => 2,
|
||||
0x7 => 3,
|
||||
0xf => 4,
|
||||
0x1f => 5,
|
||||
0x3f => 6,
|
||||
0x7f => 7,
|
||||
0xff => 8,
|
||||
0x1ff => 9,
|
||||
0x3ff => 10,
|
||||
0x7ff => 11,
|
||||
0xfff => 12,
|
||||
0x1fff => 13,
|
||||
0x3fff => 14,
|
||||
0x7fff => 15,
|
||||
0xffff => 16,
|
||||
0x1ffff => 17,
|
||||
0x3ffff => 18,
|
||||
0x7ffff => 19,
|
||||
0xfffff => 20,
|
||||
0x1fffff => 21,
|
||||
0x3fffff => 22,
|
||||
0x7fffff => 23,
|
||||
0xffffff => 24,
|
||||
0x1ffffff => 25,
|
||||
0x3ffffff => 26,
|
||||
0x7ffffff => 27,
|
||||
0xfffffff => 28,
|
||||
0x1fffffff => 29,
|
||||
0x3fffffff => 30,
|
||||
0x7fffffff => 31,
|
||||
0xffffffff => 32,
|
||||
0x1ffffffff => 33,
|
||||
0x3ffffffff => 34,
|
||||
0x7ffffffff => 35,
|
||||
0xfffffffff => 36,
|
||||
0x1fffffffff => 37,
|
||||
0x3fffffffff => 38,
|
||||
0x7fffffffff => 39,
|
||||
0xffffffffff => 40,
|
||||
0x1ffffffffff => 41,
|
||||
0x3ffffffffff => 42,
|
||||
0x7ffffffffff => 43,
|
||||
0xfffffffffff => 44,
|
||||
0x1fffffffffff => 45,
|
||||
0x3fffffffffff => 46,
|
||||
0x7fffffffffff => 47,
|
||||
0xffffffffffff => 48,
|
||||
0x1ffffffffffff => 49,
|
||||
0x3ffffffffffff => 50,
|
||||
0x7ffffffffffff => 51,
|
||||
0xfffffffffffff => 52,
|
||||
0x1fffffffffffff => 53,
|
||||
];
|
||||
|
||||
private static function calcBitMask($n)
|
||||
{
|
||||
$n |= $n >> 1;
|
||||
$n |= $n >> 2;
|
||||
$n |= $n >> 4;
|
||||
$n |= $n >> 8;
|
||||
$n |= $n >> 16;
|
||||
$n |= $n >> 32;
|
||||
|
||||
return $n;
|
||||
}
|
||||
|
||||
|
||||
public function nextBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes <= 0) {
|
||||
throw new \InvalidArgumentException("{$bytes} <= 0");
|
||||
}
|
||||
|
||||
if ($this->bufferIdx + $bytes <= self::BUFFER_BYTE_SIZE) {
|
||||
$buffer = substr($this->buffer, $this->bufferIdx, $bytes);
|
||||
$this->bufferIdx += $bytes;
|
||||
if ($this->bufferIdx == self::BUFFER_BYTE_SIZE) {
|
||||
$this->genNextBlock();
|
||||
}
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
|
||||
$result = [substr($this->buffer, $this->bufferIdx)];
|
||||
$remain = $bytes - (self::BUFFER_BYTE_SIZE - $this->bufferIdx);
|
||||
|
||||
while ($remain > self::BUFFER_BYTE_SIZE) {
|
||||
$this->genNextBlock();
|
||||
$result[] = $this->buffer;
|
||||
$remain -= self::BUFFER_BYTE_SIZE;
|
||||
}
|
||||
|
||||
$this->genNextBlock();
|
||||
if ($remain == 0) {
|
||||
return join("", $result);
|
||||
}
|
||||
|
||||
$result[] = substr($this->buffer, 0, $remain);
|
||||
$this->bufferIdx = $remain;
|
||||
return join("", $result);
|
||||
}
|
||||
|
||||
public function nextBits(int $bits): string
|
||||
{
|
||||
$bytes = ($bits + 7) >> 3;
|
||||
$headBits = $bits & 0x7;
|
||||
|
||||
$buffer = $this->nextBytes($bytes);
|
||||
if ($headBits != 0) {
|
||||
$buffer[$bytes - 1] = chr(ord($buffer[$bytes - 1]) & (0xff >> (8 - $headBits))) ;
|
||||
}
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
|
||||
static private function parseU64(string $value): int
|
||||
{
|
||||
return unpack('P', $value)[1];
|
||||
}
|
||||
|
||||
private function _nextInt(int $max): int
|
||||
{
|
||||
$mask = self::calcBitMask($max);
|
||||
$bits = self::INT_BIT_MASK_MAP[$mask];
|
||||
|
||||
$buffer = $this->nextBits($bits) . "\x00\x00\x00\x00\x00\x00\x00";
|
||||
|
||||
return self::parseU64($buffer);
|
||||
}
|
||||
|
||||
public function nextInt(?int $max = null): int
|
||||
{
|
||||
if ($max === null || $max === self::MAX_INT) {
|
||||
$buffer = $this->nextBits(MAX_RNG_SUPPORT_BIT) . "\x00";
|
||||
return self::parseU64($buffer);
|
||||
}
|
||||
|
||||
if ($max > self::MAX_INT) {
|
||||
throw new \InvalidArgumentException('Over Max Int');
|
||||
} else if ($max === 0) {
|
||||
return 0;
|
||||
} else if ($max < 0) {
|
||||
return -$this->nextInt(-$max);
|
||||
}
|
||||
|
||||
$n = $this->_nextInt($max);
|
||||
while ($n > $max) {
|
||||
$n = $this->_nextInt($max);
|
||||
}
|
||||
|
||||
return $n;
|
||||
}
|
||||
|
||||
public function nextFloat1(): float
|
||||
{
|
||||
$max = 1 << MAX_RNG_SUPPORT_BIT;
|
||||
while(true){
|
||||
$buffer = $this->nextBits(MAX_RNG_SUPPORT_BIT + 1) . "\x00";
|
||||
$nInt = self::parseU64($buffer);
|
||||
if($nInt <= $max){
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $nInt / $max;
|
||||
}
|
||||
|
||||
static function build(string $seed, int $idx = 0): self
|
||||
{
|
||||
return new LiteHashDRBG($seed, $idx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace sammo;
|
||||
|
||||
interface RNG
|
||||
{
|
||||
|
||||
/**
|
||||
* @return int nextInt()가 반환 가능한 최대값
|
||||
*/
|
||||
public static function getMaxInt(): int;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param int $bytes
|
||||
* @return string Little Endian 형태로 채워진 binary 값
|
||||
*/
|
||||
public function nextBytes(int $bytes): string;
|
||||
public function nextBits(int $bits): string;
|
||||
|
||||
/**
|
||||
* @param ?int $max 최대치(해당 값 포함)
|
||||
* @return int 0과 최대치 사이의 임의의 정수
|
||||
*/
|
||||
public function nextInt(?int $max = null): int;
|
||||
|
||||
public function nextFloat1(): float;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace sammo;
|
||||
|
||||
class RandUtil
|
||||
{
|
||||
public function __construct(protected RNG $rng)
|
||||
{
|
||||
}
|
||||
|
||||
public function nextFloat1(): float
|
||||
{
|
||||
return $this->rng->nextFloat1();
|
||||
}
|
||||
|
||||
public function nextRange(int|float $min, int|float $max): float
|
||||
{
|
||||
$range = $max - $min;
|
||||
return $this->nextFloat1() * $range + $min;
|
||||
}
|
||||
|
||||
public function nextRangeInt(int $min, int $max): int
|
||||
{
|
||||
$range = $max - $min;
|
||||
if ($range > $this->rng->getMaxInt()) {
|
||||
throw new \InvalidArgumentException("Invalid random int range");
|
||||
}
|
||||
return $this->rng->nextInt($range) + $min;
|
||||
}
|
||||
|
||||
public function nextBit(): bool
|
||||
{
|
||||
return $this->rng->nextBits(1) !== "\0";
|
||||
}
|
||||
|
||||
public function nextBool(int|float $prob = 0.5): bool
|
||||
{
|
||||
if ($prob >= 1) {
|
||||
return true;
|
||||
}
|
||||
return $this->nextFloat1() < $prob;
|
||||
}
|
||||
|
||||
public function shuffle(array $srcArray): array
|
||||
{
|
||||
$cnt = count($srcArray);
|
||||
if ($cnt > $this->rng->getMaxInt()) {
|
||||
throw new \InvalidArgumentException("Invalid random int range");
|
||||
}
|
||||
$result = [];
|
||||
foreach ($srcArray as $val) {
|
||||
$result[] = $val;
|
||||
}
|
||||
|
||||
//PHP의 range는 max 포함
|
||||
foreach (range(0, $cnt - 1) as $srcIdx) {
|
||||
$destIdx = $this->rng->nextInt($cnt - $srcIdx - 1) + $srcIdx;
|
||||
$tmpVal = $result[$srcIdx];
|
||||
$result[$srcIdx] = $result[$destIdx];
|
||||
$result[$destIdx] = $tmpVal;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function shuffleAssoc(array $srcArray): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->shuffle(array_keys($srcArray)) as $key) {
|
||||
$result[$key] = $srcArray[$key];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function choice(array $items)
|
||||
{
|
||||
$keys = array_keys($items);
|
||||
$keyIdx = $this->rng->nextInt(count($keys) - 1);
|
||||
return $items[$keys[$keyIdx]];
|
||||
}
|
||||
|
||||
public function choiceUsingWeight(array $items)
|
||||
{
|
||||
$sum = 0;
|
||||
foreach ($items as $value) {
|
||||
if ($value <= 0) {
|
||||
continue;
|
||||
}
|
||||
$sum += $value;
|
||||
}
|
||||
|
||||
$rd = $this->nextFloat1() * $sum;
|
||||
foreach ($items as $item => $value) {
|
||||
if ($value <= 0) {
|
||||
$value = 0;
|
||||
}
|
||||
if ($rd <= $value) {
|
||||
return $item;
|
||||
}
|
||||
$rd -= $value;
|
||||
}
|
||||
|
||||
//fallback. 이곳으로 빠지지 않음
|
||||
end($items);
|
||||
return $items[key($items)][0];
|
||||
}
|
||||
|
||||
public function choiceUsingWeightPair(array $items)
|
||||
{
|
||||
$sum = 0;
|
||||
foreach ($items as [$item, $value]) {
|
||||
if ($value <= 0) {
|
||||
continue;
|
||||
}
|
||||
$sum += $value;
|
||||
}
|
||||
|
||||
$rd = $this->nextFloat1() * $sum;
|
||||
foreach ($items as [$item, $value]) {
|
||||
if ($value <= 0) {
|
||||
$value = 0;
|
||||
}
|
||||
if ($rd <= $value) {
|
||||
return $item;
|
||||
}
|
||||
$rd -= $value;
|
||||
}
|
||||
|
||||
//fallback. 이곳으로 빠지지 않음
|
||||
end($items);
|
||||
return $items[key($items)][0];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user