Go Fish/Python

From Rosetta Code
Go Fish/Python is part of Go Fish. You may find other members of Go Fish at Category:Go Fish.

OOP

Works with: Python version 2.5
import random
import sys
from collections import defaultdict

class HumanPlayer(object):
    def __init__(self,deck):
        self.hand = defaultdict(int)
        self.book = []
        self.deck = deck #making a copy of deck, all changes within
                        #this class should affect the global deck
        self.score = 0
        self.name = raw_input('Name yourself: ')
        
    def Draw(self): #assuming that deck is a global
        cardDrawn = self.deck.pop() #removes the last card from deck
        self.hand[cardDrawn] += 1 #adds card to hand
        print '%s drew %s.' % (self.name,cardDrawn)
        self.checkForBooks()
        
    def checkForBooks(self):
#       Removes all items of which are 4.
        for key,val in self.hand.items(): #can't use iteritems() because we are modifying hand in loop
            if val == 4: #completed a book
                self.book.append(key)
                print '%s completed the book of %s\'s.' % (self.name,key)
                self.score += 1
                del self.hand[key]
        self.emptyCheck()

    def emptyCheck(self):
        if len(self.deck)!=0 and len(self.hand)==0: #checks if deck/hand is empty
            self.Draw()
    def displayHand(self): #Displays current hand, cards separated by spaces
        return ' '.join(key for key,val in self.hand.iteritems()
                        for i in range(val)) #meh, make it prettier

    def makeTurn(self):
        print '%s\'s hand: %s' % (self.name,self.displayHand())
        chooseCard = raw_input('What card do you ask for? ').strip()
        if chooseCard == 'quit':
            sys.exit(0)
        if chooseCard not in self.hand:
            print 'You don\'t have that card. Try again! (or enter quit to exit)'
            chooseCard = self.makeTurn()
        return chooseCard
    
    def fishFor(self,card):
        if card in self.hand: # if card in hand, returns count and removes the card from hand
            val = self.hand.pop(card)
            self.emptyCheck()
            return val
        else:
            return False
    def gotCard(self,card,amount):
        self.hand[card] += amount
        self.checkForBooks()
        
        
        
class Computer(HumanPlayer):
    def __init__(self,deck):
        self.name = 'Computer'
        self.hand = defaultdict(int)
        self.book = []
        self.deck = deck
        self.opponentHas = set()
        self.score = 0

    def Draw(self): #assuming that deck is a global
        cardDrawn = self.deck.pop() #removes the last card from deck
        self.hand[cardDrawn] += 1 #adds card to hand
        print '%s drew a card.' % (self.name)
        self.checkForBooks()

    ##AI: guesses cards that knows you have, then tries cards he has at random.
    ##Improvements: remember if the card was rejected before, guess probabilities
    def makeTurn(self):
#        print self.displayHand(),self.opponentHas
        candidates = list(self.opponentHas & set(self.hand.keys())) #checks for cards in hand that computer knows you have
        if not candidates:
            candidates = self.hand.keys() #if no intersection between those two, random guess
        move = random.choice(candidates)
        print '%s fishes for %s.' % (self.name,move)
        return move
    
    def fishFor(self,card): #Same as for humans players, but adds the card fished for to opponentHas list.
        self.opponentHas.add(card)
        if card in self.hand: # if card in hand, returns count and removes the card from hand
            val = self.hand.pop(card)
            self.emptyCheck()
            return val
        else:
            return False
    
    def gotCard(self,card,amount):
        self.hand[card] += amount
        self.opponentHas.discard(card)
        self.checkForBooks()
        
