221 lines
6.9 KiB
Go
221 lines
6.9 KiB
Go
package main
|
||
|
||
import "math/rand"
|
||
|
||
// thousandNamePool — имена ботов в 1000. Отдельный пул — мужские
|
||
// имена для разнообразия среди игр коллекции.
|
||
var thousandNamePool = []string{
|
||
"Виктор", "Роман", "Максим", "Артём", "Кирилл", "Егор", "Денис", "Павел",
|
||
}
|
||
|
||
func pickThousandBotName(used map[string]bool) string {
|
||
free := make([]string, 0, len(thousandNamePool))
|
||
for _, name := range thousandNamePool {
|
||
if !used[name] {
|
||
free = append(free, name)
|
||
}
|
||
}
|
||
if len(free) == 0 {
|
||
free = thousandNamePool
|
||
}
|
||
return free[rand.Intn(len(free))]
|
||
}
|
||
|
||
func newThousandBotNames(n int) []string {
|
||
used := make(map[string]bool, n)
|
||
names := make([]string, n)
|
||
for i := 0; i < n; i++ {
|
||
name := pickThousandBotName(used)
|
||
used[name] = true
|
||
names[i] = name
|
||
}
|
||
return names
|
||
}
|
||
|
||
// ThousandBot — единственная (без градации уровней) стратегия
|
||
// бота для 1000: оценивает силу руки для торгов, сбрасывает
|
||
// заведомо слабые карты, старается объявлять марьяжи и играть
|
||
// дёшево там, где нельзя выиграть взятку.
|
||
type ThousandBot struct{}
|
||
|
||
// estimateHandStrength грубо оценивает, сколько очков реалистично
|
||
// набрать с такой рукой: сумма очков карт на руке плюс стоимость
|
||
// лучшего марьяжа (марьяж считается один раз, даже если их
|
||
// несколько, — консервативная оценка).
|
||
func estimateHandStrength(hand []Card) int {
|
||
sum := 0
|
||
for _, c := range hand {
|
||
sum += thousandCardValue(c.Rank)
|
||
}
|
||
return sum + bestMarriageValue(hand)
|
||
}
|
||
|
||
// DecideBid решает, поднимать ли ставку в текущих торгах и до
|
||
// какой суммы, либо пасовать. Возвращает (amount, true), если нужно
|
||
// вызвать g.Bid(amount), иначе (_, false) для g.Pass().
|
||
func (ThousandBot) DecideBid(g *ThousandGameState) (int, bool) {
|
||
p := g.current()
|
||
estimate := estimateHandStrength(p.Hand)
|
||
capAmt := g.maxBidFor(g.CurrentBidderIdx)
|
||
|
||
minRequired := g.HighBid + 5
|
||
mandatory := g.bidsMade == 0 && g.CurrentBidderIdx == g.firstBidderIdx
|
||
if mandatory {
|
||
minRequired = 100
|
||
}
|
||
|
||
if minRequired > capAmt {
|
||
if mandatory {
|
||
return 100, true
|
||
}
|
||
return 0, false
|
||
}
|
||
if estimate+10 < minRequired && !mandatory {
|
||
return 0, false
|
||
}
|
||
bid := minRequired
|
||
if bid > capAmt {
|
||
bid = capAmt
|
||
}
|
||
return bid, true
|
||
}
|
||
|
||
// DecideDiscards выбирает 2 карты для сноса сопернику: самые
|
||
// дешёвые карты руки, по возможности не разбивая марьяж.
|
||
func (ThousandBot) DecideDiscards(hand []Card) [2]Card {
|
||
type scored struct {
|
||
card Card
|
||
cost int
|
||
}
|
||
list := make([]scored, len(hand))
|
||
for i, c := range hand {
|
||
cost := thousandCardValue(c.Rank)
|
||
if (c.Rank == King || c.Rank == Queen) && hasMarriage(hand, c.Suit) {
|
||
cost += 1000
|
||
}
|
||
list[i] = scored{card: c, cost: cost}
|
||
}
|
||
for i := 0; i < len(list); i++ {
|
||
for j := i + 1; j < len(list); j++ {
|
||
if list[j].cost < list[i].cost {
|
||
list[i], list[j] = list[j], list[i]
|
||
}
|
||
}
|
||
}
|
||
return [2]Card{list[0].card, list[1].card}
|
||
}
|
||
|
||
// DecidePlay решает, какую карту сыграть в текущей взятке: если
|
||
// ходит первым и есть необъявленный марьяж (не на первой взятке
|
||
// раздачи) — предпочитает объявить самый дорогой; иначе играет
|
||
// самую дешёвую из разрешённых карт (по возможности выигрышную
|
||
// дёшево, а если выиграть нельзя — минимальные потери).
|
||
func (ThousandBot) DecidePlay(g *ThousandGameState) Card {
|
||
actor := g.currentTrickActor()
|
||
hand := g.Players[actor].Hand
|
||
|
||
leading := len(g.CurrentTrick) == 0
|
||
if leading && g.TricksPlayed > 0 {
|
||
bestSuit := Suit(-1)
|
||
bestValue := -1
|
||
for s := Clubs; s <= Spades; s++ {
|
||
if hasMarriage(hand, s) {
|
||
if v := thousandMarriageValue[s]; v > bestValue {
|
||
bestValue = v
|
||
bestSuit = s
|
||
}
|
||
}
|
||
}
|
||
if bestValue >= 0 {
|
||
return Card{Suit: bestSuit, Rank: King}
|
||
}
|
||
}
|
||
|
||
var legal []Card
|
||
for _, c := range hand {
|
||
if leading || g.isValidPlay(hand, c) {
|
||
legal = append(legal, c)
|
||
}
|
||
}
|
||
if len(legal) == 0 {
|
||
legal = hand
|
||
}
|
||
|
||
if leading {
|
||
return weakestByThousandRank(legal)
|
||
}
|
||
|
||
led, _ := g.ledSuit()
|
||
currentBest := g.CurrentTrick[0]
|
||
bestIsTrump := g.TrumpSet && currentBest.Card.Suit == g.TrumpSuit
|
||
for _, play := range g.CurrentTrick[1:] {
|
||
isTrump := g.TrumpSet && play.Card.Suit == g.TrumpSuit
|
||
if (isTrump && !bestIsTrump) ||
|
||
(isTrump == bestIsTrump && play.Card.Suit == currentBest.Card.Suit &&
|
||
thousandRankOrder[play.Card.Rank] > thousandRankOrder[currentBest.Card.Rank]) {
|
||
currentBest = play
|
||
bestIsTrump = isTrump
|
||
}
|
||
}
|
||
|
||
var winners []Card
|
||
for _, c := range legal {
|
||
isTrump := g.TrumpSet && c.Suit == g.TrumpSuit
|
||
wins := false
|
||
switch {
|
||
case isTrump && !bestIsTrump:
|
||
wins = true
|
||
case isTrump == bestIsTrump && c.Suit == currentBest.Card.Suit &&
|
||
thousandRankOrder[c.Rank] > thousandRankOrder[currentBest.Card.Rank]:
|
||
wins = true
|
||
case !bestIsTrump && c.Suit == led && currentBest.Card.Suit != led:
|
||
wins = true
|
||
}
|
||
if wins {
|
||
winners = append(winners, c)
|
||
}
|
||
}
|
||
if len(winners) > 0 {
|
||
return weakestByThousandRank(winners)
|
||
}
|
||
return weakestByThousandRank(legal)
|
||
}
|
||
|
||
// weakestByThousandRank находит карту с наименьшим старшинством в
|
||
// наборе (при равенстве — с меньшей очковой стоимостью).
|
||
func weakestByThousandRank(cards []Card) Card {
|
||
best := cards[0]
|
||
for _, c := range cards[1:] {
|
||
if thousandRankOrder[c.Rank] < thousandRankOrder[best.Rank] ||
|
||
(thousandRankOrder[c.Rank] == thousandRankOrder[best.Rank] && thousandCardValue(c.Rank) < thousandCardValue(best.Rank)) {
|
||
best = c
|
||
}
|
||
}
|
||
return best
|
||
}
|
||
|
||
// PlayFullTurn исполняет одно решение бота (в контексте 1000 —
|
||
// один шаг торгов, снос, или один ход во взятке) — используется
|
||
// при автоматической прокрутке партии.
|
||
func (b ThousandBot) PlayFullTurn(g *ThousandGameState) error {
|
||
switch g.Phase {
|
||
case ThousandPhaseBidding:
|
||
if amount, ok := b.DecideBid(g); ok {
|
||
return g.Bid(amount)
|
||
}
|
||
return g.Pass()
|
||
case ThousandPhaseDiscard:
|
||
discards := b.DecideDiscards(g.Players[g.DeclarerIdx].Hand)
|
||
if err := g.DiscardCard(discards[0]); err != nil {
|
||
return err
|
||
}
|
||
if g.Phase != ThousandPhaseDiscard {
|
||
return nil
|
||
}
|
||
return g.DiscardCard(discards[1])
|
||
case ThousandPhaseTrick:
|
||
card := b.DecidePlay(g)
|
||
return g.PlayCard(card)
|
||
}
|
||
return nil
|
||
}
|