diff --git a/hwe/ts/util/RNG.ts b/hwe/ts/util/RNG.ts new file mode 100644 index 00000000..3641c475 --- /dev/null +++ b/hwe/ts/util/RNG.ts @@ -0,0 +1,13 @@ +export interface RNG { + + /** + * nextInt()가 반환 가능한 최대값 + */ + getMaxInt(): number; + + nextBytes(bytes: number): ArrayBuffer; + nextBits(bits: number): ArrayBuffer; + + nextInt(max?: number): number; + nextFloat1(): number; +} \ No newline at end of file diff --git a/hwe/ts/util/RandUtil.ts b/hwe/ts/util/RandUtil.ts new file mode 100644 index 00000000..573e7570 --- /dev/null +++ b/hwe/ts/util/RandUtil.ts @@ -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(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(items: T[]|Record|Set): 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{ + 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(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'); + } +} \ No newline at end of file diff --git a/src/sammo/LiteHashDRBG.php b/src/sammo/LiteHashDRBG.php new file mode 100644 index 00000000..dacf7f4f --- /dev/null +++ b/src/sammo/LiteHashDRBG.php @@ -0,0 +1,214 @@ +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); + } +} diff --git a/src/sammo/RNG.php b/src/sammo/RNG.php new file mode 100644 index 00000000..4ee1a66d --- /dev/null +++ b/src/sammo/RNG.php @@ -0,0 +1,28 @@ +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]; + } +}