Go Fish/PureBasic

From Rosetta Code
Revision as of 08:34, 31 August 2022 by PureFox (talk | contribs) (Fixed syntax highlighting.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Go Fish/PureBasic is part of Go Fish. You may find other members of Go Fish at Category:Go Fish.

If 'computer' is entered for the player's name, it will play itself.

#MaxCards = 52 ;Max #of cards possible in a card collection
#ShortCardRanks$ = "2;3;4;5;6;7;8;9;10;J;Q;K;A"
#LongCardRanks$ = "deuce;three;four;five;six;seven;eight;nine;ten;jack;queen;king;ace"
#CardRankArticles$ = "a;a;a;a;a;a;an;a;a;a;a;a;an"
#CardRankPlurals$ = "s;s;s;s;es;s;s;s;s;s;s;s;s"
#NumCardRanks = 13
#MaxHistorySize = 4 ;must be 0 < #MaxHistorySize <= #NumCardRanks
#Indent$ = "   "

Structure _membersCardCollectionClass
  *vtable.i 
  size.i ;# of cards present
  card.i[#MaxCards] ;collection content, stores rank# for each card, suits aren't used
EndStructure

Interface CardCollectionObj
  init(isDeck = #False)
  count()
  countMatchingCards(rank)
  drawFrom(src.CardCollectionObj)
  pushCard(rank)
  popCard()
  removeCards(rank)
  transferCards(src.CardCollectionObj, rank)
  shuffle()
  show.s()
  sort()
EndInterface 

Structure _membersPlayerClass
  *vtable.i 
  isHuman.i 
  name.s
  score.i
  hand.CardCollectionObj
  ranksOpponentHas.i[#NumCardRanks]  ;priority 1 for requests, index = (rank# - 1)
  newRanksDrawn.i[#NumCardRanks]     ;priority 2 for requests, index = (rank# - 1)
  history.i[#MaxHistorySize]         ;priority 3 requests are selected with the help of this history
EndStructure

Interface PlayerObj
  init(deck.CardCollectionObj)
  getScore()
  getName.s()
  isHuman() ;boolean
  countCardsInHand()
  takeTurn(otherPlayer.PlayerObj,deck.CardCollectionObj)
  draw(deck.CardCollectionObj)
  shiftHistory()
  setHistory(rank)
  createDiffHistory(*hand.CardCollectionObj)
  request(dest.CardCollectionObj, rank) ;boolean
  updateScore()
EndInterface 

Enumeration
  #HumanGamePlayer
  #ComputerGamePlayer
EndEnumeration

Structure _membersGameClass
  *vtable.i 
  turn.i  ;player# whose turn it is
  deck.CardCollectionObj
  player.PlayerObj[2]
EndStructure

Interface GameObj
  play()
  displayWinner() 
EndInterface 

Procedure handleError(condition,Msg$)
  If Not condition
    MessageRequester("Error",Msg$)
    End
  EndIf 
EndProcedure

Procedure.s verboseCardInfo(rank)
  If rank > 0 And rank <= #NumCardRanks
    ProcedureReturn StringField(#LongCardRanks$, rank, ";")
  EndIf 
EndProcedure

Procedure CC_init(*this._membersCardCollectionClass, isDeck)
  Protected i
  If isDeck
    *this\size = #MaxCards - 1
    For i = 0 To #MaxCards - 1
      *this\card[i] = (i % #NumCardRanks) + 1
    Next
  Else
    *this\size = -1
  EndIf 
EndProcedure

Procedure CC_countCards(*this._membersCardCollectionClass)
  ProcedureReturn *this\size + 1
EndProcedure

Procedure CC_countMatchingCards  (*this._membersCardCollectionClass, rank)
  Protected i, count
  For i = 0 To *this\size
    If *this\card[i] = rank
      count + 1
    EndIf
  Next
  ProcedureReturn count 
EndProcedure

Procedure CC_drawFrom(*this._membersCardCollectionClass, *source.CardCollectionObj)
  Protected cardDrawn, *dest.CardCollectionObj = *this
  cardDrawn = *source\popCard()
  *dest\pushCard(cardDrawn)
  ProcedureReturn cardDrawn 
EndProcedure

Procedure CC_pushCard(*this._membersCardCollectionClass, rank)
  If *this\size < #MaxCards And (rank > 0 And rank <= #NumCardRanks)
    *this\size + 1
    *this\card[*this\size] = rank
  EndIf 
EndProcedure

Procedure CC_popCard(*this._membersCardCollectionClass)
  Protected rank
  If *this\size >= 0
    rank = *this\card[*this\size]
    *this\size - 1
  EndIf 
  ProcedureReturn rank ;returns #Null if no cards are in collection
EndProcedure

Procedure CC_removeCards(*this._membersCardCollectionClass, rank)
  ;remove all cards matching rank
  Protected i
  
  For i = *this\size To 0 Step -1
    If *this\card[i] = rank
      If *this\size < (#MaxCards - 1) And i <> *this\size
        MoveMemory(@*this\card[i + 1], @*this\card[i], SizeOf(Integer) * *this\size - i)
      EndIf 
      *this\size - 1
    EndIf 
  Next 
EndProcedure

Procedure CC_transferCards(*this._membersCardCollectionClass, *source.CardCollectionObj, rank)
  ;move all cards matching rank from source and return count
  Protected i, cardsTransfered, *src._membersCardCollectionClass = *source, blankcard
  If *source <> #Null
    For i = *src\size To 0 Step -1
      If *src\card[i] = rank
        *this\size + 1
        *this\card[*this\size] = *src\card[i]
        If *src\size < (#MaxCards - 1) And i <> *src\size
          MoveMemory(@*src\card[i + 1], @*src\card[i], SizeOf(Integer) * (*src\size - i))
        EndIf 
        *src\size - 1
        cardsTransfered + 1
      EndIf
    Next 
  EndIf 
  
  ProcedureReturn cardsTransfered
EndProcedure

Procedure CC_shuffle(*this._membersCardCollectionClass)
  Protected w, i
  If *this\size >= 0
    Dim shuffled(*this\size)
    
    For i = *this\size To 0 Step -1
      w = Random(i)
      shuffled(i) = *this\card[w]
      If w <> i
        *this\card[w] = *this\card[i]
      EndIf
    Next
    
    For i = 0 To *this\size
      *this\card[i] = shuffled(i)
    Next
  EndIf 
EndProcedure

Procedure.s CC_showCards(*this._membersCardCollectionClass)
  Protected i, output$
  
  For i = 0 To *this\size
    output$ + StringField(#ShortCardRanks$, *this\card[i],";")
    If i <> *this\size: output$ + ", ": EndIf 
  Next
  ProcedureReturn output$
EndProcedure

Procedure CC_sortCards(*this._membersCardCollectionClass)
  Protected low, high
  Protected firstIndex, lastIndex = *this\size
    
  If lastIndex > firstIndex + 1
    low = firstIndex + 1
    While low <= lastIndex
      high = low
      While high > firstIndex
        If *this\card[high] < *this\card[high - 1]
          Swap *this\card[high - 1], *this\card[high]
        Else
          Break
        EndIf
        high - 1
      Wend
      low + 1
    Wend
  EndIf
EndProcedure

Procedure newCardCollection(isDeck = #False)
  Protected *newCardCollection._membersCardCollectionClass = AllocateMemory(SizeOf(_membersCardCollectionClass))
  If *newCardCollection
    *newCardCollection\vtable = ?vTable_CardCollectionClass
    CC_init(*newCardCollection, isDeck)
  EndIf
  ProcedureReturn *newCardCollection 
EndProcedure

Procedure _player_validateRank(inputRank.s)
  ;check if inputRank is valid, allows verbose and abbrieviated entries
  Protected rankSize, result, validatedRank, i
  
  inputRank = Trim(inputRank)
  rankSize = Len(inputRank)
  Select rankSize
    Case 0
    Case 1
      inputRank = UCase(inputRank)
      If inputRank = "T": inputRank = "10": EndIf ;handle an alias
      For i = 1 To #NumCardRanks
        If inputRank = StringField(#ShortCardRanks$, i, ";")
          validatedRank = i
          Break
        EndIf 
      Next
    Default
      inputRank = LCase(inputRank)
      
      result = FindString(inputRank, " ", 1)
      If result
        inputRank = Left(inputRank, result - 1)
      EndIf
      
      result = FindString(inputRank, "s", 2)
      If result
        inputRank = Left(inputRank, result - 1)
      EndIf
      
      ;handle some aliases
      Select inputRank
        Case "10"
          inputRank = "ten"
        Case "two"
          inputRank = "deuce"
      EndSelect
      
      For i = 1 To #NumCardRanks
        If inputRank = StringField(#LongCardRanks$, i, ";")
          validatedRank = i
          Break
        EndIf 
      Next 
  EndSelect
  
  ProcedureReturn validatedRank ;returns #Null if rank not valid
EndProcedure

Procedure player_init(*this._membersPlayerClass, *deck.CardCollectionObj)
  Protected i, rankDrawn
  ;draw 9 cards from *deck and add them to player's hand
  If *deck <> #Null
    For i = 1 To 9
      rankDrawn = *this\hand\drawFrom(*deck)
      *this\newRanksDrawn[rankDrawn - 1] = #True
    Next 
  EndIf 
EndProcedure

Procedure player_getScore(*this._membersPlayerClass)
  ProcedureReturn *this\score
EndProcedure

Procedure.s player_getName(*this._membersPlayerClass)
  ProcedureReturn *this\name
EndProcedure

Procedure player_isHuman(*this._membersPlayerClass)
  ProcedureReturn *this\isHuman
EndProcedure

Procedure player_countCardsInHand(*this._membersPlayerClass)
  ProcedureReturn *this\hand\count()
EndProcedure

Procedure player_takeTurn(*this._membersPlayerClass, otherPlayer.PlayerObj, *deck.CardCollectionObj)
  Static tempHand.CardCollectionObj
  Protected player.PlayerObj = *this, *otherPlayer._membersPlayerClass = otherPlayer
  Protected request.s, rank, cardsReceived, turnOver = #False, AI_stage, i
  
  If Not tempHand
    tempHand = newCardCollection()
    handleError(tempHand,"Unable to allocate enough memory.")
  EndIf 
  
  While Not turnOver
    ;prepare request
    If player\isHuman()
      *this\hand\sort()
      Repeat
        PrintN(#CRLF$ + "Your hand: " + *this\hand\show())
        rank = *this\hand\popCard() ;peek at last card
        *this\hand\pushCard(rank)
        If *this\hand\countMatchingCards(rank) = *this\hand\count()
          Print(#CRLF$ + "You are obligated to ask for " + verboseCardInfo(rank) + StringField(#CardRankPlurals$, rank, ";") + ".")
          Break
        Else
          Print(#CRLF$ + player\getName() + ", which rank do you want?")
          rank = _player_validateRank(Input())
          Select rank
            Case #Null 
              PrintN("** That's not a valid rank, try one from your hand.")
            Case 1 To #NumCardRanks
              If *this\hand\countMatchingCards(rank)
                Break
              Else
                PrintN("** You don't have any of those cards in your hand, choose another.")  
              EndIf
          EndSelect
        EndIf 
      ForEver
    Else
      Repeat
        Select AI_stage
          Case 0
            ;request all known cards that are in both opponent's and player's hand
            rank = #Null
            For i = 1 To #NumCardRanks
              If *this\ranksOpponentHas[i - 1] And *this\hand\countMatchingCards(i)
                *this\ranksOpponentHas[i - 1] = 0
                rank = i
              EndIf
            Next 
            
            If rank
              player\shiftHistory()
              player\setHistory(rank)
            Else
              AI_stage = 1
              tempHand\init()
              For i = 1 To #NumCardRanks
                If *this\newRanksDrawn[i - 1]
                  If *this\hand\countMatchingCards(i)
                    tempHand\pushCard(i)
                  Else
                    *this\newRanksDrawn[i - 1] = #False ;card is no longer in hand
                  EndIf 
                EndIf 
              Next 
            EndIf
          Case 1
            ;request a random card from the drawn list
            tempHand\shuffle()
            rank = tempHand\popCard()
            
            If rank
              *this\newRanksDrawn[rank - 1] = #False
              player\shiftHistory()
              player\setHistory(rank)
            Else
              AI_stage = 2
            EndIf 
          Case 2 
            ;request a random card from remaining cards
            player\shiftHistory()
            player\createDiffHistory(tempHand)
            tempHand\shuffle()
            rank = tempHand\popCard()
            player\setHistory(rank)
        EndSelect
      Until rank <> #Null
      
      Print(#CRLF$ + player\getName() + " asks for all of ")
      If otherPlayer\isHuman()
        Print("your ")
      Else
        Print(otherPlayer\getName() + "'s ")
      EndIf
      PrintN(verboseCardInfo(rank) + StringField(#CardRankPlurals$, rank, ";") + ".")
    EndIf   
    
    cardsReceived = otherPlayer\request(*this\hand, rank)
    If cardsReceived
      If player\isHuman()
        Print(#CRLF$ + #Indent$ + "You are given ")
      ElseIf Not otherPlayer\isHuman()
        Print(#CRLF$ + #Indent$ + player\getName() + " is given ")
      Else
        Print(#CRLF$ + #Indent$ + "You give " + player\getName() + " ")
      EndIf 
      Print(Str(cardsReceived) + " " + verboseCardInfo(rank))
      If cardsReceived > 1
        PrintN(StringField(#CardRankPlurals$, rank, ";") + ".")
      Else 
        PrintN(".")
      EndIf 
      
      player\updateScore()
      If otherPlayer\countCardsInHand() = 0
        If Not otherPlayer\draw(*deck)
          turnOver = #True ;game over
        EndIf
      EndIf 
      If player\countCardsInHand() = 0
        If Not player\draw(*deck)
          turnOver = #True ;game over
          Continue
        Else
          AI_stage = 0
        EndIf 
      EndIf 
    Else 
      turnOver = #True
    EndIf 
  Wend
    
  ;go fish
  If *deck\count()
    If otherPlayer\isHuman()
      Print(#CRLF$ + "You tell ")
    Else 
      Print(#CRLF$ + otherPlayer\getName() + " tells ")
    EndIf
    
    If player\isHuman()
      Print("you to 'Go Fish!'")
    Else
      Print(player\getName() + " to 'Go Fish!'")
    EndIf
  EndIf 
  Repeat
    If Not player\draw(*deck)
      Break ;game over, exit loop
    EndIf
  Until player\countCardsInHand() > 0
EndProcedure

Procedure player_draw(*this._membersPlayerClass, *deck.CardCollectionObj)
  ;draw a card from *deck and display card if player is human
  ;return #True if a card was drawn, return #False if no cards drawn
  Protected isGoFish = #False, player.PlayerObj = *this
  If *deck And *deck\count()
    If *this\hand\count() = 0
      If player\isHuman()
        Print(#Indent$ + "You are")
      Else
        Print(#Indent$ + *this\name + " is")
      EndIf 
      Print(" out of cards, so ")
    Else
      isGoFish = #True
      Print(#Indent$)
    EndIf 
    
    Protected cardDrawn = *this\hand\drawFrom(*deck)
    If Not *this\hand\countMatchingCards(cardDrawn)
      *this\newRanksDrawn[cardDrawn - 1] = #True
    EndIf 
    If player\isHuman()
      If isGoFish
        Print("Y")
      Else
        Print("y")
      EndIf 
      PrintN("ou draw a card and get " + StringField(#CardRankArticles$, cardDrawn, ";") + " " + StringField(#LongCardRanks$, cardDrawn, ";") + ".")
    Else
      PrintN(*this\name + " draws a card.")
    EndIf 
    
    If *this\hand\count() > 3
      player\updateScore()
    EndIf 
    ProcedureReturn #True
  Else
    ;No more cards in deck.
    ProcedureReturn #False
  EndIf 
EndProcedure

Procedure player_shiftHistory(*this._membersPlayerClass)
  Protected i

  For i =#MaxHistorySize - 1 To 1 Step - 1
    *this\history[i] = *this\history[i - 1]
  Next 
EndProcedure

Procedure player_setHistory(*this._membersPlayerClass, rank)
  *this\history[0] = rank
EndProcedure

Procedure player_createDiffHistory(*this._membersPlayerClass, *hand.CardCollectionObj)
  ;update *hand to contain only ranks in hand but not in history
  Protected i, activeHistorySize
  ;determine history size (= min(#MaxHistorySize, uniqueRankCount)
  If *hand
    For i = 1 To #NumCardRanks
      If activeHistorySize = #MaxHistorySize: Break: EndIf 
      If *this\hand\countMatchingCards(i)
        activeHistorySize + 1
      EndIf 
    Next 
    ;add all card-ranks from player's hand
    *hand\init()
    For i = 1 To #NumCardRanks
      If *this\hand\countMatchingCards(i)
        *hand\pushCard(i)
      EndIf 
    Next 
    ;remove cards that are also in history
    For i = 0 To activeHistorySize - 1
      *hand\removeCards(*this\history[i])
    Next
  EndIf 
EndProcedure

Procedure player_request(*this._membersPlayerClass, *dest.CardCollectionObj, rank)
  ;report how many cards of rank are present in hand
  ;also mark the requested card as being in the opponent's hand
  Protected cardsPresent
  If *dest <> #Null
    cardsPresent = *dest\transferCards(*this\hand, rank)
    *this\ranksOpponentHas[rank - 1] = 1
    *this\newRanksDrawn[rank - 1] = #False ;no need to keep in two places
    ProcedureReturn cardsPresent
  Else
    ProcedureReturn #Null
  EndIf 
EndProcedure

Procedure player_updateScore(*this._membersPlayerClass)
  ;check for complete books and remove them from hand while increasing score
  Protected bookCount, bookFound, rank, player.PlayerObj = *this
  
  Repeat
    bookFound = #False
    For rank = #NumCardRanks To 1 Step -1
      If *this\hand\countMatchingCards(rank) = 4
        *this\hand\removeCards(rank)
        If player\isHuman()
          Print(#CRLF$ + #Indent$ + "You complete")
        Else
          Print(#CRLF$ + #Indent$ + *this\name + " completes")
        EndIf 
        PrintN(" a book of " + verboseCardInfo(rank) + StringField(#CardRankPlurals$, rank, ";") + ".")
        *this\ranksOpponentHas[rank - 1] = #False
        bookFound = #True
        *this\score + 1
        
        If player\ishuman()
          Print("** You now have ")
        Else
          Print("** " + player\getName() + " now has ")
        EndIf 
        Print(Str(player\getScore()) + " book"): If player\getScore() > 1: Print("s"): EndIf
        PrintN(".")
        Break
      EndIf 
    Next
  Until bookFound = #False
EndProcedure

Procedure newPlayer(isHuman = #False)
  Protected *newPlayer._membersPlayerClass = AllocateMemory(SizeOf(_membersPlayerClass))
  
  If *newPlayer
    *newPlayer\vtable = ?vTable_PlayerClass
    *newPlayer\score = 0
    If Not isHuman
      *newPlayer\name = "Computer"
    Else
      *newPlayer\isHuman = #True
      Protected name.s
      Print("What is your name?")
      name = Trim(Input())

      Select name
        Case ""
          name = "Human"
        Case "computer", "Computer"
          name = "*Computer*"
          *newPlayer\isHuman = #False
        Default
          If Left(name,1) <> UCase(Left(name,1))
            name = UCase(Left(name,1)) + LCase(Mid(name,2))
          EndIf
      EndSelect
      *newPlayer\name = name
    EndIf 
    *newPlayer\hand = newCardCollection()
    handleError(*newPlayer\hand,"Unable to initialize hand.")
  EndIf
  ProcedureReturn *newPlayer 
EndProcedure

Procedure game_Play(*this._membersGameClass)
  *this\deck\shuffle()
  Print(#CRLF$ + "Both players are dealt 9 cards.")
  *this\player[#HumanGamePlayer]\init(*this\deck)
  *this\player[#ComputerGamePlayer]\init(*this\deck)
  *this\player[#HumanGamePlayer]\updateScore()
  *this\player[#ComputerGamePlayer]\updateScore()
  
  PrintN(#Indent$ + *this\player[*this\turn ! 1]\getName() + " will go first.")
  PrintN(#CRLF$ + "-----------------------------------------------------------------------------")
  ;take turns until all books scored (i.e. no cards are in deck or hands)
  Repeat
    *this\turn ! 1
    *this\player[*this\turn]\takeTurn(*this\player[*this\turn ! 1], *this\deck)
    PrintN(#CRLF$ + "-----------------------------------------------------------------------------")
  Until *this\deck\count() = 0 And *this\player[*this\turn]\countCardsInHand() = 0
EndProcedure

Procedure game_DisplayWinner(*this._membersGameClass)
  Protected winningPlayer = #HumanGamePlayer
  
  If *this\player[#ComputerGamePlayer]\getScore() > *this\player[#HumanGamePlayer]\getScore()
    winningPlayer = #ComputerGamePlayer
  EndIf 
  PrintN(*this\player[winningPlayer]\getName() + " won with a score of " + Str(*this\player[winningPlayer]\getScore()) + " books.")
EndProcedure

Procedure newGame()
  Protected *newGame._membersGameClass = AllocateMemory(SizeOf(_membersGameClass))
  If *newGame
    *newGame\vtable = ?vTable_GameClass
    *newGame\player[#HumanGamePlayer] = newPlayer(#True) ;'human'
    *newGame\player[#ComputerGamePlayer] = newPlayer()   ;computer
    *newGame\deck = newCardCollection(#True)
    If *newGame\player[#HumanGamePlayer] = #Null Or *newGame\player[#ComputerGamePlayer] = #Null Or *newGame\deck = #Null
      PrintN("Unable to initialize.") 
      End ;all allocated memory is automatically freed at program's end
    EndIf 
    
    *newGame\turn = Random(1)
  EndIf 
  ProcedureReturn *newGame
EndProcedure

DataSection
  vTable_GameClass:
  Data.i @game_Play()
  Data.i @game_DisplayWinner()
  
  vTable_PlayerClass:
  Data.i @player_init()
  Data.i @player_getScore()
  Data.i @player_getName()
  Data.i @player_isHuman()
  Data.i @player_countCardsInHand()
  Data.i @player_takeTurn()
  Data.i @player_draw()
  Data.i @player_shiftHistory()
  Data.i @player_setHistory()
  Data.i @player_createDiffHistory()
  Data.i @player_request()
  Data.i @player_updateScore()
  
  vTable_CardCollectionClass:
  Data.i @CC_init()
  Data.i @CC_countCards()
  Data.i @CC_countMatchingCards()
  Data.i @CC_drawFrom()
  Data.i @CC_pushCard()
  Data.i @CC_popCard()
  Data.i @CC_removeCards()
  Data.i @CC_transferCards()
  Data.i @CC_shuffle()
  Data.i @CC_showCards()
  Data.i @CC_sortCards()
EndDataSection

handleError(OpenConsole(), "Couldn't open console.")
PrintN("===================")
PrintN("Let's play GO FISH.")
PrintN("===================")
Define game.GameObj = newGame()
handleError(game,"Couldn't initialize game.")

game\play()

PrintN(#CRLF$ + "Game over.")
game\displayWinner()

PrintN(#CRLF$ + "Press Enter to exit.")
Input()