188 lines
5.9 KiB
Go
188 lines
5.9 KiB
Go
package main
|
||
|
||
import "sort"
|
||
|
||
// PokerHandCategory — категория покерной комбинации (чем больше,
|
||
// тем сильнее).
|
||
type PokerHandCategory int
|
||
|
||
const (
|
||
PokerHighCard PokerHandCategory = iota
|
||
PokerPair
|
||
PokerTwoPair
|
||
PokerThreeOfAKind
|
||
PokerStraight
|
||
PokerFlush
|
||
PokerFullHouse
|
||
PokerFourOfAKind
|
||
PokerStraightFlush
|
||
)
|
||
|
||
// PokerHandValue — сравнимая оценка одной пятикарточной комбинации:
|
||
// категория плюс до пяти значений для сравнения кикеров, в порядке
|
||
// значимости (не все пять слотов задействованы каждой категорией —
|
||
// неиспользуемые остаются нулевыми, что не мешает сравнению, так
|
||
// как категория и более значимые слоты уже решают дело раньше).
|
||
type PokerHandValue struct {
|
||
Category PokerHandCategory
|
||
Tiebreak [5]int
|
||
}
|
||
|
||
// ComparePokerHands возвращает 1, если a сильнее b, -1, если b
|
||
// сильнее a, и 0 при полном равенстве (сплит банка).
|
||
func ComparePokerHands(a, b PokerHandValue) int {
|
||
if a.Category != b.Category {
|
||
if a.Category > b.Category {
|
||
return 1
|
||
}
|
||
return -1
|
||
}
|
||
for i := 0; i < 5; i++ {
|
||
if a.Tiebreak[i] != b.Tiebreak[i] {
|
||
if a.Tiebreak[i] > b.Tiebreak[i] {
|
||
return 1
|
||
}
|
||
return -1
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// pokerRankValue переводит ранг карты в покерное значение (2-14,
|
||
// туз всегда старший) — в отличие от Rank.Value(), используемого
|
||
// для Тонка/Дурака, где туз равен 1.
|
||
func pokerRankValue(r Rank) int {
|
||
if r == Ace {
|
||
return 14
|
||
}
|
||
return int(r)
|
||
}
|
||
|
||
// EvaluateBest5 перебирает все сочетания по 5 карт из переданных
|
||
// (обычно 5 или 7 — например, 2 карты в руке + 5 общих) и возвращает
|
||
// оценку наилучшей возможной комбинации.
|
||
func EvaluateBest5(cards []Card) PokerHandValue {
|
||
if len(cards) == 5 {
|
||
return evaluate5(cards)
|
||
}
|
||
best := PokerHandValue{}
|
||
first := true
|
||
combinations5(cards, func(combo []Card) {
|
||
v := evaluate5(combo)
|
||
if first || ComparePokerHands(v, best) > 0 {
|
||
best = v
|
||
first = false
|
||
}
|
||
})
|
||
return best
|
||
}
|
||
|
||
// combinations5 вызывает fn для каждого сочетания по 5 карт из cards.
|
||
func combinations5(cards []Card, fn func(combo []Card)) {
|
||
n := len(cards)
|
||
idx := make([]int, 5)
|
||
for i := range idx {
|
||
idx[i] = i
|
||
}
|
||
for {
|
||
combo := make([]Card, 5)
|
||
for i, ix := range idx {
|
||
combo[i] = cards[ix]
|
||
}
|
||
fn(combo)
|
||
|
||
// следующая комбинация индексов (стандартный алгоритм перебора сочетаний)
|
||
i := 4
|
||
for i >= 0 && idx[i] == n-5+i {
|
||
i--
|
||
}
|
||
if i < 0 {
|
||
return
|
||
}
|
||
idx[i]++
|
||
for j := i + 1; j < 5; j++ {
|
||
idx[j] = idx[j-1] + 1
|
||
}
|
||
}
|
||
}
|
||
|
||
// evaluate5 оценивает ровно пять карт.
|
||
func evaluate5(cards []Card) PokerHandValue {
|
||
values := make([]int, 5)
|
||
suitCounts := map[Suit]int{}
|
||
for i, c := range cards {
|
||
values[i] = pokerRankValue(c.Rank)
|
||
suitCounts[c.Suit]++
|
||
}
|
||
sort.Sort(sort.Reverse(sort.IntSlice(values)))
|
||
|
||
isFlush := len(suitCounts) == 1
|
||
straightHigh, isStraight := detectStraight(values)
|
||
|
||
if isStraight && isFlush {
|
||
return PokerHandValue{Category: PokerStraightFlush, Tiebreak: [5]int{straightHigh}}
|
||
}
|
||
|
||
// группируем по количеству повторений ранга
|
||
countByRank := map[int]int{}
|
||
for _, v := range values {
|
||
countByRank[v]++
|
||
}
|
||
type rankCount struct{ rank, count int }
|
||
var groups []rankCount
|
||
for r, c := range countByRank {
|
||
groups = append(groups, rankCount{r, c})
|
||
}
|
||
sort.Slice(groups, func(i, j int) bool {
|
||
if groups[i].count != groups[j].count {
|
||
return groups[i].count > groups[j].count
|
||
}
|
||
return groups[i].rank > groups[j].rank
|
||
})
|
||
|
||
switch {
|
||
case groups[0].count == 4:
|
||
kicker := groups[1].rank
|
||
return PokerHandValue{Category: PokerFourOfAKind, Tiebreak: [5]int{groups[0].rank, kicker}}
|
||
case groups[0].count == 3 && groups[1].count == 2:
|
||
return PokerHandValue{Category: PokerFullHouse, Tiebreak: [5]int{groups[0].rank, groups[1].rank}}
|
||
case isFlush:
|
||
var tb [5]int
|
||
copy(tb[:], values)
|
||
return PokerHandValue{Category: PokerFlush, Tiebreak: tb}
|
||
case isStraight:
|
||
return PokerHandValue{Category: PokerStraight, Tiebreak: [5]int{straightHigh}}
|
||
case groups[0].count == 3:
|
||
kickers := []int{groups[1].rank, groups[2].rank}
|
||
return PokerHandValue{Category: PokerThreeOfAKind, Tiebreak: [5]int{groups[0].rank, kickers[0], kickers[1]}}
|
||
case groups[0].count == 2 && groups[1].count == 2:
|
||
hiPair, loPair := groups[0].rank, groups[1].rank
|
||
if loPair > hiPair {
|
||
hiPair, loPair = loPair, hiPair
|
||
}
|
||
return PokerHandValue{Category: PokerTwoPair, Tiebreak: [5]int{hiPair, loPair, groups[2].rank}}
|
||
case groups[0].count == 2:
|
||
return PokerHandValue{Category: PokerPair, Tiebreak: [5]int{groups[0].rank, groups[1].rank, groups[2].rank, groups[3].rank}}
|
||
default:
|
||
var tb [5]int
|
||
copy(tb[:], values)
|
||
return PokerHandValue{Category: PokerHighCard, Tiebreak: tb}
|
||
}
|
||
}
|
||
|
||
// detectStraight проверяет пять ОТСОРТИРОВАННЫХ ПО УБЫВАНИЮ
|
||
// покерных значений на стрит, включая особый случай "колеса"
|
||
// (A-5-4-3-2, где туз считается младшей картой, а старшая карта
|
||
// стрита — пятёрка).
|
||
func detectStraight(sortedDesc []int) (high int, ok bool) {
|
||
// особый случай колеса: A,5,4,3,2
|
||
if sortedDesc[0] == 14 && sortedDesc[1] == 5 && sortedDesc[2] == 4 && sortedDesc[3] == 3 && sortedDesc[4] == 2 {
|
||
return 5, true
|
||
}
|
||
for i := 0; i < 4; i++ {
|
||
if sortedDesc[i]-sortedDesc[i+1] != 1 {
|
||
return 0, false
|
||
}
|
||
}
|
||
return sortedDesc[0], true
|
||
}
|