mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
ty reported 20 diagnostics, and 0.0.89 adds a few more; this clears them all without suppressions. - The quest monster is held in a local before being stored on the quest book, so its name no longer reads from an Optional. - PgUp/PgDn narrow the focused window with isinstance against a runtime-checkable PageScrollable protocol instead of hasattr. - The ^t cheat used to monkeypatch SignalMixin.emit. SignalMixin now has a muted() context manager that emit() checks, and it restores emission even if a tick raises. - GameView keeps its children as GameWindow, a protocol declaring stop() and sync(), so the cheat's resync type-checks. - curses.ascii is imported explicitly where it is used. - ProgressBarWindow is now Focusable itself, instead of calling Focusable.focus_standout() on a class that is not Focusable. Its four subclasses drop the duplicate Focusable base; the MRO and initialisation order are unchanged. - MainMenu narrows the Optional pad before colouring the logo. - The tests that record tasks assert player.task is set. - Context managers are annotated as Generator rather than Iterator, which ty flags as deprecated for @contextmanager. - The redundant cast() around getch() is gone. - The frozen GameData test assigns through setattr(), since the direct assignment is an error by design. Bug fixes: - ListBox.select(None) crashed on `None < 0`; it now clears the selection, as DataTable.select(None) already does. - ListBox.get() caught ValueError, which indexing never raises; it now catches IndexError, so an out-of-range index returns None as intended.
123 lines
3.0 KiB
Python
123 lines
3.0 KiB
Python
import contextlib
|
|
import time
|
|
from collections.abc import Callable, Generator, Sequence
|
|
from typing import Any
|
|
|
|
RngState = 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() -> 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:
|
|
global _current
|
|
_current = Alea(source)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def using(rng: Alea) -> Generator[None, None, 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: Sequence[Any]) -> Any:
|
|
return source[below(len(source))]
|
|
|
|
|
|
def choice_low(source: Sequence[Any]) -> Any:
|
|
return source[below_low(len(source))]
|
|
|
|
|
|
def below(num: int) -> int:
|
|
return _current.uint32() % num
|
|
|
|
|
|
def below_low(num: int) -> int:
|
|
return min(below(num), below(num))
|
|
|
|
|
|
def odds(chance: int, out_of: int) -> bool:
|
|
return below(out_of) < chance
|