3
\$\begingroup\$

Based on this question, I have written my own implementation of the Go Fish game, with the following features:

  • Full trace of all actions that happen in the game
  • More than 2 players possible
  • Each of the players can be a computer or a human
  • As a debugging aid, it is possible to see the computer player's hands by setting Spy = true

For advertising the Go programming language on Rosetta Code, I want the code to be as idiomatic as possible. So here it is:

package main

import (
    "errors"
    "fmt"
    "math/rand"
    "strings"
    "time"
)

const Spy = false

var rnd = rand.New(rand.NewSource(time.Now().UnixNano()))

type Card int

func (c Card) String() string {
    return [...]string{"2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"}[int(c)]
}

func parseCard(s string) (Card, error) {
    for i := 0; i < 13; i++ {
        card := Card(i)
        if card.String() == s {
            return card, nil
        }
    }
    return Card(0), errors.New("Unknown card value: " + s)
}

type Player struct {
    hand     []int
    score    int
    computer bool
}

func newPlayer(computer bool) *Player {
    return &Player{make([]int, 13, 13), 0, computer}
}

func (p *Player) TakeCard(card Card) {
    p.hand[card]++
    if p.hand[card] == 4 {
        p.hand[card] = 0
        p.score++
        fmt.Printf("There's a complete book of %s.\n", card)
    }
}

func (p *Player) NumberOfCards() int {
    total := 0
    for _, count := range p.hand {
        total += count
    }
    return total
}

type GoFishGame struct {
    deck      []Card
    players   []*Player
    turn      *Player
    turnIndex int
}

func NewGoFishGame(computer ...bool) *GoFishGame {
    deck := make([]Card, 52, 52)
    for i, card := range rnd.Perm(52) {
        deck[i] = Card(card % 13)
    }
    players := make([]*Player, len(computer), len(computer))
    for i, computer := range computer {
        players[i] = newPlayer(computer)
    }
    return &GoFishGame{deck, players, players[0], 0}
}

func (gm *GoFishGame) playerName(index int) string {
    if gm.players[index].computer {
        return fmt.Sprintf("Computer %d", index+1)
    } else {
        return fmt.Sprintf("Human %d", index+1)
    }
}

func (gm *GoFishGame) decideStealingComputer() (int, Card) {
    opponentIndex := rnd.Intn(len(gm.players) - 1)
    if opponentIndex >= gm.turnIndex {
        opponentIndex++
    }

    var choices []Card
    for card, count := range gm.turn.hand {
        for i := 0; i < count; i++ {
            choices = append(choices, Card(card))
        }
    }
    rank := choices[rnd.Intn(len(choices))]

    return opponentIndex, rank
}

func (gm *GoFishGame) decideStealingHuman() (int, Card) {
again:
    var index int
    if len(gm.players) == 2 {
        index = (gm.turnIndex + 1) % 2
    } else {
        var oneIndex int
        fmt.Printf("Steal from whom (1-%d)? ", len(gm.players))
        fmt.Scan(&oneIndex)
        index = oneIndex - 1
        if !(0 <= index && index <= len(gm.players) && index != gm.turnIndex) {
            fmt.Println("Wrong opponent.")
            goto again
        }
    }

    fmt.Print("Steal which rank? ")
    var card string
    fmt.Scan(&card)
    rank, err := parseCard(strings.ToUpper(card))
    if err != nil {
        fmt.Println("Wrong rank.")
        goto again
    }

    if gm.turn.hand[rank] == 0 {
        fmt.Println("You can only steal a rank from your hand.")
        goto again
    }
    return index, rank
}

func (gm *GoFishGame) decideStealing() (int, Card) {
    if gm.turn.computer {
        return gm.decideStealingComputer()
    } else {
        return gm.decideStealingHuman()
    }
}

func (gm *GoFishGame) drawCard(quiet bool) {
    card := gm.deck[0]
    gm.deck = gm.deck[1:]
    gm.turn.TakeCard(card)
    if !quiet {
        if Spy || !gm.turn.computer {
            fmt.Printf("%s drew card %s.\n", gm.playerName(gm.turnIndex), card)
        } else {
            fmt.Printf("%s drew a card.\n", gm.playerName(gm.turnIndex))
        }
    }
}

func (gm *GoFishGame) nextPlayer(quiet bool) {
    gm.turnIndex = (gm.turnIndex + 1) % len(gm.players)
    gm.turn = gm.players[gm.turnIndex]
    if !quiet {
        fmt.Printf("%s to play.\n", gm.playerName(gm.turnIndex))
    }
}

func (gm *GoFishGame) setup() {
    for range gm.players {
        for i := 0; i < 9; i++ {
            gm.drawCard(true)
        }
        gm.nextPlayer(true)
    }
}

func (gm *GoFishGame) gameOver() bool {
    if len(gm.deck) != 0 {
        return false
    }
    for _, player := range gm.players {
        if player.NumberOfCards() > 0 {
            return false
        }
    }
    return true
}

