diff --git a/src/entity/hand.go b/src/entity/hand.go index 4dbc8ba..c59e759 100644 --- a/src/entity/hand.go +++ b/src/entity/hand.go @@ -1,5 +1,9 @@ package entity +import ( + "sort" +) + type Hand struct { Public []Card Private []Card @@ -15,3 +19,38 @@ func (hand *Hand) Push(card Card) { func (hand Hand) Len() int { return len(hand.Public) + len(hand.Private) } + +func (hand Hand) AllCards() []Card { + allcards := make([]Card, 0, len(hand.Public)+len(hand.Private)) + for _, cards := range [][]Card{hand.Public, hand.Private} { + allcards = append(allcards, cards...) + } + return allcards +} + +func (hand Hand) Flush() bool { + suit := -1 + for _, card := range hand.AllCards() { + if suit == -1 { + suit = card.Suit + } else if suit != card.Suit { + return false + } + } + return suit != -1 +} + +func (hand Hand) Straight() bool { + cards := hand.AllCards() + values := make([]int, 0, len(cards)) + for _, card := range cards { + values = append(values, card.Value) + } + sort.Ints(values) + for i := 1; i < len(values); i++ { + if values[i-1]+1 != values[i] { + return false + } + } + return len(values) > 1 +} diff --git a/src/entity/hand_test.go b/src/entity/hand_test.go new file mode 100644 index 0000000..7da671f --- /dev/null +++ b/src/entity/hand_test.go @@ -0,0 +1,107 @@ +package entity + +import ( + "testing" +) + +func TestHandAllCards(t *testing.T) { + t.Run("all pub", func(t *testing.T) { + hand := Hand{ + Public: []Card{ + Card{Value: 1}, + Card{Value: 2}, + }, + } + all := hand.AllCards() + if len(all) != 2 { + t.Fatal(all) + } + }) + + t.Run("all pri", func(t *testing.T) { + hand := Hand{ + Private: []Card{ + Card{Value: 1}, + Card{Value: 2}, + }, + } + all := hand.AllCards() + if len(all) != 2 { + t.Fatal(all) + } + }) + + t.Run("both", func(t *testing.T) { + hand := Hand{ + Public: []Card{ + Card{Value: 3}, + Card{Value: 4}, + }, + Private: []Card{ + Card{Value: 1}, + Card{Value: 2}, + }, + } + all := hand.AllCards() + if len(all) != 4 { + t.Fatal(all) + } + }) +} + +func TestHandStraight(t *testing.T) { + cases := map[string]struct { + public []Card + private []Card + want bool + }{ + "empty": { + want: false, + }, + "one public card": { + public: []Card{Card{Value: 3}}, + want: false, + }, + "one private card": { + private: []Card{Card{Value: 3}}, + want: false, + }, + "two public card nonseq": { + public: []Card{Card{Value: 3}, Card{Value: 1}}, + want: false, + }, + "two private card nonseq": { + private: []Card{Card{Value: 3}, Card{Value: 1}}, + want: false, + }, + "one public card one private card nonseq": { + public: []Card{Card{Value: 3}}, + private: []Card{Card{Value: 1}}, + want: false, + }, + "two public card seq": { + public: []Card{Card{Value: 3}, Card{Value: 2}}, + want: true, + }, + "two private card seq": { + private: []Card{Card{Value: 3}, Card{Value: 2}}, + want: true, + }, + "one public card one private card seq": { + public: []Card{Card{Value: 2}}, + private: []Card{Card{Value: 1}}, + want: true, + }, + } + + for name, d := range cases { + c := d + t.Run(name, func(t *testing.T) { + hand := Hand{Public: c.public, Private: c.private} + got := hand.Straight() + if got != c.want { + t.Fatal(c.want, got) + } + }) + } +} diff --git a/src/game/rule/operation/hand.go b/src/game/rule/operation/hand.go new file mode 100644 index 0000000..223337e --- /dev/null +++ b/src/game/rule/operation/hand.go @@ -0,0 +1,135 @@ +package operation + +import ( + "local/sandbox/cards/src/entity" + "sort" +) + +func royalFlush(game *entity.Game, _ interface{}) int { + hand := getHand(game) + if !hand.Flush() || !hand.Straight() { + return 0 + } + for _, card := range hand.AllCards() { + if card.Value < 11 { + return 0 + } + } + return 1 +} + +func straightFlush(game *entity.Game, _ interface{}) int { + hand := getHand(game) + if !hand.Flush() || !hand.Straight() { + return 0 + } + biggest := 0 + for _, card := range hand.AllCards() { + if card.Value > biggest { + biggest = card.Value + } + } + return biggest +} + +func fourOfAKind(game *entity.Game, _ interface{}) int { + hand := getHand(game) + counts := counts(hand) + best := entity.Card{} + high := entity.Card{} + for k, v := range counts { + if v >= 4 && k.Value > best.Value { + best = k + } + if v != 4 && k.Value > high.Value { + high = k + } + } + if best.Value == 0 { + return 0 + } + return best.Value*100 + high.Value +} + +func fullHouse(game *entity.Game, _ interface{}) int { + hand := getHand(game) + counts := counts(hand) + trio := entity.Card{} + duo := entity.Card{} + for k, v := range counts { + if v == 3 && k.Value > trio.Value { + trio = k + } + if v == 2 && k.Value > duo.Value { + duo = k + } + } + if trio.Value == 0 || duo.Value == 0 { + return 0 + } + return trio.Value*100 + duo.Value +} + +func flush(game *entity.Game, _ interface{}) int { + hand := getHand(game) + if !hand.Flush() { + return 0 + } + values := make([]int, 0) + for _, card := range hand.AllCards() { + values = append(values, card.Value) + } + sort.Ints(values) + rank := 0 + for i := len(values) - 1; i >= 0; i-- { + rank = rank*100 + values[i] + } + return rank +} + +func straight(game *entity.Game, _ interface{}) int { + hand := getHand(game) + if !hand.Straight() { + return 0 + } + big := 0 + for _, card := range hand.AllCards() { + if card.Value > big { + big = card.Value + } + } + return big +} + +func threeOfAKind(game *entity.Game, _ interface{}) int { + panic("not impl") +} + +func twoPair(game *entity.Game, _ interface{}) int { + panic("not impl") +} + +func pair(game *entity.Game, _ interface{}) int { + panic("not impl") +} + +func highCard(game *entity.Game, _ interface{}) int { + panic("not impl") +} + +func getHand(game *entity.Game) entity.Hand { + player := game.Players[game.Current.Turn] + return player.Hand +} + +func counts(hand entity.Hand) map[entity.Card]int { + cards := hand.AllCards() + m := make(map[entity.Card]int, len(cards)) + for _, card := range cards { + if _, ok := m[card]; !ok { + m[card] = 0 + } + m[card] += 1 + } + return m +} diff --git a/src/game/rule/operation/hand_test.go b/src/game/rule/operation/hand_test.go new file mode 100644 index 0000000..60d8212 --- /dev/null +++ b/src/game/rule/operation/hand_test.go @@ -0,0 +1,279 @@ +package operation + +import ( + "local/sandbox/cards/src/entity" + "testing" +) + +type testHandCase struct { + public []entity.Card + private []entity.Card + want int +} + +func TestStraight(t *testing.T) { + cases := map[string]testHandCase{ + "no cards": { + want: 0, + }, + "straight big 3": { + public: []entity.Card{ + entity.Card{Value: 11}, + entity.Card{Value: 12}, + entity.Card{Value: 13}, + }, + want: 13, + }, + "straight sm 3": { + public: []entity.Card{ + entity.Card{Value: 1}, + entity.Card{Value: 2}, + entity.Card{Value: 3}, + }, + want: 3, + }, + "straight sm 2": { + public: []entity.Card{ + entity.Card{Value: 1}, + entity.Card{Value: 2}, + }, + want: 2, + }, + "not a straight": { + public: []entity.Card{ + entity.Card{Value: 1}, + entity.Card{Value: 3}, + }, + want: 0, + }, + } + + for name, d := range cases { + testHand(t, name, straight, d.public, d.private, d.want) + } +} + +func TestFlush(t *testing.T) { + cases := map[string]testHandCase{ + "no cards": { + want: 0, + }, + "not a flush": { + public: []entity.Card{ + entity.Card{Value: 1, Suit: 0}, + entity.Card{Value: 2, Suit: 1}, + }, + want: 0, + }, + "big, big, small, small flush": { + public: []entity.Card{ + entity.Card{Value: 12}, + entity.Card{Value: 10}, + entity.Card{Value: 2}, + entity.Card{Value: 1}, + }, + want: 12100201, + }, + "small, small flush": { + public: []entity.Card{ + entity.Card{Value: 2}, + entity.Card{Value: 1}, + }, + want: 201, + }, + "big, small flush": { + public: []entity.Card{ + entity.Card{Value: 11}, + entity.Card{Value: 1}, + }, + want: 1101, + }, + "big, big flush": { + public: []entity.Card{ + entity.Card{Value: 11}, + entity.Card{Value: 12}, + }, + want: 1211, + }, + } + + for name, d := range cases { + testHand(t, name, flush, d.public, d.private, d.want) + } +} + +func TestFullHouse(t *testing.T) { + cases := map[string]testHandCase{ + "no cards": { + want: 0, + }, + "10 > 11": { + public: []entity.Card{ + entity.Card{Value: 11}, + entity.Card{Value: 11}, + entity.Card{Value: 10}, + entity.Card{Value: 10}, + entity.Card{Value: 10}, + }, + want: 1011, + }, + "five of a kind": { + public: []entity.Card{ + entity.Card{Value: 5}, + entity.Card{Value: 5}, + entity.Card{Value: 5}, + entity.Card{Value: 5}, + entity.Card{Value: 5}, + }, + want: 0, + }, + "two pair": { + public: []entity.Card{ + entity.Card{Value: 4}, + entity.Card{Value: 4}, + entity.Card{Value: 5}, + entity.Card{Value: 5}, + entity.Card{Value: 6}, + }, + want: 0, + }, + } + + for name, d := range cases { + testHand(t, name, fullHouse, d.public, d.private, d.want) + } +} + +func TestFourOfAKind(t *testing.T) { + cases := map[string]testHandCase{ + "no cards": { + want: 0, + }, + "one card": { + public: []entity.Card{ + entity.Card{Value: 5}, + }, + want: 0, + }, + "two cards": { + public: []entity.Card{ + entity.Card{Value: 5}, + entity.Card{Value: 5}, + }, + want: 0, + }, + "three cards": { + public: []entity.Card{ + entity.Card{Value: 5}, + entity.Card{Value: 5}, + entity.Card{Value: 5}, + }, + want: 0, + }, + "four cards": { + public: []entity.Card{ + entity.Card{Value: 5}, + entity.Card{Value: 5}, + entity.Card{Value: 5}, + entity.Card{Value: 5}, + }, + want: 500, + }, + "five cards": { + public: []entity.Card{ + entity.Card{Value: 5}, + entity.Card{Value: 5}, + entity.Card{Value: 5}, + entity.Card{Value: 5}, + entity.Card{Value: 5}, + }, + want: 505, + }, + } + + for name, d := range cases { + testHand(t, name, fourOfAKind, d.public, d.private, d.want) + } +} + +func TestStraightFlush(t *testing.T) { + cases := map[string]testHandCase{ + "no cards": { + want: 0, + }, + "one nonroyal cards": { + public: []entity.Card{entity.Card{Value: 5}}, + want: 0, + }, + "one royal cards": { + public: []entity.Card{entity.Card{Value: 12}}, + want: 0, + }, + "two nonroyal cards": { + public: []entity.Card{entity.Card{Value: 5}, entity.Card{Value: 6}}, + want: 6, + }, + "twe royal cards": { + public: []entity.Card{entity.Card{Value: 12}, entity.Card{Value: 13}}, + want: 13, + }, + "twe mix cards": { + public: []entity.Card{entity.Card{Value: 11}, entity.Card{Value: 10}}, + want: 11, + }, + } + + for name, d := range cases { + testHand(t, name, straightFlush, d.public, d.private, d.want) + } +} + +func TestRoyalFlush(t *testing.T) { + cases := map[string]testHandCase{ + "no cards": { + want: 0, + }, + "one nonroyal cards": { + public: []entity.Card{entity.Card{Value: 5}}, + want: 0, + }, + "one royal cards": { + public: []entity.Card{entity.Card{Value: 12}}, + want: 0, + }, + "two nonroyal cards": { + public: []entity.Card{entity.Card{Value: 5}, entity.Card{Value: 6}}, + want: 0, + }, + "twe royal cards": { + public: []entity.Card{entity.Card{Value: 12}, entity.Card{Value: 13}}, + want: 1, + }, + "twe mix cards": { + public: []entity.Card{entity.Card{Value: 11}, entity.Card{Value: 10}}, + want: 0, + }, + } + + for name, d := range cases { + testHand(t, name, royalFlush, d.public, d.private, d.want) + } +} + +func testHand(t *testing.T, name string, foo Int, public []entity.Card, private []entity.Card, want int) { + t.Run(name, func(t *testing.T) { + hand := entity.Hand{ + Public: public, + Private: private, + } + game := &entity.Game{ + Players: []entity.Player{ + entity.Player{Hand: hand}, + }, + } + got := foo(game, nil) + if got != want { + t.Fatal(want, got) + } + }) +} diff --git a/src/game/rule/operation/int.go b/src/game/rule/operation/int.go index c3cd13c..99e20aa 100644 --- a/src/game/rule/operation/int.go +++ b/src/game/rule/operation/int.go @@ -1,5 +1,41 @@ package operation -import "local/sandbox/cards/src/entity" +import ( + "encoding/json" + "errors" + "local/sandbox/cards/src/entity" +) type Int func(*entity.Game, interface{}) int + +var intStringified = map[string]Int{ + "royalFlush": royalFlush, + "straightFlush": straightFlush, + "fourOfAKind": fourOfAKind, + "fullHouse": fullHouse, + "flush": flush, + "straight": straight, + "threeOfAKind": threeOfAKind, + "twoPair": twoPair, + "pair": pair, + "highCard": highCard, +} + +func (foo *Int) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + + return foo.FromString(s) +} + +func (foo *Int) FromString(s string) error { + for k, v := range intStringified { + if k == s { + *foo = v + return nil + } + } + return errors.New("unknown int method " + s) +}