Diese Seite ist ohne Anmeldung erreichbar. Sie enthält alles, was nötig ist,
um eine beliebige vergangene Runde ohne Mithilfe des Betreibers nachzurechnen.
Die Berechnung im Klartext
Diese Datei ist die unabhängige Zweitimplementierung des Verfahrens. Sie
ist aus der Spezifikation geschrieben, nicht aus dem Anwendungscode
übersetzt, und liefert nachweislich dieselben Ergebnisse. Speichern,
ausführen, selbst nachrechnen – es braucht nur Python.
#!/usr/bin/env python3
"""
Unabhaengige Zweitimplementierung des Provably-Fair-Verfahrens.
CLAUDE.md §8 verlangt sie: "Eine unabhaengig geschriebene Zweitimplementierung
muss dieselben Ergebnisse liefern." Sinn der Sache ist, dass ein Denkfehler
nicht in beiden Implementierungen gleich ausfaellt. Deshalb ist diese Datei
aus der Spezifikation in §3 geschrieben, nicht aus dem TypeScript-Code
uebersetzt, und sie teilt mit ihm keine einzige Hilfsfunktion.
Sie ist zugleich das Stueck Code, das auf der Transparenzseite im Klartext
steht: kurz genug, dass ein Aussenstehender es liest, und in einer Sprache,
die er ohne Projekt-Setup ausfuehren kann.
Aufruf:
python3 tools/referenz.py vektoren # Testvektoren als JSON
python3 tools/referenz.py pruefe FILE # Eingaben pruefen, Ergebnis als JSON
"""
import hashlib
import hmac
import json
import sys
def raw(server_seed: str, client_seed: str, nonce: int, cursor: int) -> bytes:
"""HMAC_SHA256(key = serverSeed, message = "<clientSeed>:<nonce>:<cursor>").
Der Schluessel ist der Server-Seed **als Zeichenkette**, nicht die aus dem
Hex dekodierten Bytes. Wer nachrechnet, fuegt den Seed genau so ein, wie
er offengelegt wurde.
"""
message = f"{client_seed}:{nonce}:{cursor}"
return hmac.new(server_seed.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).digest()
def get_float(server_seed: str, client_seed: str, nonce: int, cursor: int) -> float:
"""Gleitkommazahl in [0,1) aus den ersten vier Bytes.
b0/256 + b1/256^2 + b2/256^3 + b3/256^4
Vier Bytes, weil das Verfahren von Hand nachrechenbar bleiben soll.
"""
b = raw(server_seed, client_seed, nonce, cursor)
return b[0] / 256 + b[1] / 256**2 + b[2] / 256**3 + b[3] / 256**4
def _bytes_needed(spanne: int) -> int:
return max(1, ((spanne - 1).bit_length() + 7) // 8)
def get_int(spanne: int, server_seed: str, client_seed: str, nonce: int, cursor: int):
"""Ganzzahl in 0..spanne-1 ohne Modulo-Bias.
Es werden so viele Bytes gelesen, wie die Spanne braucht. Faellt der
Rohwert in den Rest, der sich nicht gleichmaessig aufteilen laesst, wird
er **verworfen** und mit erhoehtem Cursor neu gezogen. Genau dieses
Verwerfen macht das Ergebnis gleichverteilt.
Rueckgabe: (wert, naechster_cursor). Der Cursor wandert auch bei jedem
verworfenen Versuch weiter, damit ein Pruefer den Ablauf nachvollziehen
kann.
"""
if spanne < 1:
raise ValueError("Spanne muss mindestens 1 sein")
if spanne == 1:
return 0, cursor + 1
k = _bytes_needed(spanne)
topf = 256**k
grenze = topf - (topf % spanne)
while True:
wert = int.from_bytes(raw(server_seed, client_seed, nonce, cursor)[:k], "big")
cursor += 1
if wert < grenze:
return wert % spanne, cursor
def shuffle(anzahl: int, server_seed: str, client_seed: str, nonce: int, cursor: int = 0):
"""Fisher-Yates ueber 0..anzahl-1. Genau einmal je Runde (R8).
Rueckgabe: (reihenfolge, naechster_cursor).
"""
deck = list(range(anzahl))
for i in range(anzahl - 1, 0, -1):
j, cursor = get_int(i + 1, server_seed, client_seed, nonce, cursor)
deck[i], deck[j] = deck[j], deck[i]
return deck, cursor
def server_seed_hash(server_seed: str) -> str:
return hashlib.sha256(server_seed.encode("utf-8")).hexdigest()
# ---------------------------------------------------------------------------
SEED_A = "0000000000000000000000000000000000000000000000000000000000000000"
SEED_B = "a3f1c9d4e5b6072839a0b1c2d3e4f5061728394a5b6c7d8e9f0a1b2c3d4e5f60"
def vektoren():
out = {"hash": [], "raw": [], "float": [], "int": [], "shuffle": []}
for seed in (SEED_A, SEED_B):
out["hash"].append({"serverSeed": seed, "hash": server_seed_hash(seed)})
faelle = [
(SEED_A, "spieler-1", 0, 0),
(SEED_A, "spieler-1", 0, 1),
(SEED_A, "spieler-1", 1, 0),
(SEED_A, "", 0, 0),
(SEED_B, "MarcTestet", 42, 7),
(SEED_B, "a-b_c", 999999, 123),
]
for seed, client, nonce, cursor in faelle:
out["raw"].append({
"serverSeed": seed, "clientSeed": client, "nonce": nonce, "cursor": cursor,
"raw": raw(seed, client, nonce, cursor).hex(),
})
out["float"].append({
"serverSeed": seed, "clientSeed": client, "nonce": nonce, "cursor": cursor,
"value": repr(get_float(seed, client, nonce, cursor)),
})
for spanne in (2, 6, 37, 52, 200, 312, 10_000):
wert, folge = get_int(spanne, SEED_B, "MarcTestet", 42, 0)
out["int"].append({
"range": spanne, "serverSeed": SEED_B, "clientSeed": "MarcTestet",
"nonce": 42, "cursor": 0, "value": wert, "cursorNext": folge,
})
for anzahl in (5, 52, 312):
reihenfolge, folge = shuffle(anzahl, SEED_B, "MarcTestet", 42, 0)
out["shuffle"].append({
"count": anzahl, "serverSeed": SEED_B, "clientSeed": "MarcTestet", "nonce": 42,
"order": reihenfolge, "cursorNext": folge,
})
return out
def pruefe(pfad):
with open(pfad, encoding="utf-8") as fh:
eingaben = json.load(fh)
ergebnisse = []
for e in eingaben:
seed, client, nonce, cursor = e["serverSeed"], e["clientSeed"], e["nonce"], e["cursor"]
eintrag = {
"raw": raw(seed, client, nonce, cursor).hex(),
"float": repr(get_float(seed, client, nonce, cursor)),
}
if "range" in e:
wert, folge = get_int(e["range"], seed, client, nonce, cursor)
eintrag["int"] = wert
eintrag["cursorNext"] = folge
if "shuffleCount" in e:
reihenfolge, folge = shuffle(e["shuffleCount"], seed, client, nonce, cursor)
eintrag["shuffle"] = reihenfolge
eintrag["shuffleCursorNext"] = folge
ergebnisse.append(eintrag)
return ergebnisse
if __name__ == "__main__":
befehl = sys.argv[1] if len(sys.argv) > 1 else "vektoren"
if befehl == "vektoren":
print(json.dumps(vektoren(), indent=2))
elif befehl == "pruefe":
print(json.dumps(pruefe(sys.argv[2])))
else:
print(__doc__)
sys.exit(1)
Poker: Austeilen und Handbewertung
poker_referenz.py liegt neben der Datei oben und benutzt deren shuffle. Sie liest die
Karten an den Austeilpositionen ab und bewertet Hände – absichtlich anders gebaut als das Casino: Sie
probiert alle Fünferkombinationen durch.
#!/usr/bin/env python3
"""
Poker – unabhängige Zweitimplementierung (CLAUDE.md §8, Phase 11).
Mit diesem Programm rechnet jeder eine Pokerhand nach, ohne dem Casino zu
glauben: Stapel mischen (mit referenz.py), Karten an den öffentlichen
Positionen ablesen, Hände bewerten. Es ist absichtlich anders gebaut als
src/games/poker.ts: Es probiert alle Fünferkombinationen durch und
vergleicht Tupel, statt mit Bitmasken zu rechnen.
python3 poker_referenz.py austeilen <server_seed> <client_seed> <nonce> <spieler>
python3 poker_referenz.py bewerte <datei.json> # Liste von Kartenlisten
Karten sind Zahlen 0..51: Rang = Karte % 13 (0 = Ass, 9 = Zehn, 10..12 =
Bube, Dame, König), Farbe = Karte // 13 (♣ ♦ ♥ ♠). Beim Poker zählt das Ass
14, in der Straße A-2-3-4-5 als Eins.
"""
import itertools
import json
import os
import sys
from collections import Counter
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from referenz import shuffle # noqa: E402
KATEGORIEN = [
"high_card", "paar", "zwei_paare", "drilling", "strasse",
"flush", "full_house", "vierling", "straight_flush",
]
def rang(karte):
r = karte % 13
return 14 if r == 0 else r + 1
def strasse_hoch(raenge):
"""Höchste Karte einer Straße aus fünf verschiedenen Rängen, sonst 0."""
s = sorted(set(raenge))
if len(s) != 5:
return 0
if s[4] - s[0] == 4:
return s[4]
if s == [2, 3, 4, 5, 14]:
return 5
return 0
def fuenf(karten):
"""(Kategorie, fünf Ränge) einer Fünf-Karten-Hand."""
raenge = [rang(k) for k in karten]
farben = {k // 13 for k in karten}
flush = len(farben) == 1
hoch = strasse_hoch(raenge)
# Gruppen nach (Anzahl, Rang), größte zuerst
gruppen = sorted(Counter(raenge).items(), key=lambda kv: (kv[1], kv[0]), reverse=True)
anzahl = [n for _, n in gruppen]
werte = [r for r, _ in gruppen]
def auf5(liste):
return (list(liste) + [0, 0, 0, 0, 0])[:5]
if flush and hoch:
return 8, auf5([hoch])
if anzahl[0] == 4:
return 7, auf5(werte[:2])
if anzahl[0] == 3 and anzahl[1] == 2:
return 6, auf5(werte[:2])
if flush:
return 5, sorted(raenge, reverse=True)
if hoch:
return 4, auf5([hoch])
if anzahl[0] == 3:
return 3, auf5(werte[:3])
if anzahl[0] == 2 and anzahl[1] == 2:
return 2, auf5(werte[:3])
if anzahl[0] == 2:
return 1, auf5(werte[:4])
return 0, sorted(raenge, reverse=True)
def bewerte(karten):
"""Beste Hand aus fünf bis sieben Karten: [Kategorie, Ränge]."""
if not 5 <= len(karten) <= 7 or len(set(karten)) != len(karten):
raise ValueError("5 bis 7 verschiedene Karten")
beste = max(fuenf(c) for c in itertools.combinations(karten, 5))
return [KATEGORIEN[beste[0]], beste[1]]
def positionen(n):
"""Austeilen ab links vom Button, zweimal herum; je eine Karte verbrannt vor Flop, Turn, River."""
return {
"loch": [[i, n + i] for i in range(n)],
"flop": [2 * n + 1, 2 * n + 2, 2 * n + 3],
"turn": 2 * n + 5,
"river": 2 * n + 7,
}
def austeilen(server_seed, client_seed, nonce, n):
deck, _ = shuffle(52, server_seed, client_seed, nonce, 0)
p = positionen(n)
return {
"loch": [[deck[a], deck[b]] for a, b in p["loch"]],
"board": [deck[x] for x in p["flop"] + [p["turn"], p["river"]]],
}
def main(argv):
if len(argv) >= 2 and argv[1] == "bewerte":
with open(argv[2], encoding="utf-8") as f:
haende = json.load(f)
print(json.dumps([bewerte(h) for h in haende]))
return 0
if len(argv) == 6 and argv[1] == "austeilen":
print(json.dumps(austeilen(argv[2], argv[3], int(argv[4]), int(argv[5]))))
return 0
print(__doc__)
return 2
if __name__ == "__main__":
sys.exit(main(sys.argv))
Die Basisstrategie, gegen die der Blackjack-Hausvorteil gemessen wurde
Der ausgewiesene Hausvorteil von Blackjack ist kein gerechneter, sondern
ein gemessener Wert – und gemessen wurde gegen genau
diese Tabelle. Wer schlechter spielt, zahlt mehr; besser geht bei einem
Schuh, der vor jeder Runde neu gemischt wird, nicht.
/**
* Basisstrategie fuer das Regelwerk aus §6: 6 Decks, Dealer zieht Soft 17,
* Double nach Split erlaubt, kein Surrender.
*
* **Wozu das hier steht:** §6 sagt, der verbindliche Hausvorteil sei das
* Ergebnis der Simulation aus §8. Eine Simulation braucht aber einen Spieler,
* und wie gut der spielt, entscheidet die Zahl. Gemessen wird deshalb gegen
* **optimales Spiel ohne Kartenzaehlen** – die Basisstrategie. Wer schlechter
* spielt, zahlt mehr; besser geht bei einem Schuh, der vor jeder Runde neu
* gemischt wird, nicht.
*
* Die Tabelle gehoert damit zur ausgewiesenen Zahl und steht auf der
* Transparenzseite. Eine Prozentzahl ohne die Strategie, gegen die sie
* gemessen wurde, ist keine nachpruefbare Angabe.
*
* Die 6:5-Auszahlung kommt in der Tabelle **nicht** vor, und das ist richtig
* so: Sie aendert die Hoehe des Blackjack-Gewinns, nie die beste Entscheidung
* an einer Hand. Sie schlaegt voll auf den Hausvorteil durch, ohne das Spiel
* zu veraendern – deshalb ist sie die teuerste Regel des Tisches.
*/
import {
handWert,
kartenwert,
type Aktion,
type BlackjackRules,
BLACKJACK_RULES,
} from './blackjack.ts'
export interface Lage {
readonly karten: readonly number[]
/** Zaehlwert der offenen Dealerkarte: 2 bis 11. */
readonly dealer: number
readonly darfVerdoppeln: boolean
readonly darfTeilen: boolean
}
/** Dealerkarten, gegen die gesplittet wird – kompakt als Liste. */
const SPLIT_GEGEN: Record<number, readonly number[]> = {
11: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // Asse immer
10: [], // Zehner nie
9: [2, 3, 4, 5, 6, 8, 9], // nicht gegen 7, 10, Ass
8: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // Achter immer
7: [2, 3, 4, 5, 6, 7],
6: [2, 3, 4, 5, 6],
5: [], // nie – 5,5 wird als harte 10 gespielt
4: [5, 6], // nur mit Double nach Split sinnvoll
3: [2, 3, 4, 5, 6, 7],
2: [2, 3, 4, 5, 6, 7],
}
/** Weiche Haende: bis zu welcher Dealerkarte verdoppelt wird. */
const SOFT_DOUBLE_GEGEN: Record<number, readonly number[]> = {
13: [5, 6],
14: [5, 6],
15: [4, 5, 6],
16: [4, 5, 6],
17: [3, 4, 5, 6],
18: [2, 3, 4, 5, 6],
19: [6], // nur weil der Dealer Soft 17 zieht
}
const HART_DOUBLE_GEGEN: Record<number, readonly number[]> = {
9: [3, 4, 5, 6],
10: [2, 3, 4, 5, 6, 7, 8, 9],
11: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // gegen alles, auch das Ass (Soft 17)
}
/**
* Die Entscheidung.
*
* Reihenfolge wie am Tisch: erst Paare, dann weiche, dann harte Haende. Eine
* andere Reihenfolge waere nicht bloss unuebersichtlich, sondern falsch –
* A,A ist ein Paar **und** eine weiche Hand, und gespielt wird es als Paar.
*/
export function basisstrategie(
lage: Lage,
rules: BlackjackRules = BLACKJACK_RULES,
): Aktion {
const { karten, dealer } = lage
const wert = handWert(karten)
// --- Paare ---------------------------------------------------------------
if (lage.darfTeilen && karten.length === 2) {
const a = kartenwert(karten[0]!)
const b = kartenwert(karten[1]!)
if (a === b) {
const gegen = SPLIT_GEGEN[a]
if (gegen !== undefined && gegen.includes(dealer)) return 'teilen'
// 4,4 nur mit Double nach Split; ohne DAS wird gezogen.
if (a === 4 && !rules.doubleNachSplit) return 'ziehen'
}
}
// --- Weiche Haende -------------------------------------------------------
if (wert.soft && wert.summe >= 13 && wert.summe <= 19) {
const doppeln = SOFT_DOUBLE_GEGEN[wert.summe]
if (lage.darfVerdoppeln && doppeln !== undefined && doppeln.includes(dealer)) {
return 'verdoppeln'
}
// Ohne Double: Soft 18 steht gegen 2 bis 8, Soft 19 und 20 immer.
if (wert.summe === 18) return dealer <= 8 ? 'stehen' : 'ziehen'
if (wert.summe >= 19) return 'stehen'
return 'ziehen'
}
if (wert.soft && wert.summe >= 20) return 'stehen'
// --- Harte Haende --------------------------------------------------------
if (lage.darfVerdoppeln) {
const doppeln = HART_DOUBLE_GEGEN[wert.summe]
if (doppeln !== undefined && doppeln.includes(dealer)) return 'verdoppeln'
}
if (wert.summe >= 17) return 'stehen'
if (wert.summe >= 13) return dealer <= 6 ? 'stehen' : 'ziehen'
if (wert.summe === 12) return dealer >= 4 && dealer <= 6 ? 'stehen' : 'ziehen'
return 'ziehen'
}
/** Die Dealerkarte, gegen die entschieden wird – die **offene**. */
export function dealerZeigt(dealer: readonly number[]): number {
const karte = dealer[0]
if (karte === undefined) throw new Error('Der Dealer hat keine offene Karte.')
return kartenwert(karte)
}
/** Ob eine Hand als Paar gilt – gleicher Zaehlwert, nicht gleicher Rang. */
export function istPaar(karten: readonly number[]): boolean {
return karten.length === 2 && kartenwert(karten[0]!) === kartenwert(karten[1]!)
}