func (gm *GoFishGame) printStatus() {
    for p, player := range gm.players {
        if p != 0 {
            fmt.Print("   ")
        }
        n := player.NumberOfCards()
        fmt.Print(gm.playerName(p), ": ", n, " cards")
        if (Spy || !player.computer) && n > 0 {
            fmt.Print(" (")
            sep := ""
            for rank, count := range player.hand {
                for i := 0; i < count; i++ {
                    fmt.Print(sep, Card(rank))
                    sep = " "
                }
            }
            fmt.Print(")")
        }
        fmt.Print(", ", player.score, " points.")
    }
    fmt.Print("   ", len(gm.deck), " cards left.")
    fmt.Println()
}

func (gm *GoFishGame) move() {
    player := gm.turn

again:
    if player.NumberOfCards() == 0 {
        if len(gm.deck) == 0 {
            return
        }
        gm.drawCard(false)
    }

    opponentIndex, rank := gm.decideStealing()
    if player.hand[rank] == 0 {
        panic("strategy generated illegal move")
    }

    opponent := gm.players[opponentIndex]
    stolen := opponent.hand[rank]
    opponent.hand[rank] = 0

    if stolen != 0 {
        fmt.Printf("%s stole %d cards of rank %s from %s.\n",
            gm.playerName(gm.turnIndex), stolen, rank, gm.playerName(opponentIndex))
        for i := 0; i < stolen; i++ {
            player.TakeCard(rank)
        }
        if player.hand[rank] == 0 && gm.gameOver() {
            return
        }
        gm.printStatus()
        goto again
    }

    fmt.Printf("%s does not have cards of rank %s.\n", gm.playerName(opponentIndex), rank)
    if len(gm.deck) > 0 {
        gm.drawCard(false)
    }
}

func (gm *GoFishGame) Play() {
    gm.setup()

    for !gm.gameOver() {
        gm.printStatus()
        gm.move()
        gm.nextPlayer(false)
    }

    fmt.Println("Game over.")
    gm.printStatus()
}

func main() {
    gm := NewGoFishGame(true, true, true, true, true)
    gm.Play()
}
\$\endgroup\$
1
\$\begingroup\$

A couple of general comments:

  1. I prefer to keep all literals (e.g. 13, 4, 52...) as constants. Basically, anything other than -1, 0 and 1 is nicer to have as constant, just for the sake of having self-documenting code.

  2. Card(0) seems to be a valid card, but you return it when your function parseCard fails (will get to this in a bit). It's useful to have an invalid state, so I'd use the default value 0 for a card that's invalid.

  3. Consider log.Printf instead of fmt.Printf for your debug prints.


var rnd = rand.New(rand.NewSource(time.Now().UnixNano()))

I see no reason to create your own Random object - you can just use top-level functions from the rand package, i.e. rand.Intn instead of rnd.Intn and rand.Perm instead of rnd.Perm. Call rand.Seed(time.Now().UnixNano()) when you start your application to change the seed.


func (c Card) String() string {
    return [...]string{"2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"}[int(c)]
}

I'd move the array to be a global variable (just because arrays can't be constants). Also, you may want to do some bounds checking in this method, otherwise this may panic with invalid input.


func parseCard(s string) (Card, error) {
    for i := 0; i < 13; i++ {
        card := Card(i)
        if card.String() == s {
            return card, nil
        }
    }
    return Card(0), errors.New("Unknown card value: " + s)
}

This function is doing a lot of work just to check if a string is valid and convert it to a card. I'd suggest using a map[string]Card instead (define it as a global variable).

Speaking of which, you might want to use fmt.Errorf instead of errors.New for convenience when constructing error messages.


for i, computer := range computer {
    players[i] = newPlayer(computer)
}

You're creating a local variable computer inside the loop which has the same name as the argument, which has a different type. While the code is trivial here, this greatly reduces readability and you should pick a different name for the iterator.


func NewGoFishGame(computer ...bool) *GoFishGame {
    ...
}

...

func (gm *GoFishGame) setup() {
    ...
}

I don't understand why there are two functions that handle initialisation. I'd expect NewGoFishGame to only allocate memory and create a game object that can be run multiple times, but this doesn't seem to be the case, as it does some setup as well (e.g. shuffle the deck).


deck := make([]Card, 52, 52)

This is the same as deck := make([]Card, 52), as the capacity defaults to size. Same goes for other places where you used make.


func (gm *GoFishGame) decideStealingHuman() (int, Card) {
    ...
}

I'd only use goto if I absolutely have to. In this case, it seems a simple loop would easily suffice. Same goes for the other function.


if player.hand[rank] == 0 {
    panic("strategy generated illegal move")
}

Don't use a panic. As per Effective Go, you should always prefer to return an error, unless the error is unrecoverable.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.