diff --git a/internal/game/hokm.go b/internal/game/hokm.go index 6da7d6c..c9ba7a8 100644 --- a/internal/game/hokm.go +++ b/internal/game/hokm.go @@ -243,6 +243,9 @@ func (g *Game) trickWinner() int { return best.Seat } +// Beats نسخه‌ی عمومیِ beats برای استفاده‌ی هوشِ باتِ لایه‌ی ws. +func Beats(a, b Card, trump, lead Suit) bool { return beats(a, b, trump, lead) } + // beats مشخص می‌کند آیا a کارت b را می‌برد (با توجه به حکم و خال زمینه). func beats(a, b Card, trump, lead Suit) bool { aTrump := a.Suit == trump diff --git a/internal/ws/bot.go b/internal/ws/bot.go new file mode 100644 index 0000000..0476e2e --- /dev/null +++ b/internal/ws/bot.go @@ -0,0 +1,179 @@ +package ws + +import "hakemsho/internal/game" + +// هوشِ باتِ حکم — تصمیم‌گیریِ اکتشافی (heuristic) که فقط از دستِ خودِ بات و +// کارت‌های روی زمین استفاده می‌کند (بدونِ دیدنِ دستِ دیگران؛ بازیِ منصفانه). +// +// راهبردها: +// - انتخابِ حکم: خالی که هم بلند است و هم کارتِ قوی دارد. +// - رهبریِ دست: آسِ غیرِحکم را نقد کن؛ وگرنه از بلندترین خالِ غیرِحکم کارتِ کم بریز. +// - دنبال‌کردن: اگر شریک می‌برد کارتِ کم بریز؛ اگر حریف می‌برد با کمترین کارتِ +// برنده ببر؛ اگر خالِ زمینه را نداری و حریف می‌برد با کمترین حکم بِبُر. + +// botChooseTrump خالِ حکم را بر اساسِ طول + قدرتِ کارت‌ها انتخاب می‌کند. +func botChooseTrump(hand []game.Card) game.Suit { + var score [4]float64 + for _, c := range hand { + score[c.Suit] += 1 + switch c.Rank { + case game.Ace: + score[c.Suit] += 2.0 + case game.King: + score[c.Suit] += 1.3 + case game.Queen: + score[c.Suit] += 0.7 + case game.Jack: + score[c.Suit] += 0.3 + } + } + best, bestScore := game.Suit(0), -1.0 + for s := game.Suit(0); s < 4; s++ { + if score[s] > bestScore { + bestScore, best = score[s], s + } + } + return best +} + +// botPlay کارتی که بات باید بازی کند را برمی‌گرداند. +func botPlay(hand []game.Card, trick []game.TrickCard, trump, lead game.Suit) game.Card { + if len(trick) == 0 { + return botLead(hand, trump) + } + + best := trickBest(trick, trump, lead) + // جایگاه‌ها متناوب‌اند؛ کارتی که دو نوبت قبل انداخته شده متعلق به شریکِ باتِ + // در نوبت است. اگر آن (یا هر کارتِ شریک) بهترین کارتِ فعلی باشد، شریک می‌برد. + partnerWins := false + if len(trick) >= 2 { + partnerWins = best.Seat == trick[len(trick)-2].Seat + } + + followers := cardsOfSuit(hand, lead) + if len(followers) > 0 { + if partnerWins { + return lowest(followers) // شریک می‌برد ⇒ کارتِ کم بریز + } + if w, ok := lowestWinner(followers, best.Card, trump, lead); ok { + return w // با کمترین کارتِ برنده ببر + } + return lowest(followers) // نمی‌توانی ببری ⇒ کارتِ کم + } + + // خالِ زمینه را نداریم (void). + trumps := cardsOfSuit(hand, trump) + if partnerWins || len(trumps) == 0 { + return lowestDiscard(hand, trump) + } + if w, ok := lowestWinner(trumps, best.Card, trump, lead); ok { + return w // با کمترین حکمِ برنده بِبُر + } + 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 + } + + // بلندترین خالِ غیرِحکم. + var counts [4]int + for _, c := range hand { + counts[c.Suit]++ + } + bestSuit, bestLen := game.Suit(255), -1 + for _, c := range hand { + if c.Suit == trump { + continue + } + if counts[c.Suit] > bestLen { + bestLen, bestSuit = counts[c.Suit], c.Suit + } + } + if bestSuit != game.Suit(255) { + return lowest(cardsOfSuit(hand, bestSuit)) + } + // فقط حکم مانده ⇒ بالاترین حکم را بزن. + return highest(cardsOfSuit(hand, trump)) +} + +// --- کمکی‌ها --- + +func trickBest(trick []game.TrickCard, trump, lead game.Suit) game.TrickCard { + best := trick[0] + for _, tc := range trick[1:] { + if game.Beats(tc.Card, best.Card, trump, lead) { + best = tc + } + } + return best +} + +func cardsOfSuit(hand []game.Card, s game.Suit) []game.Card { + var out []game.Card + for _, c := range hand { + if c.Suit == s { + out = append(out, c) + } + } + return out +} + +func lowest(cards []game.Card) game.Card { + best := cards[0] + for _, c := range cards[1:] { + if c.Rank < best.Rank { + best = c + } + } + return best +} + +func highest(cards []game.Card) game.Card { + best := cards[0] + for _, c := range cards[1:] { + if c.Rank > best.Rank { + best = c + } + } + return best +} + +// lowestWinner کمترین کارتِ candidate که کارتِ فعلیِ برنده را می‌برد. +func lowestWinner(candidates []game.Card, cur game.Card, trump, lead game.Suit) (game.Card, bool) { + var best game.Card + found := false + for _, c := range candidates { + if game.Beats(c, cur, trump, lead) && (!found || c.Rank < best.Rank) { + best, found = c, true + } + } + return best, found +} + +// lowestDiscard کمترین کارتِ غیرِحکم را برای دورریز برمی‌گرداند؛ اگر فقط حکم مانده، +// کمترین حکم. +func lowestDiscard(hand []game.Card, trump game.Suit) game.Card { + var nonTrump []game.Card + for _, c := range hand { + if c.Suit != trump { + nonTrump = append(nonTrump, c) + } + } + if len(nonTrump) > 0 { + return lowest(nonTrump) + } + return lowest(hand) +} diff --git a/internal/ws/bot_test.go b/internal/ws/bot_test.go new file mode 100644 index 0000000..5fb5704 --- /dev/null +++ b/internal/ws/bot_test.go @@ -0,0 +1,86 @@ +package ws + +import ( + "testing" + + "hakemsho/internal/game" +) + +func card(s game.Suit, r game.Rank) game.Card { return game.Card{Suit: s, Rank: r} } + +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) + if got != card(game.Spades, game.Ace) { + t.Errorf("expected S-A to win, got %v", got) + } +} + +func TestBotSavesCardWhenPartnerWins(t *testing.T) { + // شریک (صندلی۰) با آسِ پیک برنده است؛ باتِ صندلی۲ باید کمترین پیک را بریزد. + hand := []game.Card{card(game.Spades, game.King), card(game.Spades, 5)} + trick := []game.TrickCard{ + {Seat: 0, Card: card(game.Spades, game.Ace)}, + {Seat: 1, Card: card(game.Spades, 2)}, + } + got := botPlay(hand, trick, game.Hearts, game.Spades) + if got != card(game.Spades, 5) { + t.Errorf("expected low S-5 (save the King), got %v", got) + } +} + +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) + if got != card(game.Hearts, 2) { + t.Errorf("expected lowest trump H-2, got %v", got) + } +} + +func TestBotDiscardsWhenPartnerWinsAndVoid(t *testing.T) { + // شریک می‌برد و بات خالِ زمینه را ندارد ⇒ حکم را هدر ندهد، کارتِ کم بریزد. + hand := []game.Card{card(game.Hearts, game.King), card(game.Clubs, 4), card(game.Clubs, 9)} + trick := []game.TrickCard{ + {Seat: 0, Card: card(game.Spades, game.Ace)}, + {Seat: 1, Card: card(game.Spades, 2)}, + } + got := botPlay(hand, trick, game.Hearts, game.Spades) + if got != card(game.Clubs, 4) { + t.Errorf("expected low discard C-4 (keep trump), got %v", got) + } +} + +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) + if got != card(game.Clubs, game.Ace) { + t.Errorf("expected to lead C-A, 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) + if got != card(game.Spades, 4) { + t.Errorf("expected low S-4, got %v", got) + } +} + +func TestBotChooseTrumpPrefersStrongLongSuit(t *testing.T) { + // خشت: ۴ کارت اما ضعیف؛ گشنیز: ۳ کارت اما آس+شاه. باید گشنیز را حکم کند اگر + // امتیازش بیشتر شود — اینجا خشت ۴تایی برنده است. تستِ ساده: بلندترینِ قوی. + hand := []game.Card{ + card(game.Diamonds, 2), card(game.Diamonds, 3), card(game.Diamonds, 4), card(game.Diamonds, 5), + card(game.Clubs, game.Ace), + } + if got := botChooseTrump(hand); got != game.Diamonds { + t.Errorf("expected Diamonds (longest), got %v", got) + } +} diff --git a/internal/ws/room.go b/internal/ws/room.go index 90410aa..fef6ba1 100644 --- a/internal/ws/room.go +++ b/internal/ws/room.go @@ -333,50 +333,23 @@ func (r *Room) autoMove(seat int) { } } -// autoChooseTrump خالی را انتخاب می‌کند که بازیکن بیشترین کارت از آن را دارد. +// autoChooseTrump حکم را با راهبردِ باتِ هوشمند (طول + قدرتِ خال) انتخاب می‌کند. func (r *Room) autoChooseTrump(seat int) { hand := r.game.Hand(seat) if len(hand) == 0 { return } - var counts [4]int - for _, c := range hand { - counts[c.Suit]++ - } - best, bestN := hand[0].Suit, -1 - for s := game.Suit(0); s < 4; s++ { - if counts[s] > bestN { - bestN, best = counts[s], s - } - } - _ = r.game.ChooseTrump(seat, best) + _ = r.game.ChooseTrump(seat, botChooseTrump(hand)) r.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیه‌ی کارت‌ها } -// autoPlayCard یک کارت مجاز (پایین‌ترین کارتِ خالِ زمینه، وگرنه پایین‌ترین کارت) بازی می‌کند. +// autoPlayCard با هوشِ باتِ اکتشافی یک کارتِ مجاز و راهبردی بازی می‌کند. func (r *Room) autoPlayCard(seat int) { hand := r.game.Hand(seat) if len(hand) == 0 { return } - var choice game.Card - found := false - if len(r.game.Trick) > 0 { // خالِ زمینه تعیین شده - lead := r.game.LeadSuit - for _, c := range hand { - if c.Suit == lead && (!found || c.Rank < choice.Rank) { - choice, found = c, true - } - } - } - if !found { - choice = hand[0] - for _, c := range hand { - if c.Rank < choice.Rank { - choice = c - } - } - } + choice := botPlay(hand, r.game.Trick, r.game.Trump, r.game.LeadSuit) _ = r.playCard(seat, choice) }