Initial commit
This commit is contained in:
commit
a4924bb15a
|
@ -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 @@
|
|||
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,2 @@
|
|||
.idea/
|
||||
*.sh
|
|
@ -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 @@
|
|||
# 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).
|
|
@ -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 @@
|
|||
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 @@
|
|||
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 @@
|
|||
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 @@
|
|||
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 @@
|
|||
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 @@
|
|||
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