class PlayGoFish(object):
    def __init__(self):
        self.deck = ('2 3 4 5 6 7 8 9 10 J Q K A '*4).split(' ')
        self.deck.remove('')
        self.player = [HumanPlayer(self.deck),Computer(self.deck)] #makes counting turns easier

    def endOfPlayCheck(self):#checks if hands/decks are empty using the any method
            return self.deck or self.player[0].hand or self.player[1].hand
        
    def play(self):
        random.shuffle(self.deck)
        for i in xrange(9): # Deal the first cards
            self.player[0].Draw()
            self.player[1].Draw()
        turn = 0
        while self.endOfPlayCheck():
            print '\nTurn %d (%s:%d %s:%d) %d cards remaining.' % (turn,self.player[0].name,
                    self.player[0].score,self.player[1].name,self.player[1].score,len(self.deck))
            whoseTurn = turn%2
            otherPlayer = (turn+1)%2
            while True: #loop until player finishes turn
                cardFished = self.player[whoseTurn].makeTurn()
                result = self.player[otherPlayer].fishFor(cardFished)
                if not result: #Draws and ends turn
                    self.player[whoseTurn].Draw()
                    break
                print '%s got %d more %s.' % (self.player[whoseTurn].name,result, cardFished)
                self.player[whoseTurn].gotCard(cardFished,result)
                if not self.endOfPlayCheck(): break
            turn+=1
        print '\nScores: \n%s: %d\n%s: %d\n' % (self.player[0].name,self.player[0].score,
                                          self.player[1].name,self.player[1].score)
        if self.player[0].score>self.player[1].score:
            print self.player[0].name,'won!'
        elif self.player[0].score==self.player[1].score:
            print 'Draw!'
        else:
            print self.player[1].name,'won!'

if __name__=="__main__":
    game = PlayGoFish()
    game.play()

Procedural

Works with: Python version 3.6
"""
Plays a Go Fish game between a user and computer
following the rules specified at
http://www.rosettacode.org/wiki/Go_Fish
"""
import itertools
import random
from copy import deepcopy
from operator import attrgetter
from typing import (Counter,
                    Iterable,
                    Iterator,
                    List,
                    NamedTuple,
                    NewType,
                    Sequence,
                    Set,
                    Tuple,
                    TypeVar)

Card = NewType('Card', str)
Hand = Counter[Card]
Deck = Tuple[Card, ...]
Watchlist = Set[Card]
T = TypeVar('T')

SUITS_COUNT = 4
RANKS = ('2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A')
DECK = RANKS * SUITS_COUNT

EMPTY_HAND_MESSAGE = "{} is without cards. They go fishing."
CARD_REQUEST_MESSAGE = "{player_name} asks for {card}"
NO_CARD_MESSAGE = "{player_name} doesn't have {card}"
BOOK_COMPLETED_MESSAGE = "{player_name} completed the book of {card}'s."
FISHING_RESULT_MESSAGE = "{player_name} fished a {card}"


class Player(NamedTuple):
    name: str
    hand: Hand
    is_human: bool
    score: int = 0
    is_lucky: bool = True  # if False, turn goes to another player
    watchlist: Watchlist = set()  # used by AI to track enemy's cards


def play(deck: Deck = DECK,
         *,
         first_hand_count: int = 9,
         lucky_fishing: bool = False) -> None:
    """
    Plays the Go Fish game according to
    http://www.rosettacode.org/wiki/Go_Fish
    :param deck: deck from where the cards are drawn
    :param first_hand_count: starting amount of cards for a player
    :param lucky_fishing: if True, continue playing
    after fishing the card from the deck
    that was asked from an opponent
    """
    deck = shuffle(deck)
    deck, (hand, other_hand) = deal_first_hands(deck, count=first_hand_count)
    human = Player(name=input('Name yourself: '),
                   hand=hand,
                   is_human=True)
    ai = Player(name='Computer',
                hand=other_hand,
                is_human=False)
    human = check_hand_for_books(human)
    ai = check_hand_for_books(ai)

    player, opponent = ai, human
    for turn in itertools.count(1):
        player, opponent = opponent, player
        print_turn_stats(turn=turn,
                         players=(player, opponent),
                         cards_left=len(deck))
        while cards_in_game(deck, hands=(player.hand, opponent.hand)):
            player, opponent, deck = play_turn(player,
                                               opponent,
                                               deck,
                                               lucky_fishing=lucky_fishing)
            if not player.is_lucky:
                break
        else:
            print_final_stats((player, opponent))
            return


