"""V2.9 deterministic rules and offline verification. Standard library only.""" import copy import hashlib import hmac import json from decimal import Decimal VERSION = "2.9" ORDER = ("lapis", "ruby", "amber", "eye", "ankh", "necklace", "scarab") BASELINE_RULES = { "ruleVersion": VERSION, "bets": [100, 200, 500, 1000, 2000], "gridColumns": 6, "gridRows": 5, "maxCascades": 5, "symbolMap": dict(zip(ORDER + ("pyramid", "pharaoh", "catQueen", "jackpot", "start"), ("牛符咒", "兔符咒", "蛇符咒", "猴符咒", "羊符咒", "马符咒", "猪符咒", "虎符咒", "鸡符咒", "狗符咒", "龙符咒", "鼠符咒"))), "payouts": dict(zip(ORDER, ([.17,.39,.87],[.26,.56,1.21],[.35,.78,1.56],[.48,1.04,2.16],[.61,1.30,2.60],[.87,1.90,3.89],[1.21,2.60,5.19]))), "symbolWeights": dict(zip(ORDER, (20,18,16,14,12,10,10))), "payoutThresholds": [8,10,12], "thresholdRevision": "8-10-12-v1", "multiplierPaid": [[1,59],[2,21],[3,10],[5,8],[10,2]], "multiplierFree": [[1,59],[2,21],[3,10],[5,8],[10,2]], "pyramidCellChancePct": 2, "pyramidMaxPerBoard": 4, "freeTriggerCount": 3, "freeAward": 5, "freeRetriggerAward": 2, "freeMax": 20, "freeAutoResolveHours": 24, "pharaohColumnChancePct": 15, "catQueenColumnChancePct": 15, "pharaohRefillChancePct": 7.5, "catQueenRefillChancePct": 7.5, "pharaohColumnsToWin": 5, "pharaohAwardBetMultiple": 300, "catQueenColumnsToWin": 5, "catQueenAwardBetMultiple": 150, "jackpotContributionPct": 20, "poolContributionRevision": "20-percent-v1", "jackpotChanceByBetPct": [.05,.10,.15,.20,.25], "jackpotSeed": 50000, "startingCoins": 10000, } def canonical(value): return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) def digest(value): return hashlib.sha256(canonical(value).encode()).hexdigest() def scaled(value, scale=100): return int(Decimal(str(value)) * scale) class FairRandom: """Counter-based HMAC-SHA256 with unbiased 256-bit rejection sampling.""" def __init__(self, seed, client_seed, nonce, ticket_id, request_id): self.key = bytes.fromhex(seed) self.prefix = f"{VERSION}|{nonce}|{ticket_id}|{request_id}|{client_seed}|" self.counter = 0 def randbelow(self, bound): if bound < 1: raise ValueError("Empty random range") limit = (1 << 256) - ((1 << 256) % bound) while True: message = (self.prefix + str(self.counter)).encode() self.counter += 1 value = int.from_bytes(hmac.new(self.key, message, hashlib.sha256).digest(), "big") if value < limit: return value % bound def chance(rng, percent): return rng.randbelow(1_000_000) < scaled(percent, 10000) def weighted(rng, items): needle = rng.randbelow(sum(weight for _, weight in items)) for item, weight in items: if needle < weight: return item needle -= weight raise AssertionError("Invalid weights") def shuffle(rng, items): for i in range(len(items) - 1, 0, -1): j = rng.randbelow(i + 1) items[i], items[j] = items[j], items[i] def place_specials(board, rules, rng, refill=False): """Separate rolls; shared empty slots; dog wins a one-slot refill tie.""" for col in board: empty = [i for i, cell in enumerate(col) if cell is None] if not empty: continue suffix = "RefillChancePct" if refill else "ColumnChancePct" chicken = "pharaoh" not in col and chance(rng, rules["pharaoh" + suffix]) dog = "catQueen" not in col and chance(rng, rules["catQueen" + suffix]) symbols = (["catQueen"] if dog else []) + (["pharaoh"] if chicken else []) for symbol in symbols: if not empty: break row = empty.pop(rng.randbelow(len(empty))) col[row] = symbol def fill_ordinary(board, rules, rng): items = [(key, scaled(rules["symbolWeights"][key])) for key in ORDER] for col in board: for row, cell in enumerate(col): if cell is None: col[row] = weighted(rng, items) def initial_board(rules, rng): board = [[None] * rules["gridRows"] for _ in range(rules["gridColumns"])] place_specials(board, rules, rng) empty = [(c, r) for c, col in enumerate(board) for r, value in enumerate(col) if value is None] shuffle(rng, empty) # The cap must not systematically favor earlier columns/rows. tigers = 0 for c, r in empty: if tigers < rules["pyramidMaxPerBoard"] and chance(rng, rules["pyramidCellChancePct"]): board[c][r] = "pyramid" tigers += 1 fill_ordinary(board, rules, rng) return board def special_awards(board, rules, bet): chicken = [c for c, col in enumerate(board) if "pharaoh" in col] dog = [c for c, col in enumerate(board) if "catQueen" in col] chicken_award = bet * rules["pharaohAwardBetMultiple"] if len(chicken) >= rules["pharaohColumnsToWin"] else 0 dog_award = bet * rules["catQueenAwardBetMultiple"] if not chicken_award and len(dog) >= rules["catQueenColumnsToWin"] else 0 return chicken, dog, chicken_award, dog_award def ordinary_wins(board, rules, bet): wins = [] for symbol in ORDER: positions = [[c, r] for c, col in enumerate(board) for r, cell in enumerate(col) if cell == symbol] count = len(positions) thresholds = rules["payoutThresholds"] if count < thresholds[0]: continue tier = 0 if count < thresholds[1] else 1 if count < thresholds[2] else 2 amount = bet * scaled(rules["payouts"][symbol][tier], 10000) // 10000 wins.append({"symbol": symbol, "count": count, "tier": tier, "amount": amount, "positions": positions}) return wins def drop_board(board): result = [] for col in board: keep = [cell for cell in col if cell is not None] result.append([None] * (len(col) - len(keep)) + keep) return result def draw(rules, bet, free, pool, rng): board = initial_board(rules, rng) initial = copy.deepcopy(board) tiger_count = sum(col.count("pyramid") for col in board) pending_free = (rules["freeRetriggerAward"] if free else rules["freeAward"]) if tiger_count >= rules["freeTriggerCount"] else 0 chains, base = [], 0 stopped_by = "no-ordinary-win" for stage in range(1, rules["maxCascades"] + 1): chicken, dog, chicken_award, dog_award = special_awards(board, rules, bet) if chicken_award or dog_award: pending_free = 0 stopped_by = "chicken" if chicken_award else "dog" break wins = ordinary_wins(board, rules, bet) if not wins: break before = copy.deepcopy(board) base += sum(win["amount"] for win in wins) for win in wins: for c, r in win["positions"]: board[c][r] = None removed = copy.deepcopy(board) board = drop_board(board) dropped = copy.deepcopy(board) refilled = stage < rules["maxCascades"] if refilled: place_specials(board, rules, rng, refill=True) fill_ordinary(board, rules, rng) chains.append({"chain": stage, "before": before, "wins": wins, "afterRemoval": removed, "afterDrop": dropped, "after": copy.deepcopy(board), "refilled": refilled}) if not refilled: stopped_by = "cascade-limit" chicken, dog, chicken_award, dog_award = special_awards(board, rules, bet) mult = weighted(rng, [(pair[0], scaled(pair[1])) for pair in rules["multiplierPaid"]]) if chains else 1 regular = base * mult # This draw always occurs after the entire board stage, including an early stop. jackpot_hit = chance(rng, rules["jackpotChanceByBetPct"][rules["bets"].index(bet)]) jackpot_award = pool if jackpot_hit else 0 cancelled = "dragon" if jackpot_hit and pending_free else stopped_by if stopped_by in ("chicken", "dog") and tiger_count >= rules["freeTriggerCount"] else None if jackpot_hit: pending_free = 0 grid = {"ruleVersion": VERSION, "initialReels": initial, "reels": board, "pharaohColumns": chicken, "catQueenColumns": dog, "pharaohAward": chicken_award, "catQueenAward": dog_award, "pyramidCount": tiger_count, "chains": chains, "ordinaryBase": base, "ordinaryAward": regular, "multiplier": mult, "stoppedBy": stopped_by, "tigerCancelledBy": cancelled, "jackpotHit": jackpot_hit, "jackpotAward": jackpot_award, "jackpotPool": pool, "freeAward": pending_free} return grid, regular + chicken_award + dog_award + jackpot_award, pending_free def settle_state(rules, context, grid, payout, granted): free = context["isFree"] prior_total = context["freeGrantedBefore"] if free else 0 effective = min(granted, max(0, rules["freeMax"] - prior_total)) remaining = context["freeBefore"] - (1 if free else 0) + effective expiry = (context["freeExpiresBefore"] if free else context["settledAt"] + rules["freeAutoResolveHours"] * 3600) if remaining else None return {"coins": context["coinsBefore"] - (0 if free else context["bet"]) + payout, "free": remaining, "freeBet": context["freeBetBefore"] if free or not effective else context["bet"], "freeGrantedTotal": prior_total + effective if remaining else 0, "freeExpires": expiry, "granted": effective, "jackpot": rules["jackpotSeed"] if grid["jackpotHit"] else context["availablePool"]} def verify_proof(proof, expected_commitment=None): """Replay every cell, cascade, award and balance without contacting a server.""" if proof["version"] != VERSION or proof["algorithm"] != "hmac-sha256-commit-chain-v1": raise ValueError("Unsupported verification version") body = {key: value for key, value in proof.items() if key != "auditHash"} if digest(body) != proof["auditHash"]: raise ValueError("Audit hash mismatch") ticket = proof["ticket"] seed_hash = hashlib.sha256(bytes.fromhex(proof["serverSeed"])).hexdigest() if seed_hash != ticket["commitment"] or expected_commitment and seed_hash != expected_commitment: raise ValueError("Precommitted seed mismatch") if digest(proof["rules"]) != ticket["rulesHash"]: raise ValueError("Rule snapshot mismatch") rng = FairRandom(proof["serverSeed"], proof["clientSeed"], ticket["nonce"], ticket["id"], proof["requestId"]) ctx = proof["context"] expected_contribution = 0 if ctx["isFree"] else ctx["bet"] * scaled(proof["rules"]["jackpotContributionPct"]) // 10000 if ctx["contribution"] != expected_contribution or ctx["availablePool"] != ctx["poolBefore"] + expected_contribution: raise ValueError("Pool contribution mismatch") grid, payout, granted = draw(proof["rules"], ctx["bet"], ctx["isFree"], ctx["availablePool"], rng) if {"grid": grid, "payout": payout, "granted": granted} != proof["result"]: raise ValueError("Draw or payout mismatch") if settle_state(proof["rules"], ctx, grid, payout, granted) != proof["stateAfter"]: raise ValueError("Balance or free-spin mismatch") return True