// Quick engine sanity sim: play full all-AI matches, assert no illegal states. import { chooseCardAI, chooseTrumpAI } from "../src/lib/hokm/ai"; import { advanceAfterTrick, chooseTrump, createInitialState, dealForTrump, playCard, selectHakem, startNextRound, } from "../src/lib/hokm/engine"; import { GameState } from "../src/lib/hokm/types"; import { applyMatchResult, getLeagueInfo } from "../src/lib/online/gamification"; import { MatchSummary, UserProfile } from "../src/lib/online/types"; function playMatch(seed: number): { rounds: number; tricks: number } { let s = createInitialState({ names: ["P0", "P1", "P2", "P3"], targetScore: 7, }); s = selectHakem(s); s = dealForTrump(s); let rounds = 0; let tricks = 0; let guard = 0; while (s.phase !== "match-over") { guard++; if (guard > 100000) throw new Error("loop guard tripped"); if (s.phase === "choosing-trump") { const suit = chooseTrumpAI(s.players[s.hakem!].hand); s = chooseTrump(s, suit); continue; } if (s.phase === "playing") { const seat = s.turn!; const card = chooseCardAI(s, seat); s = playCard(s, seat, card); continue; } if (s.phase === "trick-complete") { tricks++; // sanity: 4 cards in trick if (s.currentTrick.length !== 4) throw new Error("trick not full"); s = advanceAfterTrick(s, 2); continue; } if (s.phase === "round-over") { rounds++; // sanity: total tricks this round <= 13 const total = s.roundTricks[0] + s.roundTricks[1]; if (total > 13) throw new Error("too many tricks: " + total); s = startNextRound(s); continue; } throw new Error("unexpected phase: " + s.phase); } return { rounds, tricks }; } let totalRounds = 0; const N = 200; for (let i = 0; i < N; i++) { const r = playMatch(i); totalRounds += r.rounds; } console.log(`OK: ${N} matches completed. avg rounds/match = ${(totalRounds / N).toFixed(1)}`); /* ----------------------- gamification checks ----------------------- */ function baseProfile(): UserProfile { return { id: "u", username: "u", displayName: "Tester", avatar: "a-fox", level: 1, xp: 0, coins: 1000, rating: 1000, stats: { games: 0, wins: 0, losses: 0, kotsFor: 0, kotsAgainst: 0, tricks: 0, bestWinStreak: 0, currentWinStreak: 0, }, ownedAvatars: ["a-fox"], ownedThemes: ["royal"], achievements: {}, unlocked: [], createdAt: 0, }; } function assert(cond: boolean, msg: string) { if (!cond) throw new Error("ASSERT FAILED: " + msg); } let profile = baseProfile(); let firstWinSeen = false; const M = 500; for (let i = 0; i < M; i++) { const won = (i * 7 + 3) % 5 < 3; // deterministic-ish mix const kot = i % 6 === 0; const summary: MatchSummary = { ranked: true, stake: 100, won, kotFor: won && kot, kotAgainst: !won && kot, tricksWon: won ? 7 + (i % 6) : i % 7, rounds: 7, trump: "spades", }; const before = profile; const { profile: after, reward } = applyMatchResult(before, summary, 1000); // rating moves the right way for ranked if (won) assert(reward.ratingDelta > 0, "ranked win should raise rating"); else assert(reward.ratingDelta < 0, "ranked loss should lower rating"); // coins never negative assert(after.coins >= 0, "coins never negative"); // xp gained, level monotonic assert(reward.xpGained > 0, "xp gained"); assert(after.level >= before.level, "level monotonic"); // unlocked list only grows assert(after.unlocked.length >= before.unlocked.length, "achievements monotonic"); // first win unlocks first_win if (won && !firstWinSeen) { firstWinSeen = true; assert(after.unlocked.includes("first_win"), "first_win unlocks on first win"); } profile = after; } // casual match must not change rating { const r = applyMatchResult(baseProfile(), { ranked: false, stake: 0, won: true, kotFor: false, kotAgainst: false, tricksWon: 7, rounds: 7, trump: null, }, 1000); assert(r.reward.ratingDelta === 0, "casual match must not change rating"); } // league boundaries sane assert(getLeagueInfo(1000).tier.id === "bronze", "1000 = bronze"); assert(getLeagueInfo(1350).tier.id === "gold", "1350 = gold"); assert(getLeagueInfo(2000).tier.id === "master", "2000 = master"); console.log( `OK: gamification ${M} results. final level=${profile.level} rating=${Math.round(profile.rating)} ` + `coins=${profile.coins} achievements=${profile.unlocked.length} league=${getLeagueInfo(profile.rating).tier.id}` );