feat: smarter bot
This commit is contained in:
@@ -243,6 +243,9 @@ func (g *Game) trickWinner() int {
|
|||||||
return best.Seat
|
return best.Seat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Beats نسخهی عمومیِ beats برای استفادهی هوشِ باتِ لایهی ws.
|
||||||
|
func Beats(a, b Card, trump, lead Suit) bool { return beats(a, b, trump, lead) }
|
||||||
|
|
||||||
// beats مشخص میکند آیا a کارت b را میبرد (با توجه به حکم و خال زمینه).
|
// beats مشخص میکند آیا a کارت b را میبرد (با توجه به حکم و خال زمینه).
|
||||||
func beats(a, b Card, trump, lead Suit) bool {
|
func beats(a, b Card, trump, lead Suit) bool {
|
||||||
aTrump := a.Suit == trump
|
aTrump := a.Suit == trump
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-31
@@ -333,50 +333,23 @@ func (r *Room) autoMove(seat int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// autoChooseTrump خالی را انتخاب میکند که بازیکن بیشترین کارت از آن را دارد.
|
// autoChooseTrump حکم را با راهبردِ باتِ هوشمند (طول + قدرتِ خال) انتخاب میکند.
|
||||||
func (r *Room) autoChooseTrump(seat int) {
|
func (r *Room) autoChooseTrump(seat int) {
|
||||||
hand := r.game.Hand(seat)
|
hand := r.game.Hand(seat)
|
||||||
if len(hand) == 0 {
|
if len(hand) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var counts [4]int
|
_ = r.game.ChooseTrump(seat, botChooseTrump(hand))
|
||||||
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.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیهی کارتها
|
r.graceUntil = time.Now().Add(r.dealDelay) // انیمیشنِ پخشِ بقیهی کارتها
|
||||||
}
|
}
|
||||||
|
|
||||||
// autoPlayCard یک کارت مجاز (پایینترین کارتِ خالِ زمینه، وگرنه پایینترین کارت) بازی میکند.
|
// autoPlayCard با هوشِ باتِ اکتشافی یک کارتِ مجاز و راهبردی بازی میکند.
|
||||||
func (r *Room) autoPlayCard(seat int) {
|
func (r *Room) autoPlayCard(seat int) {
|
||||||
hand := r.game.Hand(seat)
|
hand := r.game.Hand(seat)
|
||||||
if len(hand) == 0 {
|
if len(hand) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var choice game.Card
|
choice := botPlay(hand, r.game.Trick, r.game.Trump, r.game.LeadSuit)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = r.playCard(seat, choice)
|
_ = r.playCard(seat, choice)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user