commit
a4924bb15a
14 changed files with 518 additions and 0 deletions
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
arch: amd64 |
||||
environment: |
||||
PROJECT_NAME: 'cards' |
||||
CGO_ENABLED: '1' |
||||
GO111MODULE: 'on' |
||||
image: freebsd/latest |
||||
packages: |
||||
- go |
||||
sources: |
||||
- https://git.sr.ht/~tslocum/cards |
||||
tasks: |
||||
- test: | |
||||
cd $PROJECT_NAME |
||||
go test ./... |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
arch: x86_64 |
||||
environment: |
||||
PROJECT_NAME: 'cards' |
||||
CGO_ENABLED: '1' |
||||
GO111MODULE: 'on' |
||||
image: alpine/edge |
||||
packages: |
||||
- go |
||||
sources: |
||||
- https://git.sr.ht/~tslocum/cards |
||||
tasks: |
||||
- test: | |
||||
cd $PROJECT_NAME |
||||
go test ./... |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
MIT License |
||||
|
||||
Copyright (c) 2020 Trevor Slocum <trevor@rocketnine.space> |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
# cards |
||||
[](https://godoc.org/git.sr.ht/~tslocum/cards) |
||||
[](https://builds.sr.ht/~tslocum/cards) |
||||
[](https://liberapay.com/rocketnine.space) |
||||
|
||||
Playing card library |
||||
|
||||
## Documentation |
||||
|
||||
Documentation is available on [godoc](https://godoc.org/git.sr.ht/~tslocum/cards). |
||||
|
||||
## Support |
||||
|
||||
Please share issues/suggestions [here](https://todo.sr.ht/~tslocum/cards). |
@ -0,0 +1,160 @@
@@ -0,0 +1,160 @@
|
||||
// Package cards provides playing cards.
|
||||
package cards |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"strings" |
||||
) |
||||
|
||||
// Card defines a playing card with a face and suit.
|
||||
type Card struct { |
||||
Face CardFace |
||||
Suit CardSuit |
||||
} |
||||
|
||||
// NewCard initializes a Card.
|
||||
func NewCard(face CardFace, suit CardSuit) Card { |
||||
return Card{Face: face, Suit: suit} |
||||
} |
||||
|
||||
// Value returns the numeric value of a card.
|
||||
func (c Card) Value() int { |
||||
return (c.Face.Value * 100) + c.Suit.Value |
||||
} |
||||
|
||||
// Equal returns whether both cards have the same face and suit.
|
||||
func (c Card) Equal(b Card) bool { |
||||
return c.EqualFace(b) && c.EqualSuit(b) |
||||
} |
||||
|
||||
// EqualFace returns whether both cards have the same face.
|
||||
func (c Card) EqualFace(b Card) bool { |
||||
return c.Face.Value == b.Face.Value |
||||
} |
||||
|
||||
// EqualSuit returns whether both cards have the same suit.
|
||||
func (c Card) EqualSuit(b Card) bool { |
||||
return c.Suit.Value == b.Suit.Value |
||||
} |
||||
|
||||
// GreaterFace returns whether Card c has greater face value than Card b.
|
||||
func (c Card) GreaterFace(b Card) bool { |
||||
return c.Face.Value > b.Face.Value |
||||
} |
||||
|
||||
// LessFace returns whether Card c has less face value than Card b.
|
||||
func (c Card) LessFace(b Card) bool { |
||||
return c.Face.Value < b.Face.Value |
||||
} |
||||
|
||||
// String returns a human-readable string representation of a Card.
|
||||
func (c Card) String() string { |
||||
var cardFace string |
||||
if c.Face.Value == 14 { |
||||
cardFace = "! " |
||||
} else { |
||||
cardFace = fmt.Sprintf("%c", c.Face.Name[0]) |
||||
if c.Face.Value == 10 { |
||||
cardFace += "0" |
||||
} else { |
||||
cardFace += " " |
||||
} |
||||
} |
||||
|
||||
return fmt.Sprintf("[%s %s %c]", cardFace, c.Suit.Symbol, c.Suit.Name[0]) |
||||
} |
||||
|
||||
// Identifier returns a 2-3 character machine-readable representation of a card.
|
||||
func (c Card) Identifier() string { |
||||
if c.Face.Value == 0 || c.Suit.Value == 0 { |
||||
return "??" |
||||
} |
||||
|
||||
var faceidentifier string |
||||
if c.Face.Value == FaceJoker.Value { |
||||
faceidentifier = "!" |
||||
} else if c.Face.Value == Face10.Value { |
||||
faceidentifier = c.Face.Name |
||||
} else { |
||||
faceidentifier = string(c.Face.Name[0]) |
||||
} |
||||
|
||||
var suitidentifier string |
||||
if c.Suit.Value == SuitJoker.Value { |
||||
suitidentifier = "!" |
||||
} else { |
||||
suitidentifier = c.Suit.Name[0:1] |
||||
} |
||||
|
||||
return strings.ToUpper(faceidentifier + suitidentifier) |
||||
} |
||||
|
||||
// MarshalJSON marshals a Card.
|
||||
func (c Card) MarshalJSON() ([]byte, error) { |
||||
return json.Marshal(c.Identifier()) |
||||
} |
||||
|
||||
// UnmarshalJSON unmarshals a Card.
|
||||
func (c *Card) UnmarshalJSON(b []byte) error { |
||||
var s string |
||||
if err := json.Unmarshal(b, &s); err != nil { |
||||
return err |
||||
} |
||||
if sLen := len(s); sLen < 2 || sLen > 3 { |
||||
return fmt.Errorf("invalid card identifier: %s", s) |
||||
} |
||||
|
||||
var cardFace CardFace |
||||
switch s[:len(s)-1] { |
||||
case "A": |
||||
cardFace = FaceAce |
||||
case "2": |
||||
cardFace = Face2 |
||||
case "3": |
||||
cardFace = Face3 |
||||
case "4": |
||||
cardFace = Face4 |
||||
case "5": |
||||
cardFace = Face5 |
||||
case "6": |
||||
cardFace = Face6 |
||||
case "7": |
||||
cardFace = Face7 |
||||
case "8": |
||||
cardFace = Face8 |
||||
case "9": |
||||
cardFace = Face9 |
||||
case "10": |
||||
cardFace = Face10 |
||||
case "J": |
||||
cardFace = FaceJack |
||||
case "Q": |
||||
cardFace = FaceQueen |
||||
case "K": |
||||
cardFace = FaceKing |
||||
case "!": |
||||
cardFace = FaceJoker |
||||
default: |
||||
return fmt.Errorf("invalid face identifier %s", s) |
||||
} |
||||
|
||||
var cardSuit CardSuit |
||||
switch s[len(s)-1:] { |
||||
case "H": |
||||
cardSuit = SuitHearts |
||||
case "D": |
||||
cardSuit = SuitDiamonds |
||||
case "C": |
||||
cardSuit = SuitClubs |
||||
case "S": |
||||
cardSuit = SuitSpades |
||||
case "!": |
||||
cardSuit = SuitJoker |
||||
default: |
||||
return fmt.Errorf("invalid suit identifier %s", s) |
||||
} |
||||
|
||||
*c = NewCard(cardFace, cardSuit) |
||||
return nil |
||||
} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
package cards |
||||
|
||||
// CardFace defines a card face name and numeric value.
|
||||
type CardFace struct { |
||||
Name string |
||||
Value int |
||||
} |
||||
|
||||
// Card faces:
|
||||
var ( |
||||
FaceAce = CardFace{"Ace", 1} |
||||
Face2 = CardFace{"2", 2} |
||||
Face3 = CardFace{"3", 3} |
||||
Face4 = CardFace{"4", 4} |
||||
Face5 = CardFace{"5", 5} |
||||
Face6 = CardFace{"6", 6} |
||||
Face7 = CardFace{"7", 7} |
||||
Face8 = CardFace{"8", 8} |
||||
Face9 = CardFace{"9", 9} |
||||
Face10 = CardFace{"10", 10} |
||||
FaceJack = CardFace{"Jack", 11} |
||||
FaceQueen = CardFace{"Queen", 12} |
||||
FaceKing = CardFace{"King", 13} |
||||
FaceJoker = CardFace{"Joker", 14} |
||||
) |
||||
|
||||
// StandardFaces is a slice of all faces except Jokers.
|
||||
var StandardFaces = []CardFace{ |
||||
FaceAce, |
||||
Face2, |
||||
Face3, |
||||
Face4, |
||||
Face5, |
||||
Face6, |
||||
Face7, |
||||
Face8, |
||||
Face9, |
||||
Face10, |
||||
FaceJack, |
||||
FaceQueen, |
||||
FaceKing, |
||||
} |
||||
|
||||
// AllFaces is a slice of all faces.
|
||||
var AllFaces = []CardFace{ |
||||
FaceAce, |
||||
Face2, |
||||
Face3, |
||||
Face4, |
||||
Face5, |
||||
Face6, |
||||
Face7, |
||||
Face8, |
||||
Face9, |
||||
Face10, |
||||
FaceJack, |
||||
FaceQueen, |
||||
FaceKing, |
||||
FaceJoker, |
||||
} |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
package cards |
||||
|
||||
// CardSuit defines a card suit name, symbol and numeric value.
|
||||
type CardSuit struct { |
||||
Name string |
||||
Symbol string |
||||
Value int |
||||
} |
||||
|
||||
// Card suits:
|
||||
var ( |
||||
SuitHearts = CardSuit{"Hearts", "♥", 1} |
||||
SuitDiamonds = CardSuit{"Diamonds", "♦", 2} |
||||
SuitClubs = CardSuit{"Clubs", "♣", 3} |
||||
SuitSpades = CardSuit{"Spades", "♠", 4} |
||||
SuitJoker = CardSuit{"Joker", "!", 5} |
||||
) |
||||
|
||||
// StandardSuits is a slice of all card suits except Jokers.
|
||||
var StandardSuits = []CardSuit{ |
||||
SuitHearts, |
||||
SuitDiamonds, |
||||
SuitClubs, |
||||
SuitSpades, |
||||
} |
||||
|
||||
// AllSuits is a slice of all suits.
|
||||
var AllSuits = []CardSuit{ |
||||
SuitHearts, |
||||
SuitDiamonds, |
||||
SuitClubs, |
||||
SuitSpades, |
||||
SuitJoker, |
||||
} |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
package cards |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"testing" |
||||
) |
||||
|
||||
type cardTestCase struct { |
||||
Card Card |
||||
Expected string |
||||
} |
||||
|
||||
var cardTestCases = []*cardTestCase{ |
||||
{Card: NewCard(FaceAce, SuitSpades), Expected: `"AS"`}, |
||||
{Card: NewCard(Face7, SuitDiamonds), Expected: `"7D"`}, |
||||
{Card: NewCard(Face10, SuitHearts), Expected: `"10H"`}, |
||||
{Card: NewCard(FaceQueen, SuitHearts), Expected: `"QH"`}, |
||||
{Card: NewCard(FaceJoker, SuitClubs), Expected: `"!C"`}, |
||||
{Card: NewCard(FaceJoker, SuitJoker), Expected: `"!!"`}, |
||||
{Card: NewCard(FaceKing, SuitJoker), Expected: `"K!"`}, |
||||
} |
||||
|
||||
func TestMarshalCard(t *testing.T) { |
||||
for _, c := range cardTestCases { |
||||
buf, err := json.Marshal(c.Card) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
if string(buf) != c.Expected { |
||||
t.Errorf("failed to marshal card: expected %s, got %s", c.Expected, string(buf)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestUnmarshalCard(t *testing.T) { |
||||
for _, c := range cardTestCases { |
||||
var cd Card |
||||
|
||||
err := json.Unmarshal([]byte(c.Expected), &cd) |
||||
if err != nil { |
||||
t.Error(err) |
||||
} |
||||
|
||||
if !cd.Equal(c.Card) { |
||||
t.Errorf("failed to unmarshal card: expected %s, got %s", c.Card, cd) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
package cards |
||||
|
||||
// Cards is a slice of Cards.
|
||||
type Cards []Card |
||||
|
||||
func (c Cards) Len() int { |
||||
return len(c) |
||||
} |
||||
|
||||
func (c Cards) Swap(i, j int) { |
||||
c[i], c[j] = c[j], c[i] |
||||
} |
||||
|
||||
func (c Cards) Less(i, j int) bool { |
||||
return c[i].Value() < c[j].Value() |
||||
} |
||||
|
||||
// Remove returns a slice of Cards excluding the specified card.
|
||||
func (c Cards) Remove(i int64) Cards { |
||||
return append(c[:i], c[i+1:]...) |
||||
} |
@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
|
||||
package cards |
||||
|
||||
import ( |
||||
"crypto/rand" |
||||
"errors" |
||||
"math/big" |
||||
) |
||||
|
||||
// ErrNotEnoughCards is the error returned when there aren't enough cards in
|
||||
// the deck to complete an action.
|
||||
var ErrNotEnoughCards = errors.New("not enough cards in deck") |
||||
|
||||
// Deck defines a playing card deck containing any number of cards.
|
||||
type Deck struct { |
||||
Cards Cards |
||||
} |
||||
|
||||
// StandardDeck initializes a standard deck of 52 cards.
|
||||
func StandardDeck() *Deck { |
||||
var d Deck |
||||
|
||||
for _, suit := range StandardSuits { |
||||
for _, face := range StandardFaces { |
||||
d.Cards = append(d.Cards, NewCard(face, suit)) |
||||
} |
||||
} |
||||
|
||||
return &d |
||||
} |
||||
|
||||
// Shuffle randomizes the deck.
|
||||
func (d *Deck) Shuffle(times int) error { |
||||
if len(d.Cards) == 0 { |
||||
return ErrNotEnoughCards |
||||
} |
||||
|
||||
var old, shuffled Cards |
||||
for shuf := times; shuf > 0; shuf-- { |
||||
old = d.Cards |
||||
shuffled = nil |
||||
|
||||
for i := len(old); i > 0; i-- { |
||||
nBig, e := rand.Int(rand.Reader, big.NewInt(int64(i))) |
||||
if e != nil { |
||||
panic(e) |
||||
} |
||||
j := nBig.Int64() |
||||
|
||||
shuffled = append(shuffled, old[j]) |
||||
old = old.Remove(j) |
||||
} |
||||
|
||||
d.Cards = shuffled |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Draw removes cards from the deck and returns them as a slice.
|
||||
func (d *Deck) Draw(count int) (cards Cards, err error) { |
||||
if count > len(d.Cards) { |
||||
return nil, ErrNotEnoughCards |
||||
} |
||||
|
||||
hand := d.Cards[0:count] |
||||
d.Cards = d.Cards[count:] |
||||
return hand, nil |
||||
} |
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
package cards |
||||
|
||||
import ( |
||||
"testing" |
||||
) |
||||
|
||||
func TestDeck(t *testing.T) { |
||||
d := StandardDeck() |
||||
|
||||
if len(d.Cards) != 52 { |
||||
t.Errorf("expected 52 cards, got %d", len(d.Cards)) |
||||
} |
||||
|
||||
for _, suit := range StandardSuits { |
||||
for _, face := range StandardFaces { |
||||
found := 0 |
||||
for _, c := range d.Cards { |
||||
if c.Equal(NewCard(face, suit)) { |
||||
found++ |
||||
} |
||||
} |
||||
if found != 1 { |
||||
t.Errorf("expected 1 %s, got %d", NewCard(face, suit), found) |
||||
} |
||||
} |
||||
} |
||||
|
||||
var discard Cards |
||||
for i := 0; i < 52; i++ { |
||||
c, err := d.Draw(1) |
||||
if err != nil || len(c) == 0 { |
||||
t.Errorf("failed to draw card: %s", err) |
||||
} |
||||
|
||||
discard = append(discard, c[0]) |
||||
} |
||||
|
||||
if len(d.Cards) != 0 { |
||||
t.Errorf("expected 0 cards after drawing, got %d", len(d.Cards)) |
||||
} |
||||
|
||||
for _, suit := range StandardSuits { |
||||
for _, face := range StandardFaces { |
||||
found := 0 |
||||
for _, c := range discard { |
||||
if c.Equal(NewCard(face, suit)) { |
||||
found++ |
||||
} |
||||
} |
||||
if found != 1 { |
||||
t.Errorf("failed to draw 52 cards: expected 1 %s, got %d", NewCard(face, suit), found) |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue