93 lines
2.3 KiB
Python
93 lines
2.3 KiB
Python
"""A seeded random generator that is the same on every Python: xoshiro256** seeded through splitmix64 from a SHA-256
|
|||
|
|
of the seed and a name. `sub(name)` gives an independent stream, so changing how many posts are drawn never reshuffles
|
||
|
|
the follow graph."""
|
||
|
|
import hashlib
|
||
|
|
|
||
|
|
MASK = (1 << 64) - 1
|
||
|
|
|
||
|
|
|
||
|
|
def _rotl(x, k):
|
||
|
|
return ((x << k) | (x >> (64 - k))) & MASK
|
||
|
|
|
||
|
|
|
||
|
|
def _splitmix(state):
|
||
|
|
while True:
|
||
|
|
state = (state + 0x9E3779B97F4A7C15) & MASK
|
||
|
|
z = state
|
||
|
|
z = ((z ^ (z >> 30)) * 0xBF58476D1CE4E5B9) & MASK
|
||
|
|
z = ((z ^ (z >> 27)) * 0x94D049BB133111EB) & MASK
|
||
|
|
yield z ^ (z >> 31)
|
||
|
|
|
||
|
|
|
||
|
|
class Rng:
|
||
|
|
def __init__(self, seed, name="root"):
|
||
|
|
self.seed = seed
|
||
|
|
self.name = name
|
||
|
|
digest = hashlib.sha256(f"{seed}/{name}".encode()).digest()
|
||
|
|
mix = _splitmix(int.from_bytes(digest[:8], "little"))
|
||
|
|
self.s = [next(mix) for _ in range(4)]
|
||
|
|
|
||
|
|
def sub(self, name):
|
||
|
|
return Rng(self.seed, f"{self.name}/{name}")
|
||
|
|
|
||
|
|
def next64(self):
|
||
|
|
s = self.s
|
||
|
|
result = (_rotl((s[1] * 5) & MASK, 7) * 9) & MASK
|
||
|
|
t = (s[1] << 17) & MASK
|
||
|
|
s[2] ^= s[0]
|
||
|
|
s[3] ^= s[1]
|
||
|
|
s[1] ^= s[2]
|
||
|
|
s[0] ^= s[3]
|
||
|
|
s[2] ^= t
|
||
|
|
s[3] = _rotl(s[3], 45)
|
||
|
|
return result
|
||
|
|
|
||
|
|
def random(self):
|
||
|
|
return (self.next64() >> 11) / float(1 << 53)
|
||
|
|
|
||
|
|
def below(self, n):
|
||
|
|
"""An integer in [0, n)."""
|
||
|
|
if n <= 0:
|
||
|
|
raise ValueError("below() needs n > 0")
|
||
|
|
limit = MASK - (MASK % n)
|
||
|
|
while True:
|
||
|
|
x = self.next64()
|
||
|
|
if x < limit:
|
||
|
|
return x % n
|
||
|
|
|
||
|
|
def between(self, lo, hi):
|
||
|
|
"""An integer in [lo, hi]."""
|
||
|
|
return lo + self.below(hi - lo + 1)
|
||
|
|
|
||
|
|
def chance(self, p):
|
||
|
|
return self.random() < p
|
||
|
|
|
||
|
|
def choice(self, seq):
|
||
|
|
return seq[self.below(len(seq))]
|
||
|
|
|
||
|
|
def weighted(self, weights):
|
||
|
|
"""A key of {key: weight}, keys taken in sorted order so the draw does not depend on dict order."""
|
||
|
|
keys = sorted(k for k, w in weights.items() if w > 0)
|
||
|
|
total = sum(weights[k] for k in keys)
|
||
|
|
x = self.random() * total
|
||
|
|
for k in keys:
|
||
|
|
x -= weights[k]
|
||
|
|
if x < 0:
|
||
|
|
return k
|
||
|
|
return keys[-1]
|
||
|
|
|
||
|
|
def shuffle(self, items):
|
||
|
|
items = list(items)
|
||
|
|
for i in range(len(items) - 1, 0, -1):
|
||
|
|
j = self.below(i + 1)
|
||
|
|
items[i], items[j] = items[j], items[i]
|
||
|
|
return items
|
||
|
|
|
||
|
|
def sample(self, seq, k):
|
||
|
|
return self.shuffle(seq)[:max(0, min(k, len(seq)))]
|
||
|
|
|
||
|
|
def span(self, pair):
|
||
|
|
"""A value drawn from [lo, hi] given as a two-item list, or the value itself."""
|
||
|
|
if isinstance(pair, (list, tuple)):
|
||
|
|
return self.between(pair[0], pair[1])
|
||
|
|
return pair
|