def shuffle(deck: Deck) -> Deck:
    """Returns a shuffled deck"""
    deck = list(deck)
    random.shuffle(deck)
    return tuple(deck)


def deal_first_hands(deck: Deck,
                     count: int) -> Tuple[Deck, Tuple[Hand, Hand]]:
    """Gives count cards to each player"""
    hands = (Hand(), Hand())  # type: Tuple[Hand, Hand]
    for hand in ncycles(hands, count):
        card, deck = fish(deck)
        hand[card] += 1
    return deck, hands


def fish(deck: Deck) -> Tuple[Card, Deck]:
    """Returns a fished card from a deck, and a new deck"""
    return deck[-1], deck[:-1]


def ncycles(iterable: Iterable[T], n: int) -> Iterator[T]:
    """Returns the sequence elements n times"""
    repeat = itertools.repeat(tuple(iterable), n)
    return itertools.chain.from_iterable(repeat)


def check_hand_for_books(player: Player) -> Player:
    """
    Walks through cards in the hand,
    removes books and adds corresponding score
    """

    def is_book(card_and_count: Tuple[Card, int]) -> bool:
        """
        For a pair of card-count
        checks if count equals the number of suits
        """
        return card_and_count[1] == SUITS_COUNT

    player = deepcopy(player)
    cards_counts = player.hand.items()
    books = list(filter(is_book, cards_counts))  # type: List[Tuple[Card, int]]
    for card, _ in books:
        player.hand.pop(card)
    return player._replace(score=player.score + len(books))


def print_turn_stats(*,
                     turn: int,
                     players: Sequence[Player],
                     cards_left: int) -> None:
    """Prints stats of the current turn"""
    name_score = name_score_pairs(players, sep=' ')
    print(f'\nTurn {turn} ({name_score}) {cards_left} cards remaining.')


def name_score_pairs(players: Sequence[Player],
                     *,
                     sep: str) -> str:
    """Returns a string of pairs of 'name: score'; human goes first"""
    players = sorted(players,
                     key=attrgetter('is_human'),
                     reverse=True)
    name_score_generator = map('{0.name}: {0.score}'.format, players)
    return sep.join(name_score_generator)


def cards_in_game(deck: Deck,
                  hands: Iterable[Hand]) -> bool:
    """Checks if hands or the deck have any cards"""
    return deck or any(hands)


def play_turn(player: Player,
              opponent: Player,
              deck: Deck,
              *,
              lucky_fishing: bool) -> Tuple[Player, Player, Deck]:
    """Plays one turn"""
    player, deck = replenish_card(player, deck=deck)
    opponent, deck = replenish_card(opponent, deck=deck)

    if player.is_human:
        requested_card, watchlist = human_asks_card(
            hand=player.hand,
            watchlist=opponent.watchlist)
        opponent = opponent._replace(watchlist=watchlist)
    else:
        requested_card = ai_asks_card(player)

    if requested_card in opponent.hand:
        player, opponent = correct_guess_actions(player=player,
                                                 opponent=opponent,
                                                 card=requested_card)
    else:
        player, deck = wrong_guess_actions(
            player=player,
            opponent=opponent,
            requested_card=requested_card,
            deck=deck,
            lucky_fishing=lucky_fishing)
    return player, opponent, deck


def print_final_stats(players: Sequence[Player]) -> None:
    """Prints final stats"""
    name_score = name_score_pairs(players, sep='\n')
    print(f'\nScores: \n{name_score}\n')
    scores = list(map(attrgetter('score'), players))
    if all(score == scores[0] for score in scores):
        print('Draw!')
    else:
        winning_player = max(players, key=attrgetter('score'))
        print(winning_player.name, 'won!')


def replenish_card(player: Player,
                   *,
                   deck: Deck) -> Tuple[Player, Deck]:
    """Returns a player with a card drawn from a deck"""
    player = deepcopy(player)
    if not player.hand:
        print(EMPTY_HAND_MESSAGE.format(player.name))
        card, deck = fish(deck)
        player.hand[card] += 1
        print_fishing_result(player=player,
                             card=card)
    return player, deck


