feat: smarter bot

This commit is contained in:
2026-07-07 23:23:12 +03:30
parent e7b2e05428
commit 525cd554ef
4 changed files with 112 additions and 37 deletions
+2 -2
View File
@@ -42,9 +42,9 @@ func (r *Repo) FindByID(ctx context.Context, id int64) (*User, error) {
return scan(row) return scan(row)
} }
// Create کاربر جدید می‌سازد. // Create کاربر جدید می‌سازد (با هدیه‌ی خوش‌آمدِ ۱۰۰ سکه).
func (r *Repo) Create(ctx context.Context, mobile string) (*User, error) { 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 { if err != nil {
return nil, err return nil, err
} }
+85 -28
View File
@@ -36,10 +36,11 @@ func botChooseTrump(hand []game.Card) game.Suit {
return best return best
} }
// botPlay کارتی که بات باید بازی کند را برمی‌گرداند. // botPlay کارتی که بات باید بازی کند را برمی‌گرداند. seen حافظه‌ی کارت‌های
func botPlay(hand []game.Card, trick []game.TrickCard, trump, lead game.Suit) game.Card { // بازی‌شده‌ی این هَند است (کارت‌شماری برای تصمیمِ بهتر).
func botPlay(hand []game.Card, trick []game.TrickCard, trump, lead game.Suit, seen [4]uint16) game.Card {
if len(trick) == 0 { if len(trick) == 0 {
return botLead(hand, trump) return botLead(hand, trump, seen)
} }
best := trickBest(trick, trump, lead) 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) // حکمت هم نمی‌برد ⇒ دورریز return lowestDiscard(hand, trump) // حکمت هم نمی‌برد ⇒ دورریز
} }
// botLead راهبردِ رهبریِ دست: آسِ غیرِحکم را نقد کن؛ وگرنه از بلندترین خالِ // botLead راهبردِ رهبریِ دست با کارت‌شماری:
// غیرِحکم کارتِ کم بریز؛ اگر فقط حکم داری، بالاترین حکم را بزن تا حکم‌ها را بکشی. // ۱. کارتِ «برنده‌ی قطعی» (بالاترین کارتِ باقی‌مانده‌ی یک خالِ غیرِحکم) را نقد کن.
func botLead(hand []game.Card, trump game.Suit) game.Card { // ۲. اگر کنترلِ حکم داری (بالاترین حکمِ باقی‌مانده + ۳ حکم یا بیشتر) حکم بکِش.
// آسِ غیرِحکم (برنده‌ی تقریباً قطعی). // ۳. وگرنه از کوتاه‌ترین خالِ غیرِحکم کارتِ کم بریز تا زود void شوی و بعداً بِبُری
var ace *game.Card // (اگر حکم داری)؛ در نبودِ حکم از بلندترین خال کارتِ کم.
for i := range hand { func botLead(hand []game.Card, trump game.Suit, seen [4]uint16) game.Card {
if hand[i].Suit != trump && hand[i].Rank == game.Ace { counts := suitCounts(hand)
c := hand[i]
ace = &c
break
}
}
if ace != nil {
return *ace
}
// بلندترین خالِ غیرِحکم. // ۱. برنده‌ی قطعی: کارتی از خالِ غیرِحکم که هیچ کارتِ بالاتری از آن بیرون نمانده.
var counts [4]int var boss *game.Card
for _, c := range hand { for i := range hand {
counts[c.Suit]++ c := hand[i]
}
bestSuit, bestLen := game.Suit(255), -1
for _, c := range hand {
if c.Suit == trump { if c.Suit == trump {
continue continue
} }
if counts[c.Suit] > bestLen { if higherOut(seen, hand, c.Suit, c.Rank) == 0 {
bestLen, bestSuit = counts[c.Suit], c.Suit 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) { if bestSuit != game.Suit(255) {
return lowest(cardsOfSuit(hand, bestSuit)) 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
} }
// --- کمکی‌ها --- // --- کمکی‌ها ---
+18 -6
View File
@@ -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)} 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)}} 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) { if got != card(game.Spades, game.Ace) {
t.Errorf("expected S-A to win, got %v", got) 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: 0, Card: card(game.Spades, game.Ace)},
{Seat: 1, Card: card(game.Spades, 2)}, {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) { if got != card(game.Spades, 5) {
t.Errorf("expected low S-5 (save the King), got %v", got) 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)} 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)}} 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) { if got != card(game.Hearts, 2) {
t.Errorf("expected lowest trump H-2, got %v", got) 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: 0, Card: card(game.Spades, game.Ace)},
{Seat: 1, Card: card(game.Spades, 2)}, {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) { if got != card(game.Clubs, 4) {
t.Errorf("expected low discard C-4 (keep trump), got %v", got) 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) { func TestBotLeadsNonTrumpAce(t *testing.T) {
// در رهبری، آسِ غیرِحکم را نقد کن. // در رهبری، آسِ غیرِحکم را نقد کن.
hand := []game.Card{card(game.Clubs, game.Ace), card(game.Diamonds, 7), card(game.Hearts, game.King)} 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) { if got != card(game.Clubs, game.Ace) {
t.Errorf("expected to lead C-A, got %v", got) 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) { func TestBotCannotBeatKeepsLow(t *testing.T) {
// حریف با آسِ پیک می‌برد و بات فقط پیکِ پایین دارد ⇒ کمترین را بریزد. // حریف با آسِ پیک می‌برد و بات فقط پیکِ پایین دارد ⇒ کمترین را بریزد.
hand := []game.Card{card(game.Spades, 4), card(game.Spades, 9)} hand := []game.Card{card(game.Spades, 4), card(game.Spades, 9)}
trick := []game.TrickCard{{Seat: 0, Card: card(game.Spades, game.Ace)}} 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) { if got != card(game.Spades, 4) {
t.Errorf("expected low S-4, got %v", got) t.Errorf("expected low S-4, got %v", got)
} }
+7 -1
View File
@@ -70,6 +70,9 @@ type Room struct {
// graceUntil: تا این لحظه هیچ حرکتِ خودکاری انجام نمی‌شود (هماهنگ با انیمیشن‌های کلاینت). // graceUntil: تا این لحظه هیچ حرکتِ خودکاری انجام نمی‌شود (هماهنگ با انیمیشن‌های کلاینت).
graceUntil time.Time graceUntil time.Time
chatAt [4]time.Time // آخرین زمانِ چتِ هر جایگاه (ضدِ اسپم) chatAt [4]time.Time // آخرین زمانِ چتِ هر جایگاه (ضدِ اسپم)
// seen: حافظه‌ی کارت‌های بازی‌شده در هَندِ جاری (برای هوشِ باتِ کارت‌شمار).
// بیتِ (rank-2) از seen[suit] یعنی آن کارت رو شده. هر هَند ریست می‌شود.
seen [4]uint16
} }
func newRoom(id string, seats [4]*seatInfo, hub *Hub) *Room { 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()) r.sendError(act.seat, err.Error())
return return
} }
r.seen = [4]uint16{} // شروعِ فازِ بازیِ هَند ⇒ ریستِ حافظه‌ی کارت‌ها
r.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیه‌ی کارت‌ها r.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیه‌ی کارت‌ها
r.react() r.react()
case "reshuffle": case "reshuffle":
@@ -340,6 +344,7 @@ func (r *Room) autoChooseTrump(seat int) {
return return
} }
_ = r.game.ChooseTrump(seat, botChooseTrump(hand)) _ = r.game.ChooseTrump(seat, botChooseTrump(hand))
r.seen = [4]uint16{} // شروعِ فازِ بازیِ هَند ⇒ ریستِ حافظه‌ی کارت‌ها
r.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیه‌ی کارت‌ها r.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیه‌ی کارت‌ها
} }
@@ -349,7 +354,7 @@ func (r *Room) autoPlayCard(seat int) {
if len(hand) == 0 { if len(hand) == 0 {
return 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) _ = 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 { if err := r.game.PlayCard(seat, card); err != nil {
return err return err
} }
r.seen[card.Suit] |= 1 << (card.Rank - 2) // ثبت در حافظه‌ی کارت‌شمارِ بات
// بُرِش: کارتِ غیرِاولِ دست، از خالِ حکم، در حالی که خالِ زمینه حکم نبوده // بُرِش: کارتِ غیرِاولِ دست، از خالِ حکم، در حالی که خالِ زمینه حکم نبوده
// ⇒ بازیکن خالِ زمینه را نداشته و با حکم بریده است. // ⇒ بازیکن خالِ زمینه را نداشته و با حکم بریده است.
if !wasFirst && card.Suit == r.game.Trump && r.game.LeadSuit != r.game.Trump { if !wasFirst && card.Suit == r.game.Trump && r.game.LeadSuit != r.game.Trump {