feat: smarter bot
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user