Go Fish/Nim

From Rosetta Code

We use the module "playing_cards" from task https://rosettacode.org/wiki/Playing_cards.

import strutils, strformat
import playing_cards

const
  Human = 0
  Computer = 1

type

  Player = range[Human..Computer]
  SortedHand = array[Rank, set[Suit]]   # Map card ranks to sets of suits in hand.
  Game = object
    deck: Deck                          # Current content of deck.
    hands: array[Player, SortedHand]    # Player hands.
    bookCounts: array[Player, Natural]  # Number of books.
    prevChoice: Rank                    # Previous choice done by computer.

const Quit = -1   # Special value used to indicate to quit.

#---------------------------------------------------------------------------------------------------

template other(player: Player): Player = 1 - player

#---------------------------------------------------------------------------------------------------

proc toSortedHand(hand: Hand): SortedHand =
  ## Convert a hand (which is as a sequence of cards) to a "SortedHand".
  for card in hand:
    result[card.rank].incl(card.suit)

#---------------------------------------------------------------------------------------------------

proc `$`(hand: SortedHand): string =
  ## Return the representation of a "SortedHand".
  for rank, suits in hand:
    for suit in suits:
      result.addSep(" ")
      result.add $(rank: rank, suit: suit)

#---------------------------------------------------------------------------------------------------

proc askQuestion(question: string; answers: openArray[string]): int =
  ## Ask a question and return the index in the possible answers.
  ## Return the value "Quit" if an end of file is encountered.
  try:
    while true:
      stdout.write question, ' '
      let input = stdin.readLine()
      result = answers.find(input)
      if result >= 0: return
      echo "I don’t understand your answer"
  except EOFError:
    result = Quit

#---------------------------------------------------------------------------------------------------

proc pronoun(player: Player): string {.inline.} =
  ## Return the pronoun to use according to the player.
  if player == Human: "You" else: "I"

#---------------------------------------------------------------------------------------------------

proc checkBookCompleted(game: var Game; player: Player; rank: Rank) =
  ## Check if a book is completed.
  if game.hands[player][rank].len == 4:
    game.hands[player][rank] = {}
    inc game.bookCounts[player]
    echo &"{player.pronoun} completed the book for rank: {rank}"

#---------------------------------------------------------------------------------------------------

proc findChoices(hand: SortedHand): seq[string] =
  ## Find the possible choices for ranks according to the "SortedHand".
  for rank, suits in hand:
    if suits.len != 0: result.add $rank

#---------------------------------------------------------------------------------------------------

proc updateHands(game: var Game; player: Player; rank: Rank): Player =
  ## Update the hands after a player has asked for a rank.

  let otherPlayer = player.other

  # Search in other player hand.
  if game.hands[otherPlayer][rank].len != 0:
    # Transfer cards to player.
    var cards: seq[Card]
    for suit in game.hands[otherPlayer][rank]: cards.add (rank, suit)
    echo &"{player.pronoun} get: {cards}"
    game.hands[player][rank] = game.hands[player][rank] + game.hands[otherPlayer][rank]
    game.hands[otherPlayer][rank] = {}
    # Check if a book is completed.
    game.checkBookCompleted(player, rank)
    result = player

  else:
    # No cards available. Draw a card from deck.
    echo "No luck."
    let card = game.deck.draw()
    if player == Human: echo "You draw the card: ", card
    else: echo "I draw a card"
    game.hands[player][card.rank].incl(card.suit)
    game.checkBookCompleted(player, card.rank)
    result = otherPlayer

#---------------------------------------------------------------------------------------------------

proc humanPlay(game: var Game): Player =
  ## Process human turn.

  echo "Your hand: ", game.hands[Human]
  var choices = game.hands[Human].findChoices()
  if choices.len == 0:
    # Special case if we have no cards in our hand.
    if game.deck.len == 0: return Computer  # Nothing to do.
    let card = game.deck.draw()
    echo "You draw the card: ", card
    game.hands[Human][card.rank].incl(card.suit)
    choices = game.hands[Human].findChoices()

  let choiceString = choices.join(", ")
  let answer = askQuestion(&"What is your choice ({choiceString})?", choices)
  if answer == Quit:
    echo ""
    quit("Quitting")
  let choice = parseEnum[Rank](choices[answer])
  result = game.updateHands(Human, choice)

#---------------------------------------------------------------------------------------------------

proc computerPlay(game: var Game): Player =
  ## Process computer turn.

  # The simple (but not so bad) strategy used consists to choose the rank
  # immediately succeeding to the previous used rank (wrapping around if needed).
  var choice = game.prevChoice
  while true:
    choice = if choice == King: Ace else: succ(choice)
    if game.hands[Computer][choice].len != 0 or choice == game.prevChoice: break

  if game.hands[Computer][choice].len == 0:
    # Special case if the computer has no cards in hand.
    if game.deck.len == 0: return Human  # Nothing to do.
    let card = game.deck.draw()
    echo "I draw a card"
    game.hands[Computer][card.rank].incl(card.suit)
    choice = card.rank

  game.prevChoice = choice
  echo "I want cards of rank: ", choice
  result = game.updateHands(Computer, choice)

#---------------------------------------------------------------------------------------------------

proc playGame(firstPlayer: Player) =
  ## Play a game.

  var game: Game
  game.deck = initDeck()
  game.deck.shuffle()
  let handSeqs = game.deck.deal(2, 9)
  game.hands = [handSeqs[Human].toSortedHand(), handSeqs[Computer].toSortedHand()]
  game.prevChoice = Ace

  echo &"{firstPlayer.pronoun} play first.\n"

  # Search for books before starting.
  for rank in Rank:
    game.checkBookCompleted(Human, rank)
    game.checkBookCompleted(Computer, rank)

  # Play.
  var player = firstPlayer
  while game.bookCounts[Human] + game.bookCounts[Computer] != 13:
    echo &"Your books: {game.bookCounts[Human]}     My books: {game.bookCounts[Computer]}"
    if player == Human:
      player = game.humanPlay()
    else:
      player = game.computerPlay()
    echo ""

  if game.bookCounts[Human] > game.bookCounts[Computer]:
    echo &"You win with {game.bookCounts[Human]} books against {game.bookCounts[Computer]}"
  else:
    echo &"I win with {game.bookCounts[Computer]} books against {game.bookCounts[Human]}"

#———————————————————————————————————————————————————————————————————————————————————————————————————

var firstPlayer: Player
let answer = askQuestion("Who will play first ([h]uman/[c]omputer)?", ["h", "c"])
if answer == Quit:
  echo ""
  quit("Quitting")
firstPlayer = Player(answer)

while true:
  playGame(firstPlayer)
  let answer = askQuestion("Do you want to play another game ([y]es/[n]o)? ", ["y", "n"])
  if answer == 1: break
  firstPlayer = firstPlayer.other()