commit 9cfa4b826cec1df11276d5432173704b3f18f946 Author: joshua Date: Mon Jul 24 16:44:40 2023 +0200 Init diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..981fc65 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.clement.tel/joshua/09_deck + +go 1.20 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/main.go b/main.go new file mode 100644 index 0000000..81550d8 --- /dev/null +++ b/main.go @@ -0,0 +1,214 @@ +package deck + +import ( + "fmt" + "math/rand" + "sort" +) + +//go:generate stringer -type=Suit,Rank + +// Suit represents the suit of a card: +// one of Spades, Diamonds, Clubs, Hearts, +// or Joker +type Suit uint8 + +const ( + Spades Suit = iota + Diamonds + Clubs + Hearts + Joker +) + +// Rank represents the rank of a card: +// Ace, Two... Ten, Jack... King +type Rank uint8 + +const ( + _ Rank = iota + Ace + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + Jack + Queen + King +) + +// Card contains both a Suit and a Rank. +// In the special case that Suit=Joker, +// Rank is often not relevant. +type Card struct { + Suit Suit + Rank Rank +} + +// Formats the Card type as a string +func (c Card) String() string { + if c.Suit == Joker { + return "Joker" + } + return fmt.Sprintf("%s of %s", c.Rank, c.Suit) +} + +// Generates a new deck (slice of Cards) which contains +// a full playing card deck sorted by Suit with +// no jokers. +// The function accepts functional arguments to +// modify the deck during creation +func New(opts ...func([]Card) []Card) []Card { + deck := make([]Card, 52, 52) + i := 0 + for s := 0; s < 4; s++ { + for v := 1; v < 14; v++ { + deck[i] = Card{Suit: Suit(s), Rank: Rank(v)} + i++ + } + } + for _, opt := range opts { + deck = opt(deck) + } + return deck +} + +func lessBySuit(c1 Card, c2 Card) bool { + if c1.Suit < c2.Suit { + return true + } + if c1.Suit == c2.Suit && c1.Rank < c2.Rank { + return true + } + return false +} + +func lessByRank(c1 Card, c2 Card) bool { + if c1.Rank < c2.Rank { + return true + } + if c1.Rank == c2.Rank && c1.Suit < c2.Suit { + return true + } + return false +} + +// Sorts the deck (slice of Cards) by Suit, then by Rank +func SortBySuit(deck []Card) []Card { + sort.Slice(deck, func(i, j int) bool { + return lessBySuit(deck[i], deck[j]) + }) + return deck +} + +// Sorts the deck (slice of Cards) by Rank, then by Suit +func SortByRank(deck []Card) []Card { + sort.Slice(deck, func(i, j int) bool { + return lessByRank(deck[i], deck[j]) + }) + return deck +} + +// This returns a closure that adds n joker cards to the end +// of a deck (slice of Cards). The returned function +// can be passed as an option into New() +func AddNJokers(n int) func(deck []Card) []Card { + return func(deck []Card) []Card { + toAdd := make([]Card, n, n) + for i, _ := range toAdd { + toAdd[i] = Card{Suit: Joker} + } + return append(deck, toAdd...) + } +} + +// Shuffle randomizes the order of cards in a slice +func Shuffle(deck []Card) []Card { + rand.Shuffle(len(deck), func(i, j int) { + deck[i], deck[j] = deck[j], deck[i] + }) + return deck +} + +// RemoveCards takes Card arguments, and returns a closure +// that will remove all Cards matching one of the arguments +// from the deck (slice of Cards). +// The returned function can be passed as an option into New() +func RemoveCards(cards ...Card) func([]Card) []Card { + return func(deck []Card) []Card { + output := make([]Card, 0, len(deck)) + badKeys := make(map[Card]struct{}) + for _, card := range cards { + badKeys[card] = struct{}{} + } + for _, card := range deck { + if _, ok := badKeys[card]; !ok { + output = append(output, card) + } + } + return output + } +} + +// RemoveSuit takes Suit arguments, and returns a closure +// that will remove all Cards with a Suit equal to one of the +// arguments from the deck (slice of Cards). +// The returned function can be passed as an option into New() +func RemoveSuit(suits ...Suit) func([]Card) []Card { + return func(deck []Card) []Card { + output := make([]Card, 0, len(deck)) + badKeys := make(map[Suit]struct{}) + for _, suit := range suits { + badKeys[suit] = struct{}{} + } + for _, card := range deck { + if _, ok := badKeys[card.Suit]; !ok { + output = append(output, card) + } + } + return output + } +} + +// RemoveRank takes Rank arguments, and returns a closure +// that will remove all Cards with a Rank equal to one of the +// arguments from the deck (slice of Cards). +// The returned function can be passed as an option into New() +func RemoveRank(ranks ...Rank) func([]Card) []Card { + return func(deck []Card) []Card { + output := make([]Card, 0, len(deck)) + badKeys := make(map[Rank]struct{}) + for _, rank := range ranks { + badKeys[rank] = struct{}{} + } + for _, card := range deck { + if _, ok := badKeys[card.Rank]; !ok { + output = append(output, card) + } + } + return output + } +} + +// AddDeck appends a default deck to the deck passed in by the user +// (slice ef Cards), where the default deck is that created by New() +// with no options. +func AddDeck(deck []Card) []Card { + newDeck := New() + deck = append(deck, newDeck...) + return deck +} + +// Draw takes the top card from deck and appends it to hand, returning the +// hand and the deck as they are after the card is moved. +func Draw(hand []Card, deck []Card) (newHand []Card, newDeck []Card) { + topCard := deck[0] + newHand = append(hand, topCard) + newDeck = deck[1:] + return newHand, newDeck +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..628a0ed --- /dev/null +++ b/main_test.go @@ -0,0 +1,129 @@ +package deck + +import ( + "testing" + "fmt" +) + +func showDeck(d []Card) { + fmt.Println("Showing whole deck") + for _, card := range d { + fmt.Println(card) + } +} + +func TestPrintCard(t *testing.T) { + tests := []struct{ + input Card + want string + }{ + {Card{Hearts, Queen}, "Queen of Hearts"}, + {Card{Suit: Joker}, "Joker"}, + } + for _, test := range tests { + if test.input.String() != test.want { + t.Errorf("Got %s; wanted %s", test.input.String(), test.want) + } + } +} + +func TestNew(t *testing.T) { + d := New() + c := Card{Diamonds, Six} + if d[18] != c { + t.Errorf("Deck's 5th card should be %s, is %s", c, d[18]) + showDeck(d) + } +} + +func TestSortByRank(t *testing.T) { + d := New(SortByRank) + c := Card{Clubs, Five} + if d[18] != c { + t.Errorf("Deck's 18th card should be %s, is %s", c, d[18]) + showDeck(d) + } +} + +func TestSortBySuit(t *testing.T) { + d := New() + d = SortByRank(d) + d = SortBySuit(d) + c := Card{Diamonds, Six} + if d[18] != c { + t.Errorf("Deck's 5th card should be %s, is %s", c, d[18]) + showDeck(d) + } +} + +func TestAddJokers(t *testing.T) { + d := New() + initLength := len(d) + d = AddNJokers(17)(d) + if initLength + 17 != len(d) { + t.Errorf("Wrong number of cards in deck after adding Jokers") + showDeck(d) + } +} + +func TestRemoveCard(t *testing.T) { + d := New(RemoveCards(Card{Spades, Ace})) + for _, card := range d { + if card == (Card{Spades, Ace}) { + t.Error("The ace of spades is still in the deck after calling RemoveCard(Card{Spades, Ace})") + } + } + before := len(d) + d = RemoveCards(Card{Clubs, Jack})(d) + if len(d) != (before - 1) { + t.Error("Remove() didn't reduce card count by 1") + } +} + +func TestRemoveRank(t *testing.T) { + d := New(RemoveRank(Seven)) + for _, card := range d { + if card.Rank == Seven { + t.Error("There are still Sevens in the deck after calling RemoveRank(7)") + } + } +} + +func TestRemoveSuit(t *testing.T) { + d := New(RemoveSuit(Hearts)) + for _, card := range d { + if card.Suit == Hearts { + t.Error("There are still Hearts in the deck after calling RemoveSuit(Hearts)") + } + } +} + +func TestAddDeck(t *testing.T) { + d := New() + before := len(d) + d = AddDeck(d) + if len(d) != before*2 { + t.Error("AddDeck didn't double default deck size") + } +} + +func TestDraw(t *testing.T) { + d := New() + lenBefore := len(d) + firstTwo := d[0:2] + hand := make([]Card, 0, 5) + hand, d = Draw(hand, d) + hand, d = Draw(hand, d) + if firstTwo[0] != hand[0] { + t.Errorf("The first drawn card (got %s) should be the first in the deck (was %s)\n", + hand[0], firstTwo[0]) + } + if firstTwo[1] != hand[1] { + t.Errorf("The second drawn card (got %s) should be the second in the deck (was %s)\n", + hand[0], firstTwo[0]) + } + if len(d) != lenBefore - 2 { + t.Errorf(`Expected length of deck to be reduced by two when calling Draw() twice, +length before is %d, and length after is %d`, lenBefore, len(d)) + } +} diff --git a/suit_string.go b/suit_string.go new file mode 100644 index 0000000..48da3ac --- /dev/null +++ b/suit_string.go @@ -0,0 +1,57 @@ +// Code generated by "stringer -type=Suit,Rank"; DO NOT EDIT. + +package deck + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Spades-0] + _ = x[Diamonds-1] + _ = x[Clubs-2] + _ = x[Hearts-3] + _ = x[Joker-4] +} + +const _Suit_name = "SpadesDiamondsClubsHeartsJoker" + +var _Suit_index = [...]uint8{0, 6, 14, 19, 25, 30} + +func (i Suit) String() string { + if i >= Suit(len(_Suit_index)-1) { + return "Suit(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Suit_name[_Suit_index[i]:_Suit_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Ace-1] + _ = x[Two-2] + _ = x[Three-3] + _ = x[Four-4] + _ = x[Five-5] + _ = x[Six-6] + _ = x[Seven-7] + _ = x[Eight-8] + _ = x[Nine-9] + _ = x[Ten-10] + _ = x[Jack-11] + _ = x[Queen-12] + _ = x[King-13] +} + +const _Rank_name = "AceTwoThreeFourFiveSixSevenEightNineTenJackQueenKing" + +var _Rank_index = [...]uint8{0, 3, 6, 11, 15, 19, 22, 27, 32, 36, 39, 43, 48, 52} + +func (i Rank) String() string { + i -= 1 + if i >= Rank(len(_Rank_index)-1) { + return "Rank(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _Rank_name[_Rank_index[i]:_Rank_index[i+1]] +}