cg-core: make cg-shuffle optionally seeded and reproducible

cg-shuffle gains an optional SEED and a `cg-shuffle-seed' dynamic var.
With a seed the order is deterministic and identical on every platform,
so a deal can be replayed, shared between players, or pinned in a test;
the index comes from secure-hash (spec-defined output everywhere, 7 hex
digits stay a fixnum even on 32-bit Emacs), so no bignum/overflow
behaviour can vary it.  With no seed the shuffle is the byte-for-byte
old system-random path -- normal play is unchanged.

Test cgt-shuffle-seeded pins determinism, permutation, override and a
cross-platform golden prefix; fails on the prior one-arg cg-shuffle.
Suite 159/159.
This commit is contained in:
Corwin Brust 2026-08-03 22:18:02 -05:00
parent 1a59003b13
commit 6379127e17
8 changed files with 180 additions and 15 deletions

View file

@ -216,12 +216,39 @@ Normalised to a boolean so callers may compare two results with `eq'
"Return the other suit index of the same colour as SUIT."
(pcase suit (0 1) (1 0) (2 3) (3 2)))
(defun cg-shuffle (seq)
"Return a new list with the elements of SEQ in random order."
(defvar cg-shuffle-seed nil
"When non-nil, `cg-shuffle' is deterministic, seeded by this value.
The value may be a number or a string. Bind it around a deal to
reproduce that deal exactly -- for replays, for a deal shared between
players, or for a repeatable test -- and the same seed yields the same
order on every platform Emacs runs on. nil (the default) means an
unpredictable shuffle drawn from the system `random'.")
(defun cg-shuffle--seeded-index (seed i limit)
"Return a deterministic index in [0, LIMIT) for step I under SEED.
Uses `secure-hash', whose output is identical on every platform and
Emacs build, so a seeded deal is reproducible and shareable. The 7
hex digits taken span 28 bits, which stays a fixnum even on a 32-bit
Emacs, so no bignum or overflow behaviour can vary the result."
(let ((r (string-to-number
(substring (secure-hash 'sha256 (format "%s|%d" seed i)) 0 7)
16)))
(mod r limit)))
(defun cg-shuffle (seq &optional seed)
"Return a new list with the elements of SEQ in random order.
With a non-nil SEED, or a non-nil `cg-shuffle-seed', the shuffle is
deterministic: the same seed produces the same order on every platform,
so a deal can be replayed or shared between players. An explicit SEED
overrides `cg-shuffle-seed'. The seed may be a number or a string.
With no seed the order is unpredictable (system `random')."
(let* ((v (vconcat seq))
(n (length v)))
(n (length v))
(seed (or seed cg-shuffle-seed)))
(dotimes (i n)
(let ((j (+ i (random (- n i)))))
(let ((j (+ i (if seed
(cg-shuffle--seeded-index seed i (- n i))
(random (- n i))))))
(cl-rotatef (aref v i) (aref v j))))
(append v nil)))