greenePoker
Strategie v6
Vollständige Revision nach I1–I16. Alle externen Quellen eingearbeitet. Einzige Kontextquelle für I17+.
0Plan-Revisionen v5 → v6
0.1 Anlass
v5 wurde am 2026-05-08 nach Abschluss von I6 erstellt und nicht weitergepflegt. Der reale Backend-Stand am 2026-05-21 liegt bei I16 (Phase C funktional vollständig für Single-Table; 341 Tests grün). Die Diskrepanz zwischen Plan-Stand und Realstand machte v5 als Steuerungsdokument unbrauchbar.
0.2 Inhaltliche Änderungen gegenüber v5
| Bereich | v5 | v6 |
|---|---|---|
| Iterations-Status | I1–I6 abgeschlossen | I1–I16 abgeschlossen |
| Phasen-Reihenfolge | A → C → B → D → E (D4) | C[I17–I19] → B Port → C/D verschränkt (D24) |
| Rebuy-Mechanik | rebuy_mode ∈ {none, 1+addon} (Skizze) | 3 Modi: freeze-out / classic / re-entry (D23) |
| Übergeordneter Rahmen | Verweis auf externe Datei | Vollständig in Abschnitt 1 eingearbeitet |
| SA-Risiken | Verweis auf daten_schema-analyse_1.html | Vollständig in Abschnitt 9 ausformuliert |
| Session-Mechanismus | Verweis auf externe Datei | In Abschnitt 1.4 eingearbeitet |
| Hand-History-Schema | Verweis auf handhistoryStrategie.html | In Abschnitt 1.5 eingearbeitet |
| Stack-Begründung | Verweis auf externe Datei | In Abschnitt 1.2 eingearbeitet |
| Übergabe-Logs | Verweis auf backend/README.md | Als Anhang E eingearbeitet |
| HTML-Stand | v45.72 / v44.22 | v45.9 / v44.26 |
0.3 Neue Entscheidungen
- D23 — Rebuy-Mechanik: Wizard-Auswahl zwischen
freeze-out(keine Rebuys),classic(Rebuy-Phase + Addon) undre-entry(Bust-Out-Re-Entry im Fenster). - D24 — Phasen-Reihenfolge: Reihenfolge C ersetzt v5/D4. Begründet in
Reviews/einschaetzung_reactMigration_vor_backend_2026-05-21.md. - D25 — Lobby-Anzeigeumfang: Turnier-Lobby zeigt angemeldete Spieler mit Live-Stacks; Auszahlungsstruktur schematisch + konkret; Standard-Buttons modus-abhängig.
0.4 Beibehaltene Entscheidungen
D1 bis D22 aus v5 bleiben unverändert verbindlich (siehe Abschnitt 2).
1Übergeordneter Rahmen
concurrent-tinkering-thompson.md und ergänzt sie um die seitdem getroffenen Stack-, Session- und Schema-Entscheidungen.
1.1 Projekt-Vision und Phasen-Modell
Projekt: greenPoker — webbasiertes Texas-Hold'em-Spielgeld-Pokerportal für einen privaten Club. Single-Node-Deployment, ein zentraler Node-Prozess, mehrere gleichzeitige Tische, Echtzeit-Sync via Socket.IO.
Ziel-Funktionsumfang:
- Turniere (Single-Table und Multi-Table) mit Wizard, Anmeldung, Sitzzuteilung, Blind-Levels, Bounties, Payouts, Series-Punkte, Wiederholungen
- Cashgames (Phase D) — eigenständige Domäne mit Buy-In/Stand-Up, Top-Up-Logik
- Server-autoritative Hold'em-Logik: Deck-Shuffle aus
crypto.randomBytes, Hand-Evaluation, Pot/Side-Pot-Verteilung, Action-Validierung - Hand-History, Replayer mit Sichtbarkeitsfilter
- HUD-Stats (VPIP, PFR, 3-Bet, Aggression Factor)
- Chat (Club + Tisch), Moderation, Notifications, Inbox, Club-Board, Bans, Admin-Backoffice
- React-basiertes Frontend (Phase B) nach Konsolidierung Phase C
Phasen-Modell (aktualisiert durch D24):
Phase A — HTML-Politur ✅ abgeschlossen (Stand v45.9 / v44.26)
Phase C — Backend mit HTML-Anbindung ✅ I1–I16; I17–I19 ausstehend (D24)
Phase B — React-Port ausstehend (nach I19, D24)
Phase D — Cashgame-Engine ausstehend (verschränkt mit Phase B, D24)
Phase E — Moderator-Rolle, E-Mail,
optional PostgreSQL ausstehend (Backlog)
Multiplayer-Scope (verbindlich, unverändert seit v1):
- Ein Node-Prozess, ein SQLite-Writer
- Mehrere gleichzeitige Tische, mehrere User pro Tisch
- Socket.IO mit Rooms pro Tisch (
table:<id>), pro User (user:<id>), pro Turnier (tournament:<id>), pro Club (club:global) - Kein horizontales Scaling, kein Multi-Region-Setup, keine externe Replikation
- Reversibilitätspfad nach Postgres (Phase E) offen gehalten, aber nicht aktiv geplant
1.2 Tech-Stack — verbindliche Festlegung
| Layer | Wahl | Begründung |
|---|---|---|
| Sprache | TypeScript (strict) | Einheit von Frontend und Backend; typsichere Schnittstellen |
| Runtime | Node.js | Synchrone Event-Loop passt zu Socket.IO |
| Web-Framework | Express | Minimal-Framework, geringer Lock-in |
| Realtime | Socket.IO | Wiederverbindungsstrategie, Rooms, Browser-Kompatibilität |
| Persistenz | SQLite (data/greenpoker.db) | Viele kleine Writes aus genau einem Writer-Prozess. Kein separater DB-Prozess. |
| SQLite-Binding | better-sqlite3 | Synchron, nativer Treiber, kein async-Dispatch-Overhead |
| ORM | Drizzle ORM + drizzle-kit | Typisierte Queries, Migrations als SQL-Files |
| Journal-Modus | WAL (journal_mode=WAL) | Concurrent Reads neben einem Writer |
| Foreign Keys | PRAGMA foreign_keys=ON | Erzwingt FK-Constraints; in SQLite default off |
| Busy-Timeout | busy_timeout=5000 | Defensiv für gelegentliche Read-Lock-Konkurrenz |
| Auth-Hashing | bcrypt | Verbreitet, geprüft |
| Validierung | zod | Eingangs-Validierung von API-Bodies und JSON-Blobs (SA-3) |
| Tests | vitest | TypeScript-nativ, schnell; In-Memory-SQLite-Tests möglich |
Verworfene Alternativen
| Alternative | Verworfen, weil |
|---|---|
| Postgres | Workload-Fit-Nachteil bei vielen kleinen Writes; zusätzliche Betriebskomplexität. Reversibilitätspfad bleibt offen. |
| Prisma | Eigene Engine-IPC, höhere Latenz, weniger Kontrolle |
| libSQL / Turso | Zusätzliche Abstraktion ohne Replikationsziel |
| JWT-Sessions | Siehe 1.4 — macht strukturellen Vorteil zunichte |
| MongoDB | Für stark relationales Domänenmodell unpassend |
1.3 Backup-Strategie
db.backup('backup/greenpoker-YYYY-MM-DD.db')via better-sqlite3 Online-Backup — WAL-sicher, kein Server-Stop nötig- Worker (
workers/backup.worker.ts, I14-Restpunkt, noch nicht implementiert) ruft Backup periodisch auf (BACKUP_INTERVAL_HOURS = 24) - Aufbewahrung:
BACKUP_KEEP_DAYS = 7rollende Backups, ältere werden gelöscht - Pfad
backend/data/backups/(gitignored) - Backup ist Pflicht-Voraussetzung vor jedem Migrations-Lauf in Produktion
1.4 Session-Mechanismus — Server-Session-ID (Variante A)
httpOnly-Cookie. JWT ist verworfen.Funktionsweise:
- Login: Server prüft E-Mail + Passwort, erzeugt 32-Byte-Zufalls-ID (
crypto.randomBytes(32).toString('hex')), speichert in Tabellesessions, setzt Cookiesession_idmithttpOnly; Secure; SameSite=Lax - Folge-Request: Cookie automatisch gesendet, Middleware liest und schlägt in
sessionsnach (Prepared Statement), hängtreq.useran - Logout:
DELETE FROM sessions WHERE id = ?plus Cookie-Clear - „Alle Geräte abmelden":
DELETE FROM sessions WHERE user_id = ? - Cleanup-Worker löscht abgelaufene Sessions alle 60 Min
Warum nicht JWT: Single-Node-Scope macht den einzigen JWT-Vorteil (zustandslose Multi-Node-Validierung) gegenstandslos. Widerruf bei Ban/Reset ist mit Server-Session-ID eine DELETE-Query. DB-Lookup kostet bei WAL-SQLite + Prepared Statement einstellige Mikrosekunden.
SESSION_LIFETIME_DAYS = 30 mit Refresh bei jedem Request via last_seen.
1.5 Hand-History-Schema — Variante 2 (mit hand_board_cards)
hand_board_cards und erweiterten Stats-Flags pro Spieler.Strukturelle Bestandteile:
hands— Kopfdaten pro Hand (Tisch, Turnier, Blind-Beträge, Dealer-Sitz,boardals kompakter String, Pot-Total, Rake)hand_board_cards— pro Karte am Board eine Zeile (street ∈ {flop,turn,river},position 0/1/2,rank 2..14,suit ∈ {h,d,c,s}). Erlaubt analytische Queries auf Board-Texturen.hand_players— pro Spieler einer Hand: Sitz, Start-Stack, Hole-Cards (verschlüsselt),shown_at_showdown, gewonnener Betrag, sowie Stats-Flagsvpip/pfr/three_bet/agg_factor_num/denactions— pro Action eine Zeile (seq,street,action ∈ {fold,check,call,bet,raise,allin},amount,at)
Stats-Definition (D17b, PokerTracker-4-Standard): VPIP = freiwillige Geldzugabe preflop außer Blind-Posten; PFR = erste freiwillige Raise preflop; 3-Bet = erste Re-Raise preflop; AF = (Bet+Raise)/Call mit Numerator und Denominator separat gespeichert.
1.6 Zentrale Spielregel-Konstanten (D17c)
| Konstante | Wert | Bedeutung |
|---|---|---|
TIME_PER_ACTION_DEFAULT_SEC | 30 | Bedenkzeit pro Aktion |
TIME_BANK_INITIAL_MS | 90 000 | Time-Bank-Startwert pro Spieler |
TIME_BANK_REFILL_PER_HAND_MS | 5 000 | Auffüllbetrag pro abgeschlossener Hand |
TIME_BANK_MAX_MS | 90 000 | Obergrenze für Time-Bank-Auffüllung |
LATE_REG_DEFAULT_MIN | 30 | Late-Registration-Fenster ab Turnierstart |
MULTI_TABLE_LIMIT | 4 | Maximalzahl simultaner Tische pro Spieler |
DEFAULT_SEAT_COUNT | 8 | Sitze pro Tisch |
DEFAULT_START_STACK | 10 000 | Startstack bei Turnierstart |
MIN_TOURNAMENT_PLAYERS | 2 | Mindest-Spielerzahl für Turnier-Start |
MAX_TOURNAMENT_PLAYERS | 8 | Maximal-Spielerzahl pro Turnier (Single-Table) |
Vollständige Konstanten-Liste siehe Anhang C.
2Decision Log
Verbindliche Entscheidungen. Spätere Abweichungen müssen explizit als Plan-Revision dokumentiert werden. D23, D24, D25 sind neu in v6.
| Nr | Thema | Entscheidung | Konsequenz |
|---|---|---|---|
| D1 | Geld-Modell | Reines Spielgeld | Kein Payment-Gateway, keine KYC |
| D2 | Rollen | Nur admin und player | Moderator-Rolle ist Phase E |
| D3 | Tisch-Erstellung | Vollfreigabe für alle Spieler | Nur Bounty-Clubkonto bleibt admin-only |
| D4 | Tournament/Cashgame | Tournament zuerst, Cashgame in Phase D | Phase C liefert Tournament-Engine |
| D5 | Multi-Table-Balancing | I17 | TournamentBalancer als abgrenzbares Modul |
| D6 | Hand-History-Schema | Variante 2 + erweiterte Flags | Separate hand_players-Tabelle mit Stats-Flags |
| D7 | Replayer-Sichtbarkeit | Alle Hände am Tisch während Anwesenheit; eigene gemuckte Karten sichtbar | Filter via seat_sessions |
| D8 | Multi-Table & Time-Bank | Limit 4 Tische; TB kumuliert mit Auffüllung; Auto-Sit-Out bei Ablauf | Pro tournament_players-Zeile aktueller TB-Stand |
| D9 | Tournament-Wiederholung | Hybrid (Schedule + 4-Wochen-Window), In-Process-Scheduler | tournament_schedules + Worker-Loop |
| D10 | User-Preferences | Serverseitig pro User | Tabelle user_preferences |
| D11 | Notifikations-Transport | Socket.IO Rooms + REST | Module emittieren in eigene Rooms |
| D12 | Client Phase C | Bestehendes HTML wird angepasst | Mock-Arrays werden durch API-Calls ersetzt |
| D13 | SRP-Granularität | Lesart 3: Module nach Sachgebiet | Iterativ verfeinert |
| D14 | Iterations-Größe | Klein, jede Iteration lauffähig | Schema-Änderungen und Spiellogik nie in derselben Session |
| D15 | Buchungssystem | Hybrid (doppelte Buchung + Saldo-Cache); Buchungen final | Stornierung nur per Gegenbuchung |
| D16 | Bans | 3 Stufen (Verwarnung/Spielsperre/Vollsperre), alle befristet | bans.expires_at zwingend |
| D17a | Series-Punkte | PokerStars-Formel Buyin × √Spielerzahl × 1/(Platz+1) | In tournament_players.series_points persistiert |
| D17b | Stats-Definition | PokerTracker-4-Standard | VPIP zählt nicht BB-Post; PFR erste freiwillige Raise |
| D17c | Zeitwerte | Bedenkzeit 30 s; TB 90 s + 5 s/Hand bis 90 s; Late-Reg 30 min | Defaults als Konstanten in config.ts |
| D18 | Login-Identifier | E-Mail-Adresse, lowercase normalisiert | username entfällt; Doppelt-Eingabe im Registrierungsformular |
| D19 | Anzeigename | Pflicht, eindeutig (COLLATE NOCASE), im Profil mit 30-Tage-Cooldown änderbar | Überall statt username |
| D20 | Game-State-Persistenz | Memory-First für seats; DB-Tabelle nur als Snapshot | Reduziert Write-Amplifikation (SA-5) |
| D21 | Hand-Board-Speicherung | Hybrid: hands.board als String + Tabelle hand_board_cards | Synchron in einer Transaktion |
| D22 | Sitzzuteilung Turniere | Automatisch beim Turnierstart (Fisher-Yates); manuelles Sitzen blockiert | table.join mit seatIndex gibt 400 für Tournament-Tische |
| D23 | Rebuy-Mechanik neu | Drei Modi im Wizard: freeze-out, classic (Rebuy-Phase + Addon), re-entry (Bust-Out-Re-Entry im Fenster) | Schema-Erweiterung Migration 0011; Lifecycle-Erweiterung; UI-Anpassung in Wizard und Lobby |
| D24 | Phasen-Reihenfolge neu | C-Restpunkte (I17–I19) in HTML → React-Port Phase 1 → Lobby-Strukturarbeit in React verschränkt mit Backend-Anpassungen → Cashgame (Phase D) parallel Backend+React | D4 bleibt sachlich gültig; Reihenfolge auf Iterations-Ebene verfeinert. Begründung in Reviews/einschaetzung_reactMigration_vor_backend_2026-05-21.md |
| D25 | Lobby-Anzeigeumfang neu | Turnier-Lobby zeigt angemeldete Spieler in Liste; Sortierung bei running nach Stack-Size absteigend; Auszahlungsstruktur schematisch (Prozente) UND konkret (Chip-Beträge bei aktuellem Prize Pool); Standard-Buttons modus-abhängig | API-Erweiterung in I19; lobby.update Socket-Event; Stack-Live-Werte aus SeatStateMemory |
3Scope und Abgrenzung
3.1 In Scope (gesamtes Projekt)
Backend (Node + TypeScript + Express + Socket.IO + better-sqlite3 + Drizzle ORM):
- Authentifizierung mit E-Mail-Login (D18), Sessions (1.4), Registrierungs-Workflow mit Admin-Genehmigung
- User-Profile mit
display_name(D19), Cooldown-Endpoint, serverseitige Preferences (D10) - Buchungssystem (doppelte Buchführung) mit
ConsistencyCheckService(SA-1/SA-2), zod-Blob-Validation (SA-3), CHECK-Constraints (SA-4) - Tournament-Engine: Wizard, Registration, Sitzzuteilung (D22), Schedules (D9), Lifecycle, Blind-Levels, Bounty, Payout, Single-Table und Multi-Table mit Balancing (I17)
- Rebuy-Modi (D23): freeze-out / classic / re-entry
- Lobby-Anzeigeumfang (D25): Spielerliste mit Live-Stacks, Auszahlungsstruktur
- Server-autoritative Hold'em-Logik, Hand-History und Replayer (Variante 2, D21), HUD-Stats
- Chat (Club + Tisch), Moderation, Club Board, Notifications/Inbox, Bans
- Echtzeit-Sync via Socket.IO Rooms
- Cashgame-Engine (Phase D): Anforderungs-Spezifikation steht aus
Frontend: Phase A/C: HTML inkrementell durch API-Calls angebunden. Phase B: vollständige React-Port-Phase.
3.2 Nicht in Scope
- Echte Payment-Anbindung, KYC, E-Mail-Versand (SMTP)
- Moderator-Rolle (Phase E, D2)
- PostgreSQL-Migration (Reversibilitäts-Pfad, nicht aktiv geplant)
- Multi-Node-Skalierung, Multi-Region, Mobile native Apps
3.3 Aktualisierte Phasen-Folge (gemäß D24)
Phase A — HTML-Politur ✅ abgeschlossen
↓
Phase C — Backend mit HTML-Anbindung
I1–I16 (Auth bis HUD/Stats) ✅ abgeschlossen
I17 — Multi-Table-Balancer (in HTML) ausstehend
I18 — Rebuy-Vollausbau (in HTML) ausstehend
I19 — Turnier-Lobby-Anzeigeumfang (in HTML) ausstehend
↓
Phase B — React-Port
B1 — Stack-Festlegung und Plattform-Setup ausstehend
B2 — 1:1-Migration der stabilen Domänen ausstehend
↓
Phase B / D verschränkt
B3/D1 — Cashgame-Backend + React-Cashgame-Komponenten ausstehend
B4 — Lobby- und UI-Strukturarbeiten in React ausstehend
↓
Phase E — Moderator-Rolle, E-Mail-Versand, optional PostgreSQL Backlog
4Architektur-Übersicht
4.1 Schichten-Modell
Client (Browser)
HTML + JS (fetch + socket.io-client) — heute
React + TS (Phase B) — geplant
↓
Transport-Schicht
Express HTTP REST (/api/*) + Socket.IO (Rooms: table:/user:/tournament:/club:)
↓
Anwendungs-Schicht (Sachgebiet-Module)
Auth · User · Ban · Accounting · Tournament · Game · HandHistory · Chat · ClubBoard · Notification · Stats · Admin
↓
Querschnitt
RealtimeBus · Logger · Migrator · BackupWorker · ScheduleWorker · CleanupWorker · ConsistencyCheckService · BlobValidator · PolymorphicResolver · Clock
↓
Persistenz-Schicht
Drizzle ORM → better-sqlite3 (synchron, WAL) → greenpoker.db
Modul-Prinzipien (D13, Lesart 3): Ein Modul entspricht einem Sachgebiet. Innerhalb eines Moduls werden Klassen nach SRP gesplittet. Module kommunizieren über Direkt-Injektion in server.ts, Realtime-Bus für asynchrone Events oder Callbacks.
4.2 Memory-First-Strategie für Spielzustand (D20)
SeatStateMemory hält In-Memory-Map Map<tableId, Map<seatIndex, SeatState>>. DB-Tabelle seats ist Snapshot-Speicher, kein Action-Log.
| Ereignis | Aktion |
|---|---|
| Tournament-Start (D22) | Memory-Init + DB-Update für alle Sitze (Fisher-Yates) |
table.leave (Stand-Up) | Memory-Delete + DB-Update user_id=NULL + seat_sessions.stood_up_at |
| Hand-Ende | Snapshot-Write: UPDATE seats SET stack=? für alle besetzten Sitze |
| Server-Start | Bootstrap-Recovery aus seats-Tabelle |
| Rebuy/Re-Entry (I18) | Memory-Update Stack + DB-Update (gleiche Transaktion wie Buchung) |
4.3 Datenfluss einer Spieler-Action
Browser: socket.emit('action', { handId, type:'raise', amount: 200 })
→ game.gateway: validiert Inhaber, prüft am-Zug, prüft Stack-Decken
→ action.validator: Min-Raise, Stack-Limit
→ hand.engine: nimmt State, gibt neuen State zurück (pure)
→ hand.repository: persist action in `actions`
→ SeatStateMemory: stack-Adjustierung
→ bei Hand-Ende: snapshot in `seats` + `hand_players.won_amount`
→ realtime.bus.emit('state-update', tableId, newState)
→ io.to(`table:${tableId}`).emit('state-update', newState)
→ Browser empfängt und rendert
5Datenmodell
5.1 Migrationsreihenfolge
| # | Iteration | Tabellen |
|---|---|---|
| 0001 | I1 | users, sessions |
| 0002 | I2 | bans |
| 0003 | I3 | accounts, transactions, ledger_entries |
| 0004 | I4 | user_preferences |
| 0005 | I5 | tables, seats, seat_sessions |
| 0006 | I6 | tournaments, tournament_players, blind_levels |
| 0006b | I7 | tournament_schedules, tournament_series |
| 0007 | I9 | hands, hand_board_cards, hand_players, actions |
| 0008 | I12 | chat_messages, word_filters, mutes, chat_room_settings |
| 0009 | I13 | inbox_items, popup_log |
| 0010 | I14 | club_board |
| 0011 | I18 (geplant) | Rebuy-Erweiterung: tournaments.rebuy_mode, rebuy_phase_min, rebuy_cost, addon_cost, addon_chips, rebuy_chips, re_entry_window_min, tournament_players.re_entry_count |
5.2 Vollständiges Schema (Stand I16)
Identität & Zugang
users
├── id INTEGER PK, email TEXT UNIQUE, display_name TEXT (UNIQUE NOCASE)
├── display_name_changed_at INTEGER NULL
├── password_hash TEXT, role ENUM('admin','player') default 'player'
├── status ENUM('pending','active','banned') default 'pending'
├── avatar_path TEXT NULL, created_at INTEGER, last_seen_at INTEGER NULL
sessions
├── id TEXT PK (64 hex), user_id FK → users(id) ON DELETE CASCADE
├── created_at INTEGER, expires_at INTEGER, last_seen INTEGER, user_agent TEXT NULL
bans
├── id INTEGER PK, user_id FK → users ON DELETE CASCADE
├── level ENUM('warning','play_block','full'), reason TEXT NULL
├── issued_by FK → users, issued_at INTEGER, expires_at INTEGER
Buchungen (doppelte Buchführung, D15)
accounts
├── id INTEGER PK, owner_type ENUM('user','club'), owner_id INTEGER NULL
├── name TEXT, balance_cache INTEGER default 0, currency TEXT default 'chips', created_at INTEGER
transactions
├── id INTEGER PK, ts INTEGER
├── from_account FK → accounts, to_account FK → accounts
├── amount INTEGER (> 0 per CHECK)
├── ref_type ENUM('buyin','rake','bounty','transfer','admin','donation','reverse')
├── ref_id INTEGER NULL, note TEXT NULL
├── created_by FK → users, reversed_by INTEGER NULL
ledger_entries
├── id INTEGER PK, transaction_id FK → transactions
├── account_id FK → accounts, amount INTEGER (signed), ts INTEGER
Tische & Turniere
tables
├── id INTEGER PK, name TEXT, kind ENUM('tournament','cashgame') default 'tournament'
├── tournament_id INTEGER NULL, seat_count INTEGER default 8
├── status ENUM('open','paused','breaking','closed') default 'open'
├── created_at INTEGER, created_by FK → users NULL, closed_at INTEGER NULL
seats (Memory-First-Snapshot, D20)
├── id INTEGER PK, table_id FK → tables ON DELETE CASCADE
├── seat_index INTEGER, user_id FK → users ON DELETE SET NULL
├── stack INTEGER default 0, is_dealer INTEGER default 0
├── is_sb INTEGER, is_bb INTEGER, is_sit_out INTEGER, time_bank INTEGER default 90000
├── snapshot_at INTEGER NULL
seat_sessions
├── id INTEGER PK, table_id FK, user_id FK
├── sat_down_at INTEGER, stood_up_at INTEGER NULL
tournament_series
├── id INTEGER PK, title TEXT, description TEXT NULL
├── created_by FK → users, created_at INTEGER
tournament_schedules
├── id INTEGER PK, title TEXT
├── recurrence_json TEXT (zod-validiert), template_json TEXT (zod-validiert)
├── series_id FK → tournament_series NULL, is_active INTEGER default 1
├── created_by FK → users, created_at INTEGER
tournaments
├── id INTEGER PK, title TEXT
├── status ENUM('registering','running','paused','finished','cancelled')
├── table_id FK → tables NOT NULL (D22)
├── start_at INTEGER, buyin INTEGER, rake_pct INTEGER
├── speed ENUM('slow','normal','turbo','hyper') default 'normal'
├── payout_curve ENUM('steep','standard','flat') default 'standard'
├── rebuy_mode ENUM('none','1+addon') → I18: ENUM('freeze-out','classic','re-entry')
├── max_players INTEGER default 8, prize_pool INTEGER default 0
├── created_by FK → users, created_at INTEGER
├── schedule_id FK, series_id FK
tournament_players
├── id INTEGER PK, tournament_id FK, user_id FK
├── registered_at INTEGER, seated_at INTEGER NULL
├── final_place INTEGER NULL, prize_won INTEGER default 0
├── series_points INTEGER default 0
├── rebuy_count INTEGER default 0, addon_used INTEGER default 0
├── (bei I18) re_entry_count INTEGER default 0
blind_levels
├── id INTEGER PK, tournament_id FK ON DELETE CASCADE
├── level INTEGER, sb INTEGER, bb INTEGER, ante INTEGER default 0, duration_min INTEGER
Hand-History (D6, D21)
hands
├── id INTEGER PK, table_id FK, tournament_id FK NULL
├── started_at INTEGER, ended_at INTEGER NULL
├── sb_amount INTEGER, bb_amount INTEGER, ante INTEGER default 0
├── dealer_seat INTEGER, board TEXT NULL
├── pot_total INTEGER NULL, rake INTEGER default 0
hand_board_cards
├── id INTEGER PK, hand_id FK ON DELETE CASCADE
├── street ENUM('flop','turn','river'), position INTEGER
├── rank INTEGER (2..14), suit TEXT (h|d|c|s)
hand_players
├── id INTEGER PK, hand_id FK, user_id FK
├── seat INTEGER, stack_start INTEGER, hole_cards TEXT NULL
├── shown_at_showdown INTEGER default 0, won_amount INTEGER default 0
├── vpip, pfr, three_bet, agg_factor_num, agg_factor_den (INTEGER default 0)
actions
├── id INTEGER PK, hand_id FK, user_id FK
├── seq INTEGER, street TEXT
├── action ENUM('fold','check','call','bet','raise','allin'), amount INTEGER default 0
├── at INTEGER
Chat, Board, Notifications
chat_messages
├── id INTEGER PK, room_kind ENUM('table','club'), room_ref INTEGER NULL
├── user_id FK, text TEXT, at INTEGER
├── pinned INTEGER default 0, deleted_at INTEGER NULL, edited_at INTEGER NULL
word_filters · mutes · chat_room_settings (slow_mode_sec, updated_at)
inbox_items
├── id INTEGER PK, user_id FK, kind TEXT, title TEXT, body TEXT
├── ref_type TEXT NULL, ref_id INTEGER NULL
├── read_at INTEGER NULL, created_at INTEGER, action_required INTEGER default 0
popup_log
├── id INTEGER PK, sent_by FK, target_type ENUM('all','user'), target_id INTEGER NULL
├── title TEXT, body TEXT, sent_at INTEGER
club_board
├── id INTEGER PK, text TEXT, author_id FK
├── created_at INTEGER, edited_at INTEGER NULL, deleted_at INTEGER NULL
6Programmstruktur
6.1 Verzeichnisstruktur (Stand I16)
backend/ ├── data/ (gitignored) │ ├── greenpoker.db │ ├── avatars/ │ └── backups/ ├── drizzle/ (SQL-Migrationen 0001–0010) ├── scripts/seed.ts ├── src/ │ ├── config.ts (zentrale Konstanten, D17c) │ ├── types.ts (AuthenticatedUser, Express-Augmentation) │ ├── server.ts (Bootstrap, DI-Wurzel) │ ├── db/ │ │ ├── client.ts (PRAGMA-Setup, Drizzle-Wrapper) │ │ ├── schema.ts │ │ └── migrator.ts │ ├── core/ │ │ ├── logger.ts · errors.ts · time.ts · ids.ts │ │ ├── realtime.ts (RealtimeBus) │ │ ├── consistency-check.service.ts │ │ └── polymorphic-resolver.ts │ ├── validation/ │ │ ├── prefs.schemas.ts · auth.schemas.ts │ │ ├── accounting.schemas.ts · schedule.schemas.ts │ │ └── blob-versioning.ts (SA-3) │ ├── modules/ │ │ ├── auth/ ✅ I1 │ │ ├── ban/ ✅ I2 │ │ ├── accounting/ ✅ I3 │ │ ├── user/ ✅ I4 │ │ ├── game/ ✅ I5, I9, I10, I11 │ │ │ ├── seat-state.memory.ts · table.repository.ts │ │ │ ├── game.gateway.ts · game.routes.ts · game.module.ts │ │ │ ├── deck.ts · hand.engine.ts · hand.evaluator.ts │ │ │ ├── hand.repository.ts · hand.service.ts · hand-state.memory.ts │ │ │ ├── pot.calculator.ts · action.validator.ts · time.bank.service.ts │ │ ├── tournament/ ✅ I6, I7, I8, I11 │ │ │ ├── tournament.repository.ts · tournament.routes.ts │ │ │ ├── tournament.module.ts · tournament.lifecycle.service.ts │ │ │ ├── buyin.service.ts · payout.calculator.ts │ │ │ ├── schedule.repository.ts · schedule.service.ts · schedule.module.ts │ │ ├── handhistory/ ✅ I15, I16 │ │ │ ├── handhistory.repository.ts · replayer.service.ts │ │ │ ├── handhistory.routes.ts · handhistory.module.ts │ │ │ ├── stats.flagger.ts · stats.aggregator.ts │ │ │ └── stats.routes.ts · stats.module.ts │ │ ├── chat/ ✅ I12 │ │ ├── clubboard/ ✅ I14 │ │ └── notification/ ✅ I13 │ ├── workers/ │ │ ├── cleanup.worker.ts (Sessions/Bans, 60 min) │ │ ├── schedule.worker.ts (Schedule-Window, 60 min) │ │ └── (geplant: backup.worker.ts) │ └── tests/ │ ├── helpers/app.ts │ └── *.test.ts (341 Tests, alle grün)
6.2 Modul-Verantwortlichkeiten
| Modul | Verantwortet | Wichtige Klassen |
|---|---|---|
auth | Registrierung, Login, Session, Middleware | auth.service.ts, auth.middleware.ts |
ban | 3-Stufen-Bans (D16), Guards | ban.service.ts, ban.guards.ts |
accounting | Doppelte Buchführung (D15), Konten | account.repository.ts, transaction.service.ts |
user | Profile, Anzeigename (D19), Preferences (D10), Avatar | display-name.service.ts, preferences.service.ts |
game | Tische, Sitze, Memory-First-State (D20), Hand-Engine, Action-Loop, Realtime | siehe 6.1 |
tournament | Turnier-Lifecycle, Registration, Sitzzuteilung (D22), Schedules, Buyin/Payout, Bounty | siehe 6.1 |
handhistory | Replayer (D7), HUD-Stats (D17b), Series-Leaderboards | siehe 6.1 |
chat | Club-Chat + Tisch-Chat, Moderation, Wortfilter, Mutes, Slow-Mode | chat.gateway.ts, moderation.service.ts |
clubboard | Korkbrett (Posts mit Soft-Delete) | clubboard.repository.ts |
notification | Inbox-Items, Popups, Realtime-Notification | notification.service.ts |
7API-Spezifikation
Alle Endpoints unter /api. Authentifizierung über Session-Cookie. Antworten als JSON. Bei Fehlern: { message: string, code?: string }.
7.1 Implementierte Endpoints (I1–I16)
Auth & Session
| Method | Path | Beschreibung |
|---|---|---|
| POST | /auth/register | {email, emailConfirm, displayName, password} |
| POST | /auth/login | {email, password} → Cookie |
| POST | /auth/logout | Löscht aktuelle Session |
| POST | /auth/logout-all | Löscht alle Sessions des Users |
| GET | /auth/me | {id, email, displayName, role, status, avatarPath} |
User & Preferences
| Method | Path | Beschreibung |
|---|---|---|
| GET | /users/:id/profile | Öffentliches Profil |
| PUT | /api/profile/display-name | Cooldown 30 Tage |
| GET | /api/users/me/preferences | 4 Blobs |
| PUT | /api/users/me/preferences | Partial-Update |
| POST | /api/users/me/avatar | Multipart, max 200 KB |
Accounting
| Method | Path | Rolle |
|---|---|---|
| GET | /api/accounts/me | player |
| POST | /api/accounts/me/transfer | player |
| POST | /api/accounts/me/donate | player |
| POST | /api/accounts/me/request-load | player |
| GET | /api/admin/accounts | admin |
| POST | /api/admin/accounts/transfer | admin |
| POST | /api/admin/users/:id/chips | admin |
Tournaments
| Method | Path | Beschreibung |
|---|---|---|
| GET | /api/tournaments | Liste (?status=registering) |
| GET | /api/tournaments/:id | Detail + players + blind_levels |
| POST | /api/tournaments | Anlegen (alle Spieler, D3) |
| POST | /api/tournaments/:id/register | Anmelden |
| POST | /api/tournaments/:id/unregister | Abmelden (vor Start) |
| POST | /api/tournaments/:id/start | Start + Fisher-Yates |
| POST | /api/tournaments/:id/rebuy | Rebuy (heutige 1+addon-Variante) |
| POST | /api/tournaments/:id/addon | Addon |
| POST | /api/admin/tournaments/:id/pause | Admin |
| POST | /api/admin/tournaments/:id/resume | Admin |
Schedules & Series (I7)
| Method | Path | Beschreibung |
|---|---|---|
| GET | /api/tournament-schedules | Liste |
| POST | /api/tournament-schedules | Wiederholung anlegen |
| DELETE | /api/tournament-schedules/:id | Löschen |
| GET | /api/tournament-series | Liste |
| POST | /api/tournament-series | Serie anlegen |
| GET | /api/tournament-series/:id/leaderboard | Series-Punkte-Rangliste |
Hand-History & Stats (I15, I16)
| Method | Path | Beschreibung |
|---|---|---|
| GET | /api/hands/:id | Hand-Detail mit D7-Sichtbarkeitsfilter |
| GET | /api/hands?table=&from=&to= | Hände-Liste (nur eigene Anwesenheit) |
| GET | /api/stats/me | VPIP/PFR/3-Bet/AF aggregiert |
| GET | /api/stats/users/:id | Stats anderer User (öffentlich) |
Chat & ClubBoard
| Method | Path | Beschreibung |
|---|---|---|
| GET | /api/chat/club | Club-Chat-Verlauf |
| DELETE | /api/chat/messages/:id | Eigene/Admin |
| PATCH | /api/chat/messages/:id | Eigene |
| GET | /api/clubboard | Alle Posts (newest first) |
| POST | /api/clubboard | Post anlegen |
| PATCH | /api/clubboard/:id | Eigenen Post bearbeiten |
| DELETE | /api/clubboard/:id | Soft-Delete |
Notifications & Inbox (I13)
| Method | Path | Beschreibung |
|---|---|---|
| GET | /api/inbox | Eigener Posteingang (unread zuerst) |
| PATCH | /api/inbox/:id/read | Als gelesen markieren |
| POST | /api/admin/popup | Popup an alle oder einen User |
7.2 Geplante Endpoints (I17–I19)
| Method | Path | Iter | Zweck |
|---|---|---|---|
| — | Balancer-Logik (interne Events) | I17 | seat.assigned-Event bei Tischbruch |
| POST | /api/tournaments/:id/re-entry | I18 | Re-Entry nach Bust-Out (modusabhängig) |
| GET | /api/tournaments/:id/lobby-state | I19 | Erweiterter Lobby-State: Spielerliste mit Live-Stack, Payout-Vorschau |
| GET | /api/tournaments/:id/payout-structure | I19 | Schematische + konkrete Auszahlungsstruktur |
7.3 Socket.IO-Events
| Event (Client → Server) | Payload | Seit |
|---|---|---|
table.join | {tableId, seatIndex?} — seatIndex nur für Cashgame-Tische (D22) | I5 |
table.leave | {tableId} | I5 |
action | {handId, type, amount?} | I10 |
chat.send | {roomKind, roomRef?, text} | I12 |
| Event (Server → Client) | Room | Seit |
|---|---|---|
state-update | table:<id> | I5 |
balance:update | user:<id> | I3 |
chat.new | table:<id> / club:global | I12 |
notification | user:<id> | I13 |
tournament.update | tournament:<id> | I11 |
lobby.update I19 | tournament:<id> | geplant |
seat.assigned I17 | tournament:<id> | geplant |
8Querschnittsthemen
core/realtime.ts — Fassade um Socket.IO. Module rufen bus.emit(room, event, payload). Ban-Check beim Verbindungsaufbau. Module kennen io nicht direkt.
Beim Bootstrap: verifyAccounts() (SA-1), verifyLedgerInvariants() (SA-2), verifyPrizePools(). Korrigiert balance_cache aus Ledger-Summe und protokolliert Abweichungen.
validation/blob-versioning.ts — Validiert JSON-Blobs gegen zod-Schema, prüft _v ≤ BLOB_SCHEMA_VERSION (SA-3). BlobMigrationWorker beim Bootstrap migriert auf aktuelle Version.
core/time.ts — Interface mit realClock und MockClock für Tests. Alle Zeitlogik (Cooldown, Session-Ablauf, Late-Reg) geht durch Clock — keine direkten Date.now()-Aufrufe in Modulen.
Läuft alle 60 Min: löscht abgelaufene Sessions, setzt status='active' nach Ablauf von Vollsperren, löscht abgelaufene Mutes.
Rollender 4-Wochen-Window-Generator für tournament_schedules. Idempotent (hasInstance-Guard), läuft beim Bootstrap und alle 60 Min.
9Schema-Analyse — Risiken SA-1 bis SA-7
| ID | Befund | Behandlung | Status |
|---|---|---|---|
| SA-1 | Cache-Konsistenz von balance_cache und prize_pool. Denormalisierte Aggregate ohne DB-seitige Konsistenzerzwingung. |
Alle Schreibwege durch TransactionService.commit() (Nullsummen-Pattern). ConsistencyCheckService.verifyAccounts() beim Bootstrap korrigiert Abweichungen. |
✅ I3 |
| SA-2 | Nullsummen-Invariante nur per Test. Die Buchungsregel ist eine Anwendungsinvariante, kein DB-Constraint. | TransactionService.commit() validiert Nullsumme in derselben Transaktion. verifyLedgerInvariants() beim Bootstrap. |
✅ I3 |
| SA-3 | JSON-Blobs in Preferences und Schedule-Templates. Entziehen sich DB-Schema; Schema-Drift möglich. | Jeder Blob trägt _v. BlobValidator prüft gegen zod-Schema. BlobMigrationWorker beim Bootstrap. |
✅ I4 |
| SA-4 | Polymorphe Referenzen ohne FK-Constraint. ref_type + ref_id in transactions und inbox_items kann SQLite nicht per FK absichern. |
PolymorphicResolver als einzige Auflösungsstelle. CHECK-Constraint für accounts.owner_type. |
✅ I3+ |
| SA-5 | Write-Amplifikation durch Game-State in seats. DB-getriebene Live-Verwaltung wäre SQLite-Bottleneck. |
Memory-First-Strategie (D20): SeatStateMemory. DB-Snapshot nur bei Hand-Ende, Sit-Down/Stand-Up, Tournament-Start. |
✅ I5 (D20) |
| SA-6 | Fehlende Moderator-Rolle. users.role kennt nur admin und player. |
Backlog für Phase E. Rollen-Erweiterung als eigene Migration plus Permission-Map. | Backlog (E) |
| SA-7 | Board als String in hands.board. Kompakter String gut für Replayer, blockiert analytische Queries auf Karten-Ebene. |
Hybrid-Modell (D21): String bleibt; zusätzlich hand_board_cards mit pro-Karten-Zeilen. Beide in einer Transaktion. |
✅ I9 (D21) |
10Risiken R1 bis R14
| ID | Risiko | Status nach I16 |
|---|---|---|
| R1 | Tournament-Engine groß, Kontext-Sprengung | Mitigiert — I6–I8 + I11 sauber zerlegt; nur Balancer (I17) bleibt offen |
| R2 | Hand-Engine Sonderfälle (All-In, Side-Pots, Split, Walk) | Mitigiert — I9 Unit-Test-Abdeckung, I11 End-to-End verifiziert |
| R3 | HTML-Anbindung organisch gewachsen | Wächst weiter mit I17–I19. Wird mit Phase B (React-Port) adressiert. |
| R4 | Replayer-Sichtbarkeitsfilter subtil | Mitigiert — I15 mit D7-Filter implementiert, getestet |
| R5 | In-Process-Scheduler Neustart-Verhalten | Mitigiert — ScheduleWorker idempotent (hasInstance-Guard) |
| R6 | Multi-Table-Balancing vertagt | Offen — I17 |
| R7 | better-sqlite3 synchron auf Eventloop | Beobachtet — keine Auffälligkeiten in I10/I11. Bei Cashgame erneut prüfen. |
| R8 | Frontend-Mock-Inkonsistenzen | Reduziert — fast alle Mock-Stellen ersetzt; Restpunkte siehe Anhang D |
| R9 | Memory ↔ DB-Drift bei Crash | Mitigiert — Bootstrap-Recovery; Hand-in-flight wird bei Crash verworfen (akzeptiert) |
| R10 | Anzeigename-Wechsel während laufender Hand | Mitigiert — hand_players.user_id referenziert; Anzeigename zur Laufzeit aufgelöst |
| R11 | E-Mail-Tippfehler bei Registrierung | Teil-Mitigation — Doppelt-Eingabe-Validierung. Admin-Korrektur bleibt Backlog. |
| R12 | React-Stack-Wahl: ungeeignete Library zementiert Probleme | Mitigation: separate Brainstorming-Session vor Phase B (Stack-Festlegung) |
| R13 | Cashgame-Anforderungen unklar | Mitigation: separate Brainstorming-Session vor Phase D |
| R14 | Lobby-Strukturarbeit verändert API → React-Bindings müssen nach | Mitigation: Lobby-Umbau (I19) vor Phase B Port-Phase 1 |
11Test-Strategie
11.1 Pyramide
Integration-Tests (HTTP, gegen :memory:-DB) ──────────────────── Modul-Tests (DB, ohne Edge-Layer) ─────────────────────────── Unit-Tests (Engines, Calculator, Validator) ──────────────────────────────────────
11.2 Status nach I16
| Bereich | Tests | Status |
|---|---|---|
| auth | 18 | ✅ |
| ban | 13 | ✅ |
| accounting | 17 | ✅ |
| user / preferences / display-name | 19 | ✅ |
| game / tables / seat-state | 19 | ✅ |
| tournament + lifecycle + payout | 15 | ✅ |
| schedule | 18 | ✅ |
| chat / moderation | 30 | ✅ |
| notification / inbox | 17 | ✅ |
| clubboard | 16 | ✅ |
| handhistory / replayer / stats | 15 | ✅ |
| Gesamt | 341 | ✅ alle grün |
11.3 Test-Konventionen
:memory:-SQLite-DB pro Test-Datei; vollständige Migration bei SetupMockClockfür alle Zeit-Abhängigkeiten- HTTP-Tests via supertest gegen
app.ts-Builder - Keine echten Sockets in Unit-Tests;
RealtimeBusmitvi.fn()-Mock - Seed-Daten ausschließlich pro Test-Suite, kein globaler State
11.4 Geplante Test-Schwerpunkte
| Iteration | Test-Fokus |
|---|---|
| I17 | Balancer (Tisch-Bruch, Final-Table-Merge, MULTI_TABLE_LIMIT-Check) — balancer.test.ts, ≥ 8 Fälle |
| I18 | Rebuy classic (Multi-Rebuy, Addon-Window), Re-Entry (Bust-Out-Erkennung, neue Sitzzuteilung), Wizard-Validierung — rebuy.test.ts, ≥ 12 Fälle |
| I19 | Lobby-State (Stack-Sortierung live, Payout-Berechnung bei verschiedenen Curves) — lobby-state.test.ts |
| Phase B | React-Testing-Library für Komponenten, Playwright für E2E |
| Phase D | Cashgame-Lifecycle (Sit-Down/Stand-Up, Auto-Top-Up, Rake-Erfassung) |
12Roadmap mit Iterationsschritten
12.1 Prinzipien (D14)
- Eine Iteration = eine Claude-Code-Session
- Jede Iteration endet lauffähig: Server startet, Tests grün, das adressierte Feature ist im Browser klickbar
- Schema-Änderungen und Spiellogik niemals in derselben Session mischen
- Wenn eine Iteration zu groß wird: Splitten
12.2 Iterations-Übersicht
Phase C — Backend Restpunkte
I17 — Multi-Table-Balancer
I18 — Rebuy-Vollausbau (D23)
I19 — Turnier-Lobby-Anzeigeumfang (D25)
↓
Phase B — React-Port
B0 — Stack-Festlegung (separate Brainstorming-Session)
B1 — Plattform-Setup
B2.x — Domänen-Migration in mehreren Iterationen
↓
Phase B/D verschränkt
D0 — Cashgame-Anforderungs-Spezifikation (separate Brainstorming-Session)
D-Iterationen (Schema → Lifecycle → Cashier → React-Komponenten)
↓
Phase E — Moderator-Rolle, E-Mail, Postgres-Option
12.3 I17 — Multi-Table-Balancer
Eingangsstand: I16. Phase C bis Single-Table funktional vollständig.
Lieferumfang:
modules/tournament/balancer.service.ts— Algorithmus zur Tisch-Auflösung und Sitz-Umverteilung bei ungleicher Tisch-Belegung- Erweiterung
tournament.lifecycle.service.tsum Balancer-Aufruf nach jedem Bust-Out - Konstante
MAX_PLAYERS_PER_TABLEinconfig.ts(Default 8, im Wizard überschreibbar) - Schema:
tournaments.max_players_per_table INTEGER default 8(Migration 0010b) - Socket-Event
seat.assignedfür Spieler, deren Tisch gewechselt wird - HTML-Anbindung: Toast „Du wurdest an Tisch X umgesetzt", automatischer Tisch-Wechsel
Verifikation: 10 Spieler in einem Multi-Table-Turnier (2 × 5). Spieler bustet out → Balancer prüft Differenz → bei > 1 ausgeglichen. Bei count = 8 Final-Table-Merge. Tests: balancer.test.ts mit ≥ 8 Fällen.
Übergabezustand: Multi-Table-Tournaments funktionieren End-to-End.
12.4 I18 — Rebuy-Vollausbau (D23)
Eingangsstand: I17.
Schema-Änderung (Migration 0011):
tournaments
├── rebuy_mode ENUM('freeze-out','classic','re-entry') default 'freeze-out'
├── rebuy_phase_min INTEGER default 60 -- nur classic
├── rebuy_cost INTEGER NULL -- classic/re-entry; default = buyin
├── addon_cost INTEGER NULL -- nur classic
├── addon_chips INTEGER NULL -- nur classic
├── rebuy_chips INTEGER NULL -- nur classic (default = Startstack)
├── re_entry_window_min INTEGER default 60 -- nur re-entry
├── rebuy_phase_end_at INTEGER NULL -- berechnet bei Tournament-Start
tournament_players
├── re_entry_count INTEGER default 0
Lieferumfang:
- Lifecycle-Erweiterung: Bei
startberechnerebuy_phase_end_at. Timer triggert „Rebuy-Phase Ende" → Broadcast. Re-Entry bei Bust-Out im Fenster: Fisher-Yates auf freie Sitze. buyin.service.tserweitern:bookRebuy()modus-abhängig;bookAddon()nurclassic;bookReEntry()nurre-entrynach Bust-Out im Fenster.- API:
POST /api/tournaments/:id/rebuy(anhand Modus),/re-entry(neu),/addon. - Wizard (HTML): Dropdown mit drei Optionen; pro Modus konditionale Felder.
- Turnier-Lobby (HTML): Rebuy-Phase-Anzeige, Rebuy- und Re-Entry-Button modus-/zustandsabhängig.
Verifikation: Classic: 3-Spieler, Rebuy-Phase 5 Min., mehrfach Rebuy + Addon → Prize Pool stimmt. Re-Entry: Bust-Out → Re-Entry → neuer Sitz; außerhalb Fenster: 409. Freeze-Out: Rebuy-Endpoint gibt 409. Tests: rebuy.test.ts ≥ 12 Fälle.
Übergabezustand: Alle drei Rebuy-Modi funktional; Wizard + Lobby korrekt.
12.5 I19 — Turnier-Lobby-Anzeigeumfang (D25)
Eingangsstand: I18.
Lieferumfang:
- API-Erweiterung:
GET /api/tournaments/:id/lobby-state— Turnier-Stamm, Spielerliste mit Live-Stacks (ausSeatStateMemory), Payout-Vorschau.GET .../payout-structure— schematisch + konkret. Socket-Eventlobby.update. - HTML-Anpassung: Spielerliste mit
displayName, Status, Stack (beirunning). Sortierung:registering→ Anmeldezeit;running→ Stack absteigend;finished→final_place. Auszahlungs-Sektion: zweispaltig (Anteil % / Chips). - Payout-Curve-Logik: Funktion
previewPayoutStructure(playerCount, prizePool, curve)inpayout.calculator.ts.
Verifikation: Anmeldung → Liste aktualisiert < 100 ms. Stack-Änderung → Sortierung aktualisiert. Payout bei standard-Curve + 8 Spielern korrekt.
Übergabezustand: Phase C funktional vollständig auch unter D25. HTML-Plattform final aufgerüstet für React-Port.
12.6 Phase B — React-Port
| Iter | Inhalt | Voraussetzung |
|---|---|---|
| B0 | Stack-Festlegung als eigene Brainstorming-Session — Entscheidung wird als D26 dokumentiert | I19 |
| B1 | Plattform-Setup: frontend/, Build, Linting, TS strict, Test-Stack | B0 |
| B2.1 | Auth + Profil + Cashier-Domäne | B1 |
| B2.2 | Lobby + Tisch-Fenster + Wizard | B2.1 |
| B2.3 | Replayer + HUD + Admin-Backoffice | B2.2 |
| B3 | UI-Strukturarbeiten in React (Lobby-Refinement, Settings) | B2.3 |
| B4 | E2E-Tests (Playwright), Performance-Baseline (Lighthouse) | B3 |
Stack-Anforderungen (ohne konkrete Produkt-Wahl): Strict-TS mit noUncheckedIndexedAccess, Komponententests mit JSDOM, Routing mit Lazy-Loading, kein Singleton-Store-Anti-Pattern, Komponentenscope-Styling, Hot-Reload < 1 s, Bundle < 500 KB gzipped initial.
12.7 Phase D — Cashgame-Engine
Offene Fragen: NL Hold'em allein oder PL/FL? Buy-In-Range pro Tisch? Auto-Top-Up: User-Preference oder pro Tisch? Stand-Up-Politik? Rake-Modell? Mehrtisch-Limit?
| Iter | Inhalt |
|---|---|
| D0 | Anforderungs-Spezifikation (Brainstorming) |
| D1 | Schema: Cashgame-Erweiterung in tables |
| D2 | Lifecycle: Sit-Down mit Buy-In, Stand-Up, Auto-Top-Up |
| D3 | Hand-Engine-Anpassung: Rake-Erfassung pro Pot |
| D4 | React-Cashgame-Tisch-Komponente |
| D5 | Lobby-Integration: Cashgame-Liste mit Stakes, Auslastung, Average-Pot |
AAnhang A — Glossar
| Begriff | Bedeutung |
|---|---|
Anzeigename (display_name) | Pflichtfeld, eindeutig (COLLATE NOCASE), 30-Tage-Cooldown. Überall statt username. |
| Lowercase normalisiert, UNIQUE. Login-Identifier. Nicht öffentlich. | |
Snapshot (seats) | DB-Persistenz des Seat-Memory-States bei definierten Sync-Punkten (D20). |
_v (Blob-Version) | Versionsfeld in JSON-Blobs (SA-3). |
| Fisher-Yates | Zufälliger Shuffle-Algorithmus für Sitzzuteilung (D22) und Deck. |
| VPIP | Voluntary Put In Pot. BB-Post zählt nicht (D17b). |
| PFR | Pre-Flop Raise. Erste freiwillige Raise (D17b). |
| 3-Bet | Erste Re-Raise preflop (D17b). |
| AF | Aggression Factor = (Bet+Raise)/(Call) (D17b). |
| WAL | Write-Ahead-Log-Modus von SQLite. Erlaubt concurrent reads neben einem writer. |
| Side-Pot | Zusätzlicher Pot bei All-In mit unterschiedlichen Stack-Höhen. |
| Time-Bank | Pro Spieler kumulierter Zeitpuffer (D8, D17c). |
| Memory-First | Live-Spielzustand im Anwendungs-Memory, DB nur Snapshot (D20). |
| Bust-Out | Spieler verliert gesamten Stack und scheidet aus. |
| Re-Entry | Wiedereinstieg nach Bust-Out im definierten Fenster mit neuem Buy-In (D23). |
| Classic Rebuy | Mehrfacher Stack-Auffüll-Kauf während Rebuy-Phase (D23). |
| Addon | Einmaliger optionaler Stack-Zukauf am Ende der Rebuy-Phase (D23). |
| Freeze-Out | Turnier ohne Rebuy/Addon — Single-Buyin (D23). |
| Late-Reg | Late-Registration-Fenster ab Tournament-Start (D17c). |
BAnhang B — Datenbank-Indexes
Vollständige Index-Liste (Stand I16):
INDEX bans(user_id, expires_at) INDEX ledger_entries(account_id, ts DESC) INDEX seats(table_id) INDEX seat_sessions(table_id, user_id, sat_down_at) INDEX tables(status) INDEX tournaments(status) INDEX tournaments(schedule_id) INDEX tournaments(series_id) INDEX tournament_players(tournament_id) INDEX tournament_players(user_id) INDEX tournament_schedules(is_active) INDEX blind_levels(tournament_id, level) INDEX hands(table_id, started_at DESC) INDEX hand_players(user_id) INDEX hand_players(hand_id) INDEX actions(hand_id, seq) INDEX hand_board_cards(rank, suit) INDEX hand_board_cards(hand_id, street, position) INDEX chat_messages(room_kind, room_ref, at DESC) INDEX inbox_items(user_id, read_at) INDEX club_board(deleted_at, created_at DESC)
Geplant (I18): tournaments(rebuy_mode), falls Lobby-Filter nach Modus eingeführt wird.
CAnhang C — Konfigurations-Konstanten
Quelle: backend/src/config.ts. Stand I16; ergänzt für I17/I18.
export const CONFIG = {
// Gameplay (D17c)
TIME_PER_ACTION_DEFAULT_SEC: 30,
TIME_BANK_INITIAL_MS: 90_000,
TIME_BANK_REFILL_PER_HAND_MS: 5_000,
TIME_BANK_MAX_MS: 90_000,
LATE_REG_DEFAULT_MIN: 30,
PAUSE_PATTERN_DEFAULT: 'none' as const,
MULTI_TABLE_LIMIT: 4,
SHUTDOWN_GRACE_MS: 5_000,
// Infrastruktur
BACKUP_INTERVAL_HOURS: 24,
BACKUP_KEEP_DAYS: 7,
SESSION_LIFETIME_DAYS: 30,
SCHEDULE_WINDOW_DAYS: 28,
SCHEDULE_WORKER_INTERVAL_MIN: 60,
CLEANUP_WORKER_INTERVAL_MIN: 60,
// User / Anzeigename (D19)
DISPLAY_NAME_COOLDOWN_DAYS: 30,
DISPLAY_NAME_MIN_LEN: 3,
DISPLAY_NAME_MAX_LEN: 24,
DISPLAY_NAME_REGEX: /^[A-Za-z0-9_\-äöüÄÖÜß ]+$/,
// Upload
AVATAR_MAX_BYTES: 200_000,
// Tisch / Sitze (I5)
DEFAULT_SEAT_COUNT: 8,
DEFAULT_START_STACK: 10_000,
// Turniere (I6 — D22, I17 — Multi-Table)
MIN_TOURNAMENT_PLAYERS: 2,
MAX_TOURNAMENT_PLAYERS: 8,
MAX_PLAYERS_PER_TABLE: 8, // I17 — Default für Wizard
// Rebuy (I18 — D23)
REBUY_PHASE_DEFAULT_MIN: 60,
RE_ENTRY_WINDOW_DEFAULT_MIN: 60,
// Validation + Konsistenz (SA-1/SA-3)
BLOB_SCHEMA_VERSION: 2,
CONSISTENCY_CHECK_INTERVAL_HOURS: 24,
} as const
DAnhang D — Frontend-Anpassungspunkte
Stand der HTML-Dateien nach I16: HTML/greenPoker_v45.9.html, HTML/greenPokerAdmin_v44.26.html.
D.1 — Spieler-Frontend
| Bereich | Status | Beschreibung |
|---|---|---|
| Login-Feld (E-Mail) | ✅ I1 | type="email", Placeholder „E-Mail-Adresse" |
| Registrierungsformular | ✅ I1 | Anzeigename + E-Mail-Bestätigung; kein Username |
| submitLogin() / submitRegister() | ✅ I1 | Rufen POST /auth/login / /auth/register |
| Cashier (Balance, Transfer, Donate, Request-Load, Verlauf) | ✅ I3 | Aus API |
| Anzeigename-Änderung im Profil-Tab | ✅ I4 | Cooldown-Anzeige, API |
| Layout-Slots / Tisch-Einstellungen → API | ✅ I4 | |
| Lobby-Tab „Turniere" | ✅ I6 | Aus API mit Anmelden/Starten |
| Turnier-Wizard in Lobby | ✅ I6 | POST /api/tournaments |
| Wizard Tab „Wiederholung / Serien-Zuordnung" | ✅ I7 | Schedule + Series-Dropdown |
| Tisch-Fenster (openLiveTableWin) | ✅ I5/I6 | Socket.IO; kein manuelles Sitzen bei Tournament-Tischen |
| demoActionForWin → echtes socket.emit('action', ...) | ✅ I10 | |
| Inbox-Bell + Badge | ✅ I13 | |
| Club Board | ✅ I14 | |
| HUD zeigt displayName | ✅ I16 | |
| Replayer zeigt displayName | ✅ I15 | |
| Rebuy-Button (heute 1+addon) | ✅ I8 (Teil) | Eingeschränkt; voller Ausbau in I18 |
| Multi-Table-Toast / auto. Tisch-Wechsel | → I17 | |
| Wizard mit Modus-Auswahl (freeze/classic/re-entry) | → I18 | |
| Re-Entry-Button nach Bust-Out | → I18 | |
| Spielerliste in Lobby-Detail mit Live-Stack | → I19 | |
| Schematische + konkrete Auszahlungsstruktur | → I19 |
D.2 — Admin-Frontend
| Bereich | Status | Beschreibung |
|---|---|---|
| Login (E-Mail) | ✅ I2 | |
| Kicks & Bans | ✅ I2 | Aus API |
| Chip-Verwaltung + Club-Konten | ✅ I3 | |
| Tisch-Monitor | ✅ I5/I6 | Aus API; Turnier-Detail + Admin-Start-Button |
| Pause/Resume-Buttons für Turniere | ✅ I14 | |
| Admin-Posteingang (Konto-Anträge) | ✅ I13 | |
| Freischalt-Button für Pending-User | ✅ I13 | |
| Popup-Sender | ✅ I13 | |
| Club Board | ✅ I14 | |
| Chat-Moderation (Wortfilter, Mutes, Slow-Mode, Pin) | ✅ I12 | |
| Balancer-Monitoring (welche Tische, Differenzen) | → I17 | |
| Wizard mit Rebuy-Modus-Konfiguration | → I18 |
EAnhang E — Übergabe-Logs der abgeschlossenen Iterationen
Lieferumfang: Express + Socket.IO + DB-Init, users + sessions, Auth-Module (E-Mail-Login, Anzeigename, Doppelt-Eingabe), Session-Middleware, Health-Endpoint, Seed-Script.
Übergabezustand: Server läuft mit Auth. Sessions per httpOnly-Cookie. Admin-Genehmigung: Registrierung setzt status='pending'. 18 Tests.
Lieferumfang: bans-Tabelle, 3-Stufen-Logik (D16), banGuard-Middleware, Admin-Endpoints, cleanup.worker-Skeleton, Ban-Check bei Socket.IO-Connect.
Übergabezustand: Vollsperre invalidiert Sessions sofort. CleanupWorker stellt status='active' nach Ablauf wieder her. 31 Tests.
Lieferumfang: accounts, transactions, ledger_entries, TransactionService (Nullsummen-Pattern), ConsistencyCheckService, PolymorphicResolver, vollständige Accounting-Endpoints.
Übergabezustand: Jede Registrierung legt automatisch Spieler-Konto an. Club-Konten beim Server-Start sichergestellt. Nullsummen-Invariante mit Rollback. 48 Tests.
Lieferumfang: user_preferences-Tabelle (4 Blobs), BlobMigrationWorker, display-name.service.ts mit Cooldown, Avatar-Upload, PUT /api/profile/display-name, GET/PUT /api/users/me/preferences.
Übergabezustand: Preferences serverseitig in 4 versionierten JSON-Blobs (SA-3). 67 Tests.
Lieferumfang: Schema tables, seats, seat_sessions. SeatStateMemory-Klasse (D20). Socket.IO table.join / table.leave. Bootstrap-Recovery.
Übergabezustand: Tisch-Modell steht; bootstrapFromDb() lädt Sitze beim Start. 86 Tests.
Planabweichung gegenüber v4: Registration + automatische Sitzzuteilung (ursprünglich I8) vorgezogen.
Lieferumfang: Schema tournaments, tournament_players, blind_levels. Endpoints POST/GET /tournaments/..., register, unregister, start mit Fisher-Yates. Manuelles Sitzen für Tournament-Tische blockiert (D22).
Übergabezustand: Tournament-Anmeldung und Sitzzuteilung vollständig. 101 Tests.
Lieferumfang: Schema tournament_series, tournament_schedules. schedule.service.ts mit calculateOccurrences() und generateInstances() (rolling 4-Wochen-Window). schedule.worker.ts. 7 Endpoints.
Übergabezustand: ScheduleWorker idempotent (hasInstance-Guard). 119 Tests.
Lieferumfang: buyin.service.ts mit bookBuyin(), bookRebuy() (heutige 1+addon-Variante), bookAddon(), bookRefund(). payout.calculator.ts. verifyPrizePools().
Bekannte Lücken (Anlass für I18): Rebuy nur einmalig, kein Mid-Stack-Rebuy in echter Phase, kein Re-Entry nach Bust-Out.
Lieferumfang: Schema hands, hand_board_cards (D21), hand_players, actions. deck.ts (crypto.randomBytes-Shuffle). hand.evaluator.ts. pot.calculator.ts (Side-Pots). hand.engine.ts (pure).
Übergabezustand: Tests grün für All-In, Side-Pots, Split, Showdown.
Lieferumfang: game.gateway.ts Action-Handler, action.validator.ts, time.bank.service.ts, hand.service.ts. Memory-Sync bei Action; Snapshot bei Hand-Ende. Realtime-Broadcast state-update.
Übergabezustand: Action in Tab A erscheint in Tab B < 100 ms.
Lieferumfang: Showdown-Pfad, Side-Pot-Verteilung. Bounty-Verteilung. Tournament-Ende: Payout-Curve-Anwendung, Series-Punkte-Berechnung (D17a), final_place, prize_won.
Übergabezustand: Single-Table-Tournaments funktionieren End-to-End.
Lieferumfang: chat_messages, word_filters, mutes, chat_room_settings. chat.gateway.ts. moderation.service.ts (Wortfilter-Cache, Mute-Check, Slow-Mode-Throttle). 14 REST-Endpoints inkl. Admin-Pinned-Message.
Übergabezustand: Club-Chat + Tisch-Chat. Admin: Wortfilter, Mutes, Slow-Mode, Pin. 30 neue Tests.
Lieferumfang: inbox_items, popup_log. notification.service.ts. 4 Endpoints. Callbacks onUserRegistered, onRequestLoad, onUserApproved.
Übergabezustand: Registrierung → Admin-Inbox. Request-Load → Admin-Inbox. Approve → Inbox an User. 17 neue Tests.
Lieferumfang: club_board. 4 Endpoints. Admin: Pause/Resume-Buttons für Turniere. Tisch-Chat aus REST entfernt.
Übergabezustand: Club Board als Soft-Delete-Liste. 16 neue Tests.
Restpunkt: backup.worker.ts nicht implementiert. Bei Bedarf Mini-Iteration „I14b — BackupWorker".
Lieferumfang: handhistory.repository.ts mit D7-Filter. replayer.service.ts mit getHandDetail(). Endpoints GET /api/hands/:id, GET /api/hands?table=.... Frontend-Replayer API-gesteuert mit convertApiHandToRpl().
Übergabezustand: Eigene Karten immer sichtbar; fremde nur bei shown_at_showdown=1. 15 neue Tests.
Lieferumfang: stats.flagger.ts (VPIP/PFR/3-Bet pro Hand-Action-Sequenz, D17b), stats.aggregator.ts, stats.routes.ts mit GET /api/stats/me, GET /api/stats/users/:id. Series-Leaderboard.
Übergabezustand: Phase C funktional vollständig für Single-Table-Tournaments. 341 Tests grün.