mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
random: port Alea, one generator per character
pqcli used Python's global Mersenne Twister, seeded once from the clock. Upstream uses Johannes Baagøe's Alea, with Random(n) = uint32() % n, and keeps the generator state with each character. The port matches the JS output bit for bit, including UTF-16 seeding of non-BMP characters. Each Player now owns an Alea that is pickled with the save, and Simulation.tick draws from it. Saves without one get a fresh generator. As upstream does, a new character's game continues from the state the kept stat roll was made from. The parity traces are re-recorded, since every random number changes.
This commit is contained in:
11 files changed
+5721
-5432
No files matched your search
+45
-8
@@ -30,6 +30,7 @@ from pqcli.lingo import (
|
|||||||
special,
|
special,
|
||||||
young,
|
young,
|
||||||
)
|
)
|
||||||
|
from pqcli.random import Alea, RngState
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -346,12 +347,14 @@ class Player(SignalMixin):
|
|||||||
race: Race,
|
race: Race,
|
||||||
class_: Class,
|
class_: Class,
|
||||||
stats: Stats,
|
stats: Stats,
|
||||||
|
rng: T.Optional[Alea] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.birthday: datetime.datetime = birthday
|
self.birthday: datetime.datetime = birthday
|
||||||
self.race: Race = race
|
self.race: Race = race
|
||||||
self.class_: Class = class_
|
self.class_: Class = class_
|
||||||
self.stats: Stats = stats
|
self.stats: Stats = stats
|
||||||
|
self.rng: Alea = rng or Alea()
|
||||||
self.elapsed = 0.0
|
self.elapsed = 0.0
|
||||||
|
|
||||||
self.exp_bar = Bar(max_=level_up_time(1))
|
self.exp_bar = Bar(max_=level_up_time(1))
|
||||||
@@ -369,6 +372,8 @@ class Player(SignalMixin):
|
|||||||
def __setstate__(self, obj: T.Any) -> None:
|
def __setstate__(self, obj: T.Any) -> None:
|
||||||
self.__dict__.update(obj)
|
self.__dict__.update(obj)
|
||||||
self.elapsed = obj.get("elapsed", 0.0)
|
self.elapsed = obj.get("elapsed", 0.0)
|
||||||
|
if "rng" not in obj:
|
||||||
|
self.rng = Alea()
|
||||||
|
|
||||||
def set_task(self, task: BaseTask) -> None:
|
def set_task(self, task: BaseTask) -> None:
|
||||||
self.task = task
|
self.task = task
|
||||||
@@ -494,6 +499,11 @@ class Simulation:
|
|||||||
self.last_tick = datetime.datetime.now()
|
self.last_tick = datetime.datetime.now()
|
||||||
|
|
||||||
def tick(self, elapsed: float = 100.0) -> None:
|
def tick(self, elapsed: float = 100.0) -> None:
|
||||||
|
# each character plays on its own generator, saved along with it
|
||||||
|
with random.using(self.player.rng):
|
||||||
|
self._tick(elapsed)
|
||||||
|
|
||||||
|
def _tick(self, elapsed: float) -> None:
|
||||||
self.player.elapsed += elapsed
|
self.player.elapsed += elapsed
|
||||||
|
|
||||||
if self.player.task is None:
|
if self.player.task is None:
|
||||||
@@ -963,11 +973,30 @@ def monster_task(
|
|||||||
return KillTask(description, duration, monster=monster)
|
return KillTask(description, duration, monster=monster)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _Roll:
|
||||||
|
stats: Stats
|
||||||
|
seed: RngState # generator state before the roll
|
||||||
|
after: RngState
|
||||||
|
|
||||||
|
|
||||||
class StatsBuilder:
|
class StatsBuilder:
|
||||||
def __init__(self) -> None:
|
def __init__(self, rng: T.Optional[Alea] = None) -> None:
|
||||||
self.history: T.List[Stats] = []
|
self._rng = rng or Alea()
|
||||||
|
self.history: T.List[_Roll] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def seed(self) -> RngState:
|
||||||
|
"""Generator state the current stats were rolled from.
|
||||||
|
|
||||||
|
Like upstream, a new character's game continues from this state,
|
||||||
|
so its first rolls replay the stat roll's numbers.
|
||||||
|
"""
|
||||||
|
return self.history[-1].seed
|
||||||
|
|
||||||
def roll(self) -> Stats:
|
def roll(self) -> Stats:
|
||||||
|
seed = self._rng.state()
|
||||||
|
with random.using(self._rng):
|
||||||
values: T.Dict[StatType, int] = {
|
values: T.Dict[StatType, int] = {
|
||||||
stat: 3 + random.below(6) + random.below(6) + random.below(6)
|
stat: 3 + random.below(6) + random.below(6) + random.below(6)
|
||||||
for stat in PRIME_STATS
|
for stat in PRIME_STATS
|
||||||
@@ -979,21 +1008,29 @@ class StatsBuilder:
|
|||||||
random.below(8) + values[StatType.intelligence] // 6
|
random.below(8) + values[StatType.intelligence] // 6
|
||||||
)
|
)
|
||||||
stats = Stats(values)
|
stats = Stats(values)
|
||||||
self.history.append(stats)
|
self.history.append(_Roll(stats, seed, self._rng.state()))
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def unroll(self) -> Stats:
|
def unroll(self) -> Stats:
|
||||||
if len(self.history) > 1:
|
if len(self.history) > 1:
|
||||||
self.history.pop()
|
self.history.pop()
|
||||||
return self.history[-1]
|
self._rng = Alea.from_state(self.history[-1].after)
|
||||||
|
return self.history[-1].stats
|
||||||
|
|
||||||
|
|
||||||
def create_player(
|
def create_player(
|
||||||
name: str, race: Race, class_: Class, stats: Stats
|
name: str,
|
||||||
|
race: Race,
|
||||||
|
class_: Class,
|
||||||
|
stats: Stats,
|
||||||
|
seed: T.Optional[RngState] = None,
|
||||||
) -> Player:
|
) -> Player:
|
||||||
now = datetime.datetime.now()
|
|
||||||
random.seed(str(now))
|
|
||||||
player = Player(
|
player = Player(
|
||||||
birthday=now, name=name, race=race, class_=class_, stats=stats
|
birthday=datetime.datetime.now(),
|
||||||
|
name=name,
|
||||||
|
race=race,
|
||||||
|
class_=class_,
|
||||||
|
stats=stats,
|
||||||
|
rng=Alea.from_state(seed) if seed is not None else None,
|
||||||
)
|
)
|
||||||
return player
|
return player
|
||||||
+98
-3
@@ -1,9 +1,104 @@
|
|||||||
import random
|
import contextlib
|
||||||
|
import time
|
||||||
import typing as T
|
import typing as T
|
||||||
|
|
||||||
|
RngState = T.Tuple[float, float, float, int]
|
||||||
|
|
||||||
|
_2_POW_32 = 0x100000000
|
||||||
|
_2_POW_MINUS_32 = 2.3283064365386963e-10
|
||||||
|
|
||||||
|
|
||||||
|
def _to_uint32(value: float) -> float:
|
||||||
|
# JS `value >>> 0` for the non-negative values Mash produces
|
||||||
|
return float(int(value) % _2_POW_32)
|
||||||
|
|
||||||
|
|
||||||
|
def _mash() -> T.Callable[[str], float]:
|
||||||
|
n = float(0xEFC8249D)
|
||||||
|
|
||||||
|
def mash(data: str) -> float:
|
||||||
|
nonlocal n
|
||||||
|
# JS strings are UTF-16, and charCodeAt() returns code units
|
||||||
|
units = data.encode("utf-16-le")
|
||||||
|
for i in range(0, len(units), 2):
|
||||||
|
n += units[i] | (units[i + 1] << 8)
|
||||||
|
h = 0.02519603282416938 * n
|
||||||
|
n = _to_uint32(h)
|
||||||
|
h -= n
|
||||||
|
h *= n
|
||||||
|
n = _to_uint32(h)
|
||||||
|
h -= n
|
||||||
|
n += h * _2_POW_32
|
||||||
|
return _to_uint32(n) * _2_POW_MINUS_32
|
||||||
|
|
||||||
|
return mash
|
||||||
|
|
||||||
|
|
||||||
|
class Alea:
|
||||||
|
"""Johannes Baagøe's Alea PRNG (MIT), as used by Progress Quest web.
|
||||||
|
|
||||||
|
Ported bit for bit so that games seeded alike play out alike. Saves
|
||||||
|
pickle this class by import path, so it must stay in this module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args: str) -> None:
|
||||||
|
if not args:
|
||||||
|
args = (str(int(time.time() * 1000)),)
|
||||||
|
mash = _mash()
|
||||||
|
self._s0 = mash(" ")
|
||||||
|
self._s1 = mash(" ")
|
||||||
|
self._s2 = mash(" ")
|
||||||
|
self._c = 1
|
||||||
|
for arg in args:
|
||||||
|
self._s0 -= mash(arg)
|
||||||
|
if self._s0 < 0:
|
||||||
|
self._s0 += 1
|
||||||
|
self._s1 -= mash(arg)
|
||||||
|
if self._s1 < 0:
|
||||||
|
self._s1 += 1
|
||||||
|
self._s2 -= mash(arg)
|
||||||
|
if self._s2 < 0:
|
||||||
|
self._s2 += 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_state(cls, state: RngState) -> "Alea":
|
||||||
|
rng = cls.__new__(cls)
|
||||||
|
rng._s0, rng._s1, rng._s2, rng._c = state
|
||||||
|
return rng
|
||||||
|
|
||||||
|
def state(self) -> RngState:
|
||||||
|
return (self._s0, self._s1, self._s2, self._c)
|
||||||
|
|
||||||
|
def random(self) -> float:
|
||||||
|
t = 2091639 * self._s0 + self._c * _2_POW_MINUS_32
|
||||||
|
self._s0 = self._s1
|
||||||
|
self._s1 = self._s2
|
||||||
|
self._c = int(t)
|
||||||
|
self._s2 = t - self._c
|
||||||
|
return self._s2
|
||||||
|
|
||||||
|
def uint32(self) -> int:
|
||||||
|
return int(self.random() * _2_POW_32)
|
||||||
|
|
||||||
|
|
||||||
|
_current = Alea()
|
||||||
|
|
||||||
|
|
||||||
def seed(source: str) -> None:
|
def seed(source: str) -> None:
|
||||||
random.seed(source.encode())
|
global _current
|
||||||
|
_current = Alea(source)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def using(rng: Alea) -> T.Iterator[None]:
|
||||||
|
"""Draw from rng instead of the shared generator inside the block."""
|
||||||
|
global _current
|
||||||
|
previous = _current
|
||||||
|
_current = rng
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
_current = previous
|
||||||
|
|
||||||
|
|
||||||
def choice(source: T.Sequence[T.Any]) -> T.Any:
|
def choice(source: T.Sequence[T.Any]) -> T.Any:
|
||||||
@@ -15,7 +110,7 @@ def choice_low(source: T.Sequence[T.Any]) -> T.Any:
|
|||||||
|
|
||||||
|
|
||||||
def below(num: int) -> int:
|
def below(num: int) -> int:
|
||||||
return random.randint(0, num - 1)
|
return _current.uint32() % num
|
||||||
|
|
||||||
|
|
||||||
def below_low(num: int) -> int:
|
def below_low(num: int) -> int:
|
||||||
|
|||||||
@@ -124,7 +124,11 @@ class BasicUserInterface(BaseUserInterface):
|
|||||||
break
|
break
|
||||||
|
|
||||||
player = create_player(
|
player = create_player(
|
||||||
name=name, race=race, class_=class_, stats=stats
|
name=name,
|
||||||
|
race=race,
|
||||||
|
class_=class_,
|
||||||
|
stats=stats,
|
||||||
|
seed=stats_builder.seed,
|
||||||
)
|
)
|
||||||
self.roster.players.append(player)
|
self.roster.players.append(player)
|
||||||
if auto_play and self.confirm(
|
if auto_play and self.confirm(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from pqcli import lingo
|
|||||||
from pqcli.config import Class, Race
|
from pqcli.config import Class, Race
|
||||||
from pqcli.game_data import GameData
|
from pqcli.game_data import GameData
|
||||||
from pqcli.mechanic import Player, Stats, create_player
|
from pqcli.mechanic import Player, Stats, create_player
|
||||||
|
from pqcli.random import RngState
|
||||||
from pqcli.roster import Roster
|
from pqcli.roster import Roster
|
||||||
from pqcli.ui.base import BaseUserInterface
|
from pqcli.ui.base import BaseUserInterface
|
||||||
from pqcli.ui.curses.colors import (
|
from pqcli.ui.curses.colors import (
|
||||||
@@ -156,8 +157,8 @@ class CursesUserInterface(BaseUserInterface):
|
|||||||
view.on_cancel += lambda: self._switch_to_create_char_class_view(
|
view.on_cancel += lambda: self._switch_to_create_char_class_view(
|
||||||
player_name, race, class_
|
player_name, race, class_
|
||||||
)
|
)
|
||||||
view.on_confirm += lambda stats: self._create_character(
|
view.on_confirm += lambda stats, seed: self._create_character(
|
||||||
player_name, race, class_, stats
|
player_name, race, class_, stats, seed
|
||||||
)
|
)
|
||||||
self._switch_view(view)
|
self._switch_view(view)
|
||||||
|
|
||||||
@@ -196,9 +197,14 @@ class CursesUserInterface(BaseUserInterface):
|
|||||||
self._view.start()
|
self._view.start()
|
||||||
|
|
||||||
def _create_character(
|
def _create_character(
|
||||||
self, name: str, race: Race, class_: Class, stats: Stats
|
self,
|
||||||
|
name: str,
|
||||||
|
race: Race,
|
||||||
|
class_: Class,
|
||||||
|
stats: Stats,
|
||||||
|
seed: RngState,
|
||||||
) -> None:
|
) -> None:
|
||||||
player = create_player(name, race, class_, stats)
|
player = create_player(name, race, class_, stats, seed)
|
||||||
self.roster.players.append(player)
|
self.roster.players.append(player)
|
||||||
if self.args.use_saves:
|
if self.args.use_saves:
|
||||||
self.roster.save()
|
self.roster.save()
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class ChooseCharacterStatsView(BaseView):
|
|||||||
elif self._active_widget == 1:
|
elif self._active_widget == 1:
|
||||||
self._stats = self._stats_builder.unroll()
|
self._stats = self._stats_builder.unroll()
|
||||||
elif self._active_widget == 2:
|
elif self._active_widget == 2:
|
||||||
self.on_confirm(self._stats)
|
self.on_confirm(self._stats, self._stats_builder.seed)
|
||||||
elif self._active_widget == 3:
|
elif self._active_widget == 3:
|
||||||
self.on_cancel()
|
self.on_cancel()
|
||||||
else:
|
else:
|
||||||
@@ -68,7 +68,7 @@ class ChooseCharacterStatsView(BaseView):
|
|||||||
self._stats = self._stats_builder.unroll()
|
self._stats = self._stats_builder.unroll()
|
||||||
|
|
||||||
elif key == curses.KEY_F10:
|
elif key == curses.KEY_F10:
|
||||||
self.on_confirm(self._stats)
|
self.on_confirm(self._stats, self._stats_builder.seed)
|
||||||
|
|
||||||
elif key in KEYS_CYCLE:
|
elif key in KEYS_CYCLE:
|
||||||
self._active_widget = (self._active_widget + 1) % 4
|
self._active_widget = (self._active_widget + 1) % 4
|
||||||
|
|||||||
Vendored
+1789
-1787
File diff suppressed because it is too large.
Load diff
Vendored
+1775
-1775
File diff suppressed because it is too large.
Load diff
Vendored
+1847
-1852
File diff suppressed because it is too large.
Load diff
@@ -6,6 +6,7 @@ from pqcli import random
|
|||||||
from pqcli.config import StatType
|
from pqcli.config import StatType
|
||||||
from pqcli.game_data import load_game_data
|
from pqcli.game_data import load_game_data
|
||||||
from pqcli.mechanic import Player, Simulation, Stats, monster_task
|
from pqcli.mechanic import Player, Simulation, Stats, monster_task
|
||||||
|
from pqcli.random import Alea
|
||||||
|
|
||||||
STATS = {
|
STATS = {
|
||||||
StatType.strength: 12,
|
StatType.strength: 12,
|
||||||
@@ -28,6 +29,7 @@ def run_trace(seed: str, ticks: int = 3000) -> T.List[str]:
|
|||||||
race=data.races[0],
|
race=data.races[0],
|
||||||
class_=data.classes[0],
|
class_=data.classes[0],
|
||||||
stats=Stats(dict(STATS)),
|
stats=Stats(dict(STATS)),
|
||||||
|
rng=Alea(seed),
|
||||||
)
|
)
|
||||||
random.seed(seed)
|
random.seed(seed)
|
||||||
simulation = Simulation(player, data)
|
simulation = Simulation(player, data)
|
||||||
@@ -40,6 +42,7 @@ def run_trace(seed: str, ticks: int = 3000) -> T.List[str]:
|
|||||||
for _ in range(ticks):
|
for _ in range(ticks):
|
||||||
simulation.tick(100000.0)
|
simulation.tick(100000.0)
|
||||||
|
|
||||||
|
with random.using(player.rng):
|
||||||
# plot cinematics and quests are rare in a short run, so force them
|
# plot cinematics and quests are rare in a short run, so force them
|
||||||
for _ in range(30):
|
for _ in range(30):
|
||||||
simulation.interplot_cinematic()
|
simulation.interplot_cinematic()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import pickle
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -11,9 +12,12 @@ from pqcli.mechanic import (
|
|||||||
Player,
|
Player,
|
||||||
Simulation,
|
Simulation,
|
||||||
Stats,
|
Stats,
|
||||||
|
StatsBuilder,
|
||||||
|
create_player,
|
||||||
impressive_guy,
|
impressive_guy,
|
||||||
level_up_time,
|
level_up_time,
|
||||||
)
|
)
|
||||||
|
from pqcli.random import Alea
|
||||||
|
|
||||||
|
|
||||||
def test_level_up_time_matches_upstream() -> None:
|
def test_level_up_time_matches_upstream() -> None:
|
||||||
@@ -24,6 +28,10 @@ def test_level_up_time_matches_upstream() -> None:
|
|||||||
assert level_up_time(100) == 70460007
|
assert level_up_time(100) == 70460007
|
||||||
|
|
||||||
|
|
||||||
|
RACE = load_game_data().races[0]
|
||||||
|
CLASS = load_game_data().classes[0]
|
||||||
|
|
||||||
|
|
||||||
def make_simulation() -> Simulation:
|
def make_simulation() -> Simulation:
|
||||||
data = load_game_data()
|
data = load_game_data()
|
||||||
player = Player(
|
player = Player(
|
||||||
@@ -32,6 +40,7 @@ def make_simulation() -> Simulation:
|
|||||||
race=data.races[0],
|
race=data.races[0],
|
||||||
class_=data.classes[0],
|
class_=data.classes[0],
|
||||||
stats=Stats({stat: 10 for stat in StatType}),
|
stats=Stats({stat: 10 for stat in StatType}),
|
||||||
|
rng=Alea("test"),
|
||||||
)
|
)
|
||||||
return Simulation(player, data)
|
return Simulation(player, data)
|
||||||
|
|
||||||
@@ -181,3 +190,53 @@ def test_prologue_fills_plot_bar() -> None:
|
|||||||
]
|
]
|
||||||
assert positions[5][0] == "Loading Act I"
|
assert positions[5][0] == "Loading Act I"
|
||||||
assert positions[6][0] == "Heading to the killing fields"
|
assert positions[6][0] == "Heading to the killing fields"
|
||||||
|
|
||||||
|
|
||||||
|
def test_player_rng_survives_save_and_load() -> None:
|
||||||
|
simulation = make_simulation()
|
||||||
|
for _ in range(50):
|
||||||
|
simulation.tick(100000.0)
|
||||||
|
restored = pickle.loads(pickle.dumps(simulation.player))
|
||||||
|
assert restored.rng.state() == simulation.player.rng.state()
|
||||||
|
|
||||||
|
|
||||||
|
def test_old_save_without_rng_gets_one() -> None:
|
||||||
|
player = make_simulation().player
|
||||||
|
state = player.__dict__.copy()
|
||||||
|
del state["rng"]
|
||||||
|
restored = Player.__new__(Player)
|
||||||
|
restored.__setstate__(state)
|
||||||
|
assert isinstance(restored.rng, Alea)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tick_draws_from_player_rng_only() -> None:
|
||||||
|
random.seed("global")
|
||||||
|
expected = Alea("global").uint32() % 1000
|
||||||
|
simulation = make_simulation()
|
||||||
|
before = simulation.player.rng.state()
|
||||||
|
for _ in range(50):
|
||||||
|
simulation.tick(100000.0)
|
||||||
|
assert simulation.player.rng.state() != before
|
||||||
|
assert random.below(1000) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_game_replays_stat_roll_numbers() -> None:
|
||||||
|
# upstream seeds the game with the state from before the stat roll
|
||||||
|
builder = StatsBuilder(Alea("roll"))
|
||||||
|
builder.roll()
|
||||||
|
stats = builder.roll()
|
||||||
|
player = create_player("New", RACE, CLASS, stats, builder.seed)
|
||||||
|
assert player.rng.state() == builder.history[-1].seed
|
||||||
|
with random.using(player.rng):
|
||||||
|
rerolled = StatsBuilder(player.rng).roll()
|
||||||
|
assert dict(rerolled) == dict(stats)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unroll_restores_previous_stats_and_generator() -> None:
|
||||||
|
builder = StatsBuilder(Alea("roll"))
|
||||||
|
first = builder.roll()
|
||||||
|
after_first = builder.history[-1].after
|
||||||
|
builder.roll()
|
||||||
|
assert builder.unroll() is first
|
||||||
|
assert builder.seed == builder.history[-1].seed
|
||||||
|
assert builder._rng.state() == after_first
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import pickle
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pqcli import random
|
||||||
|
from pqcli.random import Alea
|
||||||
|
|
||||||
|
# First outputs and resulting state of upstream's JS Alea(seed).uint32()
|
||||||
|
VECTORS = {
|
||||||
|
"alpha": (
|
||||||
|
[3768637044, 756790225, 3147161637, 1726577786, 279486812],
|
||||||
|
(0.732755669625476, 0.4020002172328532, 0.06507309433072805, 368555),
|
||||||
|
),
|
||||||
|
"": (
|
||||||
|
[715789690, 2091287642, 486307, 3866591158, 760028033],
|
||||||
|
(
|
||||||
|
0.00011322717182338238,
|
||||||
|
0.9002609080635011,
|
||||||
|
0.17695781611837447,
|
||||||
|
1018452,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"1697543210123": (
|
||||||
|
[3110287155, 2843160345, 1794106530, 2361562046, 1804721005],
|
||||||
|
(
|
||||||
|
0.41772297816351056,
|
||||||
|
0.5498440111987293,
|
||||||
|
0.42019435320980847,
|
||||||
|
1384612,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
# Mash hashes UTF-16 code units, so the dragon counts as two
|
||||||
|
"Ünïcødé 🐉": (
|
||||||
|
[4162081648, 198215584, 541466577, 6418444, 2254846220],
|
||||||
|
(
|
||||||
|
0.12607001163996756,
|
||||||
|
0.0014944104477763176,
|
||||||
|
0.5249972967430949,
|
||||||
|
96530,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("seed", VECTORS)
|
||||||
|
def test_alea_matches_upstream(seed: str) -> None:
|
||||||
|
first, state = VECTORS[seed]
|
||||||
|
rng = Alea(seed)
|
||||||
|
assert [rng.uint32() for _ in first] == first
|
||||||
|
assert rng.state() == state
|
||||||
|
|
||||||
|
|
||||||
|
def test_alea_state_round_trip() -> None:
|
||||||
|
rng = Alea("alpha")
|
||||||
|
rng.uint32()
|
||||||
|
copy = Alea.from_state(rng.state())
|
||||||
|
assert [copy.uint32() for _ in range(100)] == [
|
||||||
|
rng.uint32() for _ in range(100)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_alea_pickles_with_its_state() -> None:
|
||||||
|
rng = Alea("alpha")
|
||||||
|
rng.uint32()
|
||||||
|
copy = pickle.loads(pickle.dumps(rng))
|
||||||
|
assert copy.state() == rng.state()
|
||||||
|
|
||||||
|
|
||||||
|
def test_unseeded_alea_differs() -> None:
|
||||||
|
assert Alea().state() != Alea("alpha").state()
|
||||||
|
|
||||||
|
|
||||||
|
def test_below_is_uint32_modulo() -> None:
|
||||||
|
# Random(n) = seed.uint32() % n
|
||||||
|
random.seed("alpha")
|
||||||
|
assert [random.below(1000) for _ in range(5)] == [
|
||||||
|
n % 1000 for n in VECTORS["alpha"][0]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_using_restores_previous_generator() -> None:
|
||||||
|
random.seed("outer")
|
||||||
|
expected = Alea("outer").uint32() % 1000
|
||||||
|
inner = Alea("inner")
|
||||||
|
with random.using(inner):
|
||||||
|
random.below(10)
|
||||||
|
assert inner.state() != Alea("inner").state()
|
||||||
|
assert random.below(1000) == expected
|
||||||
Reference in new issue
Block a user