From 525cd554ef3cdbfba95c62f95cc5630ccad1fede Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Tue, 7 Jul 2026 23:23:12 +0330 Subject: [PATCH] feat: smarter bot --- internal/user/user.go | 4 +- internal/ws/bot.go | 113 ++++++++++++++++++++++++++++++---------- internal/ws/bot_test.go | 24 ++++++--- internal/ws/room.go | 8 ++- 4 files changed, 112 insertions(+), 37 deletions(-) diff --git a/internal/user/user.go b/internal/user/user.go index 4704c36..4ee9d0e 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -42,9 +42,9 @@ func (r *Repo) FindByID(ctx context.Context, id int64) (*User, error) { return scan(row) } -// Create کاربر جدید می‌سازد. +// Create کاربر جدید می‌سازد (با هدیه‌ی خوش‌آمدِ ۱۰۰ سکه). func (r *Repo) Create(ctx context.Context, mobile string) (*User, error) { - res, err := r.db.ExecContext(ctx, `INSERT INTO users (mobile) VALUES (?)`, mobile) + res, err := r.db.ExecContext(ctx, `INSERT INTO users (mobile, coins) VALUES (?, 100)`, mobile) if err != nil { return nil, err } diff --git a/internal/ws/bot.go b/internal/ws/bot.go index 0476e2e..a18b318 100644 --- a/internal/ws/bot.go +++ b/internal/ws/bot.go @@ -36,10 +36,11 @@ func botChooseTrump(hand []game.Card) game.Suit { return best } -// botPlay کارتی که بات باید بازی کند را برمی‌گرداند. -func botPlay(hand []game.Card, trick []game.TrickCard, trump, lead game.Suit) game.Card { +// botPlay کارتی که بات باید بازی کند را برمی‌گرداند. seen حافظه‌ی کارت‌های +// بازی‌شده‌ی این هَند است (کارت‌شماری برای تصمیمِ بهتر). +func botPlay(hand []game.Card, trick []game.TrickCard, trump, lead game.Suit, seen [4]uint16) game.Card { if len(trick) == 0 { - return botLead(hand, trump) + return botLead(hand, trump, seen) } best := trickBest(trick, trump, lead) @@ -72,41 +73,97 @@ func botPlay(hand []game.Card, trick []game.TrickCard, trump, lead game.Suit) ga return lowestDiscard(hand, trump) // حکمت هم نمی‌برد ⇒ دورریز } -// botLead راهبردِ رهبریِ دست: آسِ غیرِحکم را نقد کن؛ وگرنه از بلندترین خالِ -// غیرِحکم کارتِ کم بریز؛ اگر فقط حکم داری، بالاترین حکم را بزن تا حکم‌ها را بکشی. -func botLead(hand []game.Card, trump game.Suit) game.Card { - // آسِ غیرِحکم (برنده‌ی تقریباً قطعی). - var ace *game.Card - for i := range hand { - if hand[i].Suit != trump && hand[i].Rank == game.Ace { - c := hand[i] - ace = &c - break - } - } - if ace != nil { - return *ace - } +// botLead راهبردِ رهبریِ دست با کارت‌شماری: +// ۱. کارتِ «برنده‌ی قطعی» (بالاترین کارتِ باقی‌مانده‌ی یک خالِ غیرِحکم) را نقد کن. +// ۲. اگر کنترلِ حکم داری (بالاترین حکمِ باقی‌مانده + ۳ حکم یا بیشتر) حکم بکِش. +// ۳. وگرنه از کوتاه‌ترین خالِ غیرِحکم کارتِ کم بریز تا زود void شوی و بعداً بِبُری +// (اگر حکم داری)؛ در نبودِ حکم از بلندترین خال کارتِ کم. +func botLead(hand []game.Card, trump game.Suit, seen [4]uint16) game.Card { + counts := suitCounts(hand) - // بلندترین خالِ غیرِحکم. - var counts [4]int - for _, c := range hand { - counts[c.Suit]++ - } - bestSuit, bestLen := game.Suit(255), -1 - for _, c := range hand { + // ۱. برنده‌ی قطعی: کارتی از خالِ غیرِحکم که هیچ کارتِ بالاتری از آن بیرون نمانده. + var boss *game.Card + for i := range hand { + c := hand[i] if c.Suit == trump { continue } - if counts[c.Suit] > bestLen { - bestLen, bestSuit = counts[c.Suit], c.Suit + if higherOut(seen, hand, c.Suit, c.Rank) == 0 { + if boss == nil || counts[c.Suit] > counts[boss.Suit] || + (counts[c.Suit] == counts[boss.Suit] && c.Rank > boss.Rank) { + cc := c + boss = &cc + } + } + } + if boss != nil { + return *boss + } + + // ۲. کشیدنِ حکم: اگر بالاترین حکمِ باقی‌مانده دستِ ماست و حکمِ زیاد داریم. + trumps := cardsOfSuit(hand, trump) + if len(trumps) >= 3 { + top := highest(trumps) + if higherOut(seen, hand, trump, top.Rank) == 0 { + return top + } + } + + // ۳. کوتاه‌ترین خالِ غیرِحکم (برای void شدن) اگر حکم داریم؛ وگرنه بلندترین. + haveTrump := len(trumps) > 0 + bestSuit, bestLen := game.Suit(255), -1 + for s := game.Suit(0); s < 4; s++ { + if s == trump || counts[s] == 0 { + continue + } + better := false + if bestLen < 0 { + better = true + } else if haveTrump { + better = counts[s] < bestLen // کوتاه‌ترین + } else { + better = counts[s] > bestLen // بلندترین + } + if better { + bestLen, bestSuit = counts[s], s } } if bestSuit != game.Suit(255) { return lowest(cardsOfSuit(hand, bestSuit)) } // فقط حکم مانده ⇒ بالاترین حکم را بزن. - return highest(cardsOfSuit(hand, trump)) + return highest(trumps) +} + +// suitCounts تعدادِ کارتِ هر خال در دست. +func suitCounts(hand []game.Card) [4]int { + var c [4]int + for _, x := range hand { + c[x.Suit]++ + } + return c +} + +// higherOut تعدادِ کارت‌های هم‌خالِ بالاتر از rank که هنوز بازی نشده‌اند و در دستِ +// خودِ بات هم نیستند (یعنی دستِ حریف/شریک‌اند). ۰ یعنی این کارت «برنده‌ی خال» است. +func higherOut(seen [4]uint16, hand []game.Card, suit game.Suit, rank game.Rank) int { + n := 0 + for r := rank + 1; r <= game.Ace; r++ { + if seen[suit]&(1<<(r-2)) != 0 { + continue // بازی شده + } + held := false + for _, c := range hand { + if c.Suit == suit && c.Rank == r { + held = true + break + } + } + if !held { + n++ + } + } + return n } // --- کمکی‌ها --- diff --git a/internal/ws/bot_test.go b/internal/ws/bot_test.go index 5fb5704..660d5ec 100644 --- a/internal/ws/bot_test.go +++ b/internal/ws/bot_test.go @@ -12,7 +12,7 @@ func TestBotWinsWhenOpponentLeads(t *testing.T) { // حریف با شاهِ پیک رهبری کرده؛ بات باید با آسِ پیک (کمترین کارتِ برنده) ببرد. hand := []game.Card{card(game.Spades, game.Ace), card(game.Spades, 3), card(game.Clubs, 5)} trick := []game.TrickCard{{Seat: 0, Card: card(game.Spades, game.King)}} - got := botPlay(hand, trick, game.Hearts, game.Spades) + got := botPlay(hand, trick, game.Hearts, game.Spades, [4]uint16{}) if got != card(game.Spades, game.Ace) { t.Errorf("expected S-A to win, got %v", got) } @@ -25,7 +25,7 @@ func TestBotSavesCardWhenPartnerWins(t *testing.T) { {Seat: 0, Card: card(game.Spades, game.Ace)}, {Seat: 1, Card: card(game.Spades, 2)}, } - got := botPlay(hand, trick, game.Hearts, game.Spades) + got := botPlay(hand, trick, game.Hearts, game.Spades, [4]uint16{}) if got != card(game.Spades, 5) { t.Errorf("expected low S-5 (save the King), got %v", got) } @@ -35,7 +35,7 @@ func TestBotTrumpsOpponent(t *testing.T) { // بات پیک ندارد و حریف با آسِ پیک می‌برد ⇒ باید با کمترین حکم بِبُرد. hand := []game.Card{card(game.Hearts, 2), card(game.Hearts, game.King), card(game.Clubs, 9)} trick := []game.TrickCard{{Seat: 0, Card: card(game.Spades, game.Ace)}} - got := botPlay(hand, trick, game.Hearts, game.Spades) + got := botPlay(hand, trick, game.Hearts, game.Spades, [4]uint16{}) if got != card(game.Hearts, 2) { t.Errorf("expected lowest trump H-2, got %v", got) } @@ -48,7 +48,7 @@ func TestBotDiscardsWhenPartnerWinsAndVoid(t *testing.T) { {Seat: 0, Card: card(game.Spades, game.Ace)}, {Seat: 1, Card: card(game.Spades, 2)}, } - got := botPlay(hand, trick, game.Hearts, game.Spades) + got := botPlay(hand, trick, game.Hearts, game.Spades, [4]uint16{}) if got != card(game.Clubs, 4) { t.Errorf("expected low discard C-4 (keep trump), got %v", got) } @@ -57,17 +57,29 @@ func TestBotDiscardsWhenPartnerWinsAndVoid(t *testing.T) { func TestBotLeadsNonTrumpAce(t *testing.T) { // در رهبری، آسِ غیرِحکم را نقد کن. hand := []game.Card{card(game.Clubs, game.Ace), card(game.Diamonds, 7), card(game.Hearts, game.King)} - got := botPlay(hand, nil, game.Hearts, 0) + got := botPlay(hand, nil, game.Hearts, 0, [4]uint16{}) if got != card(game.Clubs, game.Ace) { t.Errorf("expected to lead C-A, got %v", got) } } +func TestBotLeadsBossWhenAceGone(t *testing.T) { + // آسِ گشنیز قبلاً بازی شده ⇒ شاهِ گشنیز حالا برنده‌ی قطعی است؛ باید آن را نقد کند + // (نه کارتِ کم). این رفتار فقط با کارت‌شماری ممکن است. + hand := []game.Card{card(game.Clubs, game.King), card(game.Clubs, 4), card(game.Diamonds, 2)} + var seen [4]uint16 + seen[game.Clubs] |= 1 << (game.Ace - 2) // آسِ گشنیز رو شده + got := botPlay(hand, nil, game.Hearts, 0, seen) + if got != card(game.Clubs, game.King) { + t.Errorf("expected to cash boss C-K (Ace gone), got %v", got) + } +} + func TestBotCannotBeatKeepsLow(t *testing.T) { // حریف با آسِ پیک می‌برد و بات فقط پیکِ پایین دارد ⇒ کمترین را بریزد. hand := []game.Card{card(game.Spades, 4), card(game.Spades, 9)} trick := []game.TrickCard{{Seat: 0, Card: card(game.Spades, game.Ace)}} - got := botPlay(hand, trick, game.Hearts, game.Spades) + got := botPlay(hand, trick, game.Hearts, game.Spades, [4]uint16{}) if got != card(game.Spades, 4) { t.Errorf("expected low S-4, got %v", got) } diff --git a/internal/ws/room.go b/internal/ws/room.go index fef6ba1..33edd5e 100644 --- a/internal/ws/room.go +++ b/internal/ws/room.go @@ -70,6 +70,9 @@ type Room struct { // graceUntil: تا این لحظه هیچ حرکتِ خودکاری انجام نمی‌شود (هماهنگ با انیمیشن‌های کلاینت). graceUntil time.Time chatAt [4]time.Time // آخرین زمانِ چتِ هر جایگاه (ضدِ اسپم) + // seen: حافظه‌ی کارت‌های بازی‌شده در هَندِ جاری (برای هوشِ باتِ کارت‌شمار). + // بیتِ (rank-2) از seen[suit] یعنی آن کارت رو شده. هر هَند ریست می‌شود. + seen [4]uint16 } func newRoom(id string, seats [4]*seatInfo, hub *Hub) *Room { @@ -280,6 +283,7 @@ func (r *Room) handleInput(act roomAction) { r.sendError(act.seat, err.Error()) return } + r.seen = [4]uint16{} // شروعِ فازِ بازیِ هَند ⇒ ریستِ حافظه‌ی کارت‌ها r.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیه‌ی کارت‌ها r.react() case "reshuffle": @@ -340,6 +344,7 @@ func (r *Room) autoChooseTrump(seat int) { return } _ = r.game.ChooseTrump(seat, botChooseTrump(hand)) + r.seen = [4]uint16{} // شروعِ فازِ بازیِ هَند ⇒ ریستِ حافظه‌ی کارت‌ها r.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیه‌ی کارت‌ها } @@ -349,7 +354,7 @@ func (r *Room) autoPlayCard(seat int) { if len(hand) == 0 { return } - choice := botPlay(hand, r.game.Trick, r.game.Trump, r.game.LeadSuit) + choice := botPlay(hand, r.game.Trick, r.game.Trump, r.game.LeadSuit, r.seen) _ = r.playCard(seat, choice) } @@ -359,6 +364,7 @@ func (r *Room) playCard(seat int, card game.Card) error { if err := r.game.PlayCard(seat, card); err != nil { return err } + r.seen[card.Suit] |= 1 << (card.Rank - 2) // ثبت در حافظه‌ی کارت‌شمارِ بات // بُرِش: کارتِ غیرِاولِ دست، از خالِ حکم، در حالی که خالِ زمینه حکم نبوده // ⇒ بازیکن خالِ زمینه را نداشته و با حکم بریده است. if !wasFirst && card.Suit == r.game.Trump && r.game.LeadSuit != r.game.Trump {