feat(inprogress): 특정 시드값을 기반으로 하는 결정론적 난수 생성기

This commit is contained in:
2022-03-12 20:31:13 +09:00
parent b19bd263a7
commit 11e17c4ae5
5 changed files with 504 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
export interface RNG {
/**
* nextInt()가 반환 가능한 최대값
*/
getMaxInt(): number;
nextBytes(bytes: number): ArrayBuffer;
nextBits(bits: number): ArrayBuffer;
nextInt(max?: number): number;
nextFloat1(): number;
}
+116
View File
@@ -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');
}
}
+214
View File
@@ -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);
}
}
+28
View File
@@ -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;
}
+133
View File
@@ -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];
}
}