#!/usr/bin/env python3 """ SiggySoft Solitaire: recompute any deal on a PC (the deal specification, docs/DEAL-SPEC.md). python deal.py klondike 4711 numbered deal 4711 of Klondike python deal.py spider 1 --suits 1 Spider with one suit (its deck is 104 spades) python deal.py --seed <128 hex digits> klondike python deal.py --code SOL1-klondike-draw=1;passes=unlimited;scoring=standard-N4711 Prints the shuffled deck, first card dealt first. Each game lays these cards out in its own fixed way (Klondike: seven piles across, a row at a time). Uses only Python's standard library. """ import argparse import base64 import hashlib import struct DEAL_DOMAIN = b"SiggySoft Solitaire deal stream v1" NUMBERED_DOMAIN = b"SiggySoft Solitaire numbered deal v1" SUITS = "SHDC" RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"] # Games whose numbered deals share one deal key (draw 1 and draw 3 Klondike deal the same cards). TWO_DECKS = {"spider", "fortythieves"} def numbered_seed(deal_key: str, number: int) -> bytes: return hashlib.sha512(NUMBERED_DOMAIN + b"\0" + deal_key.encode() + b"\0" + struct.pack(">q", number)).digest() class Stream: """block i = SHA-512(domain 00 seed i as 8 bytes, big-endian); read in order.""" def __init__(self, seed: bytes, domain: bytes = DEAL_DOMAIN): self.prefix = domain + b"\0" + seed self.counter = 0 self.buf = b"" def u32(self) -> int: while len(self.buf) < 4: self.buf += hashlib.sha512(self.prefix + struct.pack(">q", self.counter)).digest() self.counter += 1 v = int.from_bytes(self.buf[:4], "big") self.buf = self.buf[4:] return v def below(self, n: int) -> int: """Rejection sampling: no number is more likely than another.""" limit = (2**32 // n) * n while True: x = self.u32() if x < limit: return x % n def permutation(self, size: int) -> list: a = list(range(size)) for i in range(size - 1, 0, -1): j = self.below(i + 1) a[i], a[j] = a[j], a[i] return a def deck_for(game: str, suits: int) -> list: """The fresh, unshuffled deck: spades ace to king, then hearts, diamonds, clubs (a second deck repeats).""" if game == "spider": use = {1: "S", 2: "SH", 4: "SHDC"}[suits] return [r + use[run % len(use)] for run in range(8) for r in RANKS] decks = 2 if game in TWO_DECKS else 1 return [r + s for _ in range(decks) for s in SUITS for r in RANKS] def main(): ap = argparse.ArgumentParser() ap.add_argument("game", nargs="?") ap.add_argument("number", nargs="?", type=int) ap.add_argument("--suits", type=int, default=1) ap.add_argument("--seed") ap.add_argument("--code") a = ap.parse_args() game, suits = a.game, a.suits if a.code: head, game, opts, tail = a.code.split("-", 3) if "suits=" in opts: suits = int(opts.split("suits=")[1].split(";")[0]) if tail[0] == "N": seed = numbered_seed(game, int(tail[1:])) elif tail[0] == "S": t = tail[1:] seed = base64.b32decode(t + "=" * (-len(t) % 8)) else: raise SystemExit("classic FreeCell numbers use the classic generator, not this script") elif a.seed: seed = bytes.fromhex(a.seed) else: seed = numbered_seed(game, a.number) deck = deck_for(game, suits) order = Stream(seed).permutation(len(deck)) print("seed ", seed.hex()) print("order", " ".join(deck[i] for i in order)) if __name__ == "__main__": main()