// Package poker deals with poker game.
package poker
import (
"fmt"
"sort"
"strings"
"unicode/utf8"
)
// cardValue stores card values for sorting the cards.
// Ace position based values are handled in isStraight() function.
var cardValue = map[string]int{
"2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8,
"9": 9, "10": 10, "J": 11, "Q": 12, "K": 13, "A": 14,
}
// card represents card properties.
type card struct {
value int
suit rune
}
// number of cards in a hand
const numCards = 5
// hand represents hand info.
type hand struct {
orig string
cards []card
kindValue map[int]int // number of values in a hand
kindSuit map[rune]int // number of kinds in a hand
}
// ranker maps checking function for various rank values.
var ranker = map[int]func([]hand) []string{
// Five of a kind is only possible with joker, not implemented.
// 1: fiveOfAKind,
2: straightFlush,
3: fourOfAKind,
4: fullHouse,
5: flush,
6: straight,
7: threeOfAKind,
8: twoPair,
9: onePair,
}
// BestHand returns the best of input hands.
func BestHand(in []string) ([]string, error) {
// fill in hands info
var hands = make([]hand, len(in))
for i, hnd := range in {
hands[i].orig = hnd
hands[i].cards = make([]card, numCards)
hands[i].kindValue = map[int]int{}
hands[i].kindSuit = map[rune]int{}
crds := strings.Split(hnd, " ")
if len(crds) != numCards {
return nil, fmt.Errorf("too few cards")
}
for j, c := range crds {
suit, size := utf8.DecodeLastRuneInString(c)
if suit != '♤' && suit != '♡' && suit != '♢' && suit != '♧' {
return nil, fmt.Errorf("invalid suit")
}
hands[i].cards[j].suit = suit
hands[i].kindSuit[hands[i].cards[j].suit] += 1
var ok bool
hands[i].cards[j].value, ok = cardValue[c[:len(c)-size]]
if !ok {
return nil, fmt.Errorf("unrecognized card rank: %q", c[:size])
}
hands[i].kindValue[hands[i].cards[j].value] += 1
}
}
// sort hands by cards' value
for _, hnd := range hands {
sort.Slice(hnd.cards, func(i, j int) bool {
if hnd.cards[i].value > hnd.cards[j].value {
return true
}
return false
})
}
// search for highest ranked hand(s)
var ret []string
for _, r := range []int{2, 3, 4, 5, 6, 7, 8, 9} {
ret = ranker[r](hands)
if len(ret) > 0 {
break
}
}
if len(ret) != 1 {
ret = tieBreaker(hands)
}
return ret, nil
}
func straightFlush(hands []hand) (ret []string) {
for _, h := range hands {
if len(h.kindSuit) == 1 && h.isStraight() {
ret = append(ret, h.orig)
}
}
return ret
}
func fourOfAKind(hands []hand) (ret []string) {
for _, h := range hands {
for k, v := range h.kindValue {
if v == 4 {
ret = append(ret, h.orig)
h.sortHand([]int{k})
}
}
}
return ret
}
func fullHouse(hands []hand) (ret []string) {
for _, h := range hands {
if len(h.kindValue) == 2 {
ret = append(ret, h.orig)
imp := []int{0, 0}
for k, v := range h.kindValue {
if v == 3 {
imp[0] = k
} else {
imp[1] = k
}
}
h.sortHand(imp)
}
}
return ret
}
func flush(hands []hand) (ret []string) {
for _, h := range hands {
if len(h.kindSuit) == 1 {
ret = append(ret, h.orig)
}
}
return ret
}
func straight(hands []hand) (ret []string) {
for _, h := range hands {
if h.isStraight() {
ret = append(ret, h.orig)
}
}
return ret
}
// isStraight returns true when receiver is straight.
func (h hand) isStraight() bool {
var straight = true
for i, c := range h.cards {
if i == 0 {
continue
}
if c.value != h.cards[i-1].value-1 {
// ace can end a straight
if h.cards[0].value == 14 && c.value == 5 && i == 1 {
continue
}
straight = false
break
}
if h.cards[0].value == 14 && c.value == 2 && i == 4 {
// this is a 5 4 3 2 A straight, switch A to last place
h.cards[0], h.cards[4] = h.cards[4], h.cards[0]
}
}
return straight
}
func threeOfAKind(hands []hand) (ret []string) {
for _, h := range hands {
for k, v := range h.kindValue {
if v == 3 {
ret = append(ret, h.orig)
h.sortHand([]int{k})
}
}
}
return ret
}
func twoPair(hands []hand) (ret []string) {
for _, h := range hands {
if len(h.kindValue) == 3 {
ret = append(ret, h.orig)
h.sortHand(h.pair())
}
}
return ret
}
func onePair(hands []hand) (ret []string) {
for _, h := range hands {
if len(h.kindValue) == 4 {
ret = append(ret, h.orig)
h.sortHand(h.pair())
}
}
return ret
}
// pair returns the pair values for a hand.
func (hnd hand) pair() []int {
var pairs []int
for k, v := range hnd.kindValue {
if v == 2 {
pairs = append(pairs, k)
}
}
sort.Slice(pairs, func(i, j int) bool {
return pairs[i] > pairs[j]
})
return pairs
}
// sortHand sorts receiver based on imp parameter.
// Imp is a descending-sorted slice of values.
func (hnd hand) sortHand(imp []int) {
if len(imp) == 0 {
return
}
sort.Slice(hnd.cards, func(i, j int) bool {
switch {
case hnd.cards[j].value == imp[0]:
return false
case len(imp) == 2 && hnd.cards[j].value == imp[1] &&
hnd.cards[i].value != imp[0]:
return false
}
return true
})
// log.Println(hnd.cards)
}
// tieBreaker handles tie breaking between hands. Input hands has to be sorted.
func tieBreaker(hands []hand) (ret []string) {
for i := 0; i < 5; i++ {
var max int
var newhands []hand
for index, hnd := range hands {
switch {
case hnd.cards[i].value > max:
max = hnd.cards[i].value
ret = []string{hnd.orig}
newhands = hands[index:]
case hnd.cards[i].value == max:
ret = append(ret, hnd.orig)
default:
newhands = append(hands[:index], hands[index+1:]...)
}
// log.Println(index, max, ret)
// log.Println(hands)
}
if len(ret) == 1 {
break
}
hands = newhands
}
return ret
}