Files
SocialPub/tools/pasture/town/core/rng.py
T

93 lines
2.3 KiB
Python
Raw Normal View History

"""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