feat: smarter bot
This commit is contained in:
+85
-28
@@ -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
|
||||
}
|
||||
|
||||
// --- کمکیها ---
|
||||
|
||||
+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)}
|
||||
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)
|
||||
}
|
||||
|
||||
+7
-1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user