def human_asks_card(*,
                    hand: Hand,
                    watchlist: Watchlist) -> Tuple[Card, Watchlist]:
    """Set of actions for when human asks a card from computer"""
    watchlist = watchlist.copy()
    print_hand(hand)
    requested_card = ask_for_card(hand=hand)
    watchlist.add(requested_card)
    return requested_card, watchlist


def ai_asks_card(player: Player) -> Card:
    """Set of actions for when computer asks a card from human"""
    requested_card = request_card(player.hand,
                                  watchlist=player.watchlist)
    print_message(CARD_REQUEST_MESSAGE,
                  player_name=player.name,
                  card=requested_card)
    return requested_card


def correct_guess_actions(player: Player,
                          opponent: Player,
                          card: Card):
    """
    Set of actions for when player guesses correctly card
    in a hand of an opponent
    """
    player = deepcopy(player)
    opponent = deepcopy(opponent)
    card_count = opponent.hand.pop(card)
    print(f'{player.name} got {card_count} more {card} '
          f'from {opponent.name}.')
    player.hand[card] += card_count
    player = check_book(player, card)
    if not player.is_human:
        player.watchlist.discard(card)
    player = player._replace(is_lucky=True)
    return player, opponent


def wrong_guess_actions(*,
                        player: Player,
                        opponent: Player,
                        requested_card: Card,
                        deck: Deck,
                        lucky_fishing: bool) -> Tuple[Player, Deck]:
    """
    Set of actions for when player guesses incorrectly card
    in a hand of an opponent.
    `lucky_fishing` determines if player can continue playing
    after fishing the wanted card
    """
    player = deepcopy(player)
    print_message(NO_CARD_MESSAGE,
                  player_name=opponent.name,
                  card=requested_card)
    card, deck = fish(deck)
    player.hand[card] += 1
    print_fishing_result(player=player,
                         card=card)
    player = check_book(player, card)
    if lucky_fishing:
        player = player._replace(is_lucky=card == requested_card)
    else:
        player = player._replace(is_lucky=False)
    if player.is_lucky:
        print("What a luck! "
              "The fished card was the same as requested!")
    return player, deck


def print_hand(hand: Hand) -> None:
    """Prints a hand in the following form: 'Q Q 7 7 7 3'"""
    ranks = itertools.starmap(itertools.repeat, hand.items())
    print(*itertools.chain.from_iterable(ranks))


def ask_for_card(hand: Hand) -> Card:
    """Asks user for a card, and checks if it is in their hand"""
    while True:
        asked_card = Card(input('What card do you ask for? '))
        if asked_card in hand:
            return asked_card
        print("You don't have that card. Try again!")


def request_card(hand: Hand, watchlist: Watchlist) -> Card:
    """AI-like choice for a card that will be asked from a human"""
    candidates = list(watchlist & set(hand.keys()))
    if not candidates:
        candidates = list(hand.keys())
    return random.choice(candidates)


def print_message(message: str,
                  *,
                  player_name: str,
                  card: str) -> None:
    """Prints a template message"""
    print(message.format(player_name=player_name,
                         card=card))


def check_book(player: Player,
               card: Card) -> Player:
    """
    Checks if player has all cards of specified type,
    removes them if true, and adds +1 to score
    """
    player = deepcopy(player)
    if player.hand[card] == SUITS_COUNT:
        print_message(BOOK_COMPLETED_MESSAGE,
                      player_name=player.name,
                      card=card)
        player.hand.pop(card)
        player = player._replace(score=player.score + 1)
    return player


def print_fishing_result(*,
                         player: Player,
                         card: Card) -> None:
    """
    Prints result of fishing.
    Showing card or not depends on if a player is a human.
    """
    card_stub = str(card) if player.is_human else 'card'
    print_message(FISHING_RESULT_MESSAGE,
                  player_name=player.name,
                  card=card_stub)


if __name__ == "__main__":
    play()