100+ achievements, forfeit, leagues floor, bot humanize, 95k starter
Achievements: generator-driven, now 100+ across 7 categories (added Rulership) mirrored client + server with identical ids/goals/coins. New tracked stats: hakemRounds (be the hakem — incl. "7× Hakem"), roundsWon, plus losses metric. Custom achievement-only sticker packs (Rulership 👑, Firestorm 🔥) with new inline-SVG art (crown-gold, seven-zip, streak-fire), unlocked by hakem_7 / streak_10. Server GameRoom tallies hakem rounds per seat + rounds won per team; client tallies the same for vs-computer/private games (dealId-deduped). Forfeit (surrender): a player can request forfeit; if the teammate is a bot it auto-confirms, otherwise the human teammate gets a confirm/decline prompt (20s timeout). Result: forfeiting with ≥1 round won = normal loss; 0 rounds = Kot. Wired client↔server over the hub (RequestForfeit/ConfirmForfeit/DeclineForfeit + "forfeit" event); offline/vs-computer ends immediately in the store. Flag button + confirm dialogs in the table. Online count: never shows below 50 — live service floors the real count with a drifting believable number (mock base lowered to ~50–170). Matchmaking: real players get a longer priority window (9s) before bots fill; bots now occasionally react after winning a trick (humanize). Coins: starter pack is 95,000 Toman (50k coins); packs rescaled up (server + mock). Verified: dotnet build + tsc + next build clean; sim unlocks 57 achievements/500 matches; live server: starter=95000, a 7-hakem win unlocks hakem_7 + wins_1 with hakemRounds/roundsWon persisted. Images rebuilt on :1500/:1505. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+6
-3
@@ -86,6 +86,7 @@ function baseProfile(): UserProfile {
|
||||
stats: {
|
||||
games: 0, wins: 0, losses: 0, kotsFor: 0, kotsAgainst: 0,
|
||||
tricks: 0, bestWinStreak: 0, currentWinStreak: 0, shutoutWins: 0,
|
||||
hakemRounds: 0, roundsWon: 0,
|
||||
},
|
||||
plan: "free",
|
||||
ownedAvatars: ["a-fox"],
|
||||
@@ -123,6 +124,8 @@ for (let i = 0; i < M; i++) {
|
||||
rounds: 7,
|
||||
trump: "spades",
|
||||
shutout: won && i % 8 === 0,
|
||||
hakemRounds: i % 3,
|
||||
roundsWon: won ? 7 : i % 7,
|
||||
};
|
||||
const before = profile;
|
||||
const { profile: after, reward } = applyMatchResult(before, summary, 1000);
|
||||
@@ -137,10 +140,10 @@ for (let i = 0; i < M; i++) {
|
||||
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
|
||||
// first win unlocks the first-win achievement (wins_1)
|
||||
if (won && !firstWinSeen) {
|
||||
firstWinSeen = true;
|
||||
assert(after.unlocked.includes("first_win"), "first_win unlocks on first win");
|
||||
assert(after.unlocked.includes("wins_1"), "wins_1 unlocks on first win");
|
||||
}
|
||||
profile = after;
|
||||
}
|
||||
@@ -149,7 +152,7 @@ for (let i = 0; i < M; i++) {
|
||||
{
|
||||
const r = applyMatchResult(baseProfile(), {
|
||||
ranked: false, stake: 0, won: true, kotFor: false, kotAgainst: false,
|
||||
tricksWon: 7, rounds: 7, trump: null, shutout: false,
|
||||
tricksWon: 7, rounds: 7, trump: null, shutout: false, hakemRounds: 0, roundsWon: 7,
|
||||
}, 1000);
|
||||
assert(r.reward.ratingDelta === 0, "casual match must not change rating");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user