Initial commit

This commit is contained in:
Trevor Slocum 2020-01-09 07:24:45 -08:00
commit a4924bb15a
14 changed files with 518 additions and 0 deletions

14
.builds/amd64_freebsd.yml Normal file
View File

@ -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 ./...

View File

@ -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 ./...

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea/
*.sh

2
CHANGELOG Normal file
View File

@ -0,0 +1,2 @@
0.1.0:
- Initial release

21
LICENSE Normal file
View File

@ -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.

14
README.md Normal file
View File

@ -0,0 +1,14 @@
# cards
[![GoDoc](https://godoc.org/git.sr.ht/~tslocum/cards?status.svg)](https://godoc.org/git.sr.ht/~tslocum/cards)
[![builds.sr.ht status](https://builds.sr.ht/~tslocum/cards.svg)](https://builds.sr.ht/~tslocum/cards)
[![Donate](https://img.shields.io/liberapay/receives/rocketnine.space.svg?logo=liberapay)](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).

160
card.go Normal file
View File

@ -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
}

60
card_face.go Normal file
View File

@ -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,
}

34
card_suit.go Normal file
View File

@ -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,
}

49
card_test.go Normal file
View File

@ -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)
}
}
}

21
cards.go Normal file
View File

@ -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:]...)
}

68
deck.go Normal file
View File

@ -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
}

56
deck_test.go Normal file
View File

@ -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)
}
}
}
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.sr.ht/~tslocum/cards
go 1.13