Wednesday, September 17, 2014

Memory (Python / Pygame)

I highly recommend the Python tutorials by sentdex on Youtube. When I finished his series on pygame I went out looking for more projects.

I came across the Making Games with Python and Pygame textbook / e-book combo by Albert Sweigert. I've enjoyed the first few chapters so much that I want to add this to my bookshelf soon. I haven't read his section on Memory yet, but I decided to give it a try using what I know from sentdex's tutorials.

This game looks like this:
The player clicks on a white tile to expose its symbol. (Ints now, but those could be used as indices to an array of images.) The symbol is text drawn on a surface whose top left corner is lined up with the tile's. (I will center it later.) Each time a tile is revealed it is added to a list. When a tile was clicked on twice, it led to a nasty bug. But I think that's fixed with the check to see if the tile's index is in the list.

Here's the code:
import time
import random
import pygame

pad = 20
nsymb = 4
row,col = 4,4
ntiles = row*col
tile_h,tile_w = 100,100

white = (255,255,255)

disp_h = (row+1) * pad + row * tile_w
disp_w = (col+1) * pad + col * tile_h
disp = pygame.display.set_mode( (disp_h, disp_w) )

pygame.font.init()
font = pygame.font.Font('freesansbold.ttf',115)
clock = pygame.time.Clock()

class Tile:
    def __init__(self,i,j,symb):
        self.symb = symb
        self.reveal = False
        self.matched = False
        x = (i+1) * pad + i*tile_w
        y = (j+1) * pad + j*tile_h
        self.rect = pygame.Rect(x,y,tile_w,tile_h)
    def hide(self):
        self.reveal = False
    def show(self):
        self.reveal = True
        textSurf = font.render(self.__repr__(),True,(255,0,0))
        disp.blit(textSurf,self.rect.topleft)
    def __repr__(self):
        return str(self.symb) if self.reveal else "*"

def refresh(tiles,start=False):
    for tile in tiles:
        if start or tile.reveal and not tile.matched:
            tile.hide()
            pygame.draw.rect(disp,white,tile.rect)

def keep(shown,tile,index):
    shown.append(index)
    tile.matched = True

def end():    
    pygame.quit()
    quit()
    
def game_loop():
    c,r,s = range(col),range(row),range(nsymb)
    symbs = [i for i in s for j in range(int(ntiles/nsymb))]
    random.shuffle(symbs)
    tiles = [Tile(i,j,symbs[i+j*col]) for j in r for i in c]
    shown = []
    refresh(tiles,True)
    while len(shown) < ntiles:    
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                end()
            elif event.type == pygame.MOUSEBUTTONDOWN:
                loc = event.__dict__['pos']
                for i,tile in enumerate(tiles):
                    if tile.rect.collidepoint(loc):
                        tile.show()
                        pygame.display.update()
                        if i not in shown:
                            if len(shown) % 2 == 1:
                                j = shown.pop()
                                prev = tiles[j]
                                if tile.symb == prev.symb:
                                    keep(shown,tile,i)
                                    keep(shown,prev,j)
                                else:
                                    time.sleep(2)
                                    refresh(tiles)
                            else:
                                shown.append(i)
        pygame.display.update()
        clock.tick(30)
    end()
game_loop()

Saturday, August 9, 2014

War (Python)

Realized this week I knew some tricks to make a quick version of the card game War in Python. For the rules, I looked to Hoyle's Rules of Games (Second Revised Edition). I thought I knew the rules, but the ones I used led to unending games. Summarized:
  • A randomized deck is split between two players
  • The players draw the top card from their decks and add them to the pot
  • If the drawn cards are equal, the game goes into War
    • Players each draw three more cards and add them to the pot
    • Fourth cards are drawn
    • If these are equal, the war continues; these and three other cards go into the pot
  • When the cards aren't equal, the player with the highest wins and adds the pot to his deck
  • Hoyle also allows for "The first to win 3 wars wins the game"
I didn't see anything about if Ace is the highest or lowest, so I left that to war().

This is an exercise in using __x__() methods. I heard C++ let you rewrite the meanings of operators and recently found out Python does too. I use it in __gt__(self, other), which allows me to compare one card's rank to another with >. I'm pleased with how Pythonic the code looks for Card, Deck, and Player. They could all be improved, but war() needs the most work.

import random as r

war = False 

class Card:
    '''Cards have numbers as rank and strings as suits'''
    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit
    
    def __gt__(self, other):
        return self.rank > other.rank

    def __str__(self):
        face = ["J","Q","K"]
        if self.rank == 1 or self.rank == 14:
            rank = "A"    # If highA, then ace==14; else, ace==1
        elif self.rank < 10:
            rank = self.rank
        else:
            rank = face[self.rank-11] 
        return str(tuple( (rank, self.suit) ))
    
class Deck:
    '''A deck is any set of cards, owned by players or games''''
    def __init__(self, n=0, suits=None, highA=True):
       span = range(2,n+2) if highA else range(1,n+1)
       self.deck = [Card(i,j) for i in span for j in suits]

    # reveal deck, good for debugging but not used in game
    def __str__(self):
        return ','.join([str(card) for card in self.deck])

    # redefine += for easy appending
    def __iadd__(self, card):
        self.deck.append(card)
        return self

    def __next__(self):
        return self.deck.pop(0)
    
    def __len__(self):
        return len(self.deck)

    def shuffle(self):        
        r.shuffle(self.deck)

    def deal(self, hands):
        self.shuffle()
        while self.deck:
            for hand in hands:
                hand += self.__next__()

    # append cardlist to this deck
    def gets(self, cardlist):
        while cardlist:
            self += cardlist.pop(0)

class Player:
    '''Player #ID plays cards and wins wars'''    
    def __init__(self, ID):
        self.ID = ID
        self.hand = Deck()
        self.wars_won = 1

    def __len__(self):
        return self.wars_won

    def wins(self, potlist):
        global war
        print self.ID, "gets pot"
        self.hand.gets(potlist)
        # "continued" wars count as one win
        if war:
            self.wars_won += 1
            war = False

    def draw(self):
        return self.hand.__next__()

def war(war_ante=3, wins=3, highA=True):
    global war
    pot = Deck()
    p1,p2 = Player(1),Player(2)
    main = Deck(n=13, suits=['C','S','H','D'], highA=highA)
    main.deal([p1.hand,p2.hand])
    
    while len(p1.hand) and len(p2.hand) and len(p1)<wins and len(p2)<wins:
        p1card, p2card = p1.draw(), p2.draw()
        pot.gets([p1card,p2card])
        print "p1 plays", p1card, "\np2 plays", p2card
        if p1card > p2card:
            p1.wins(pot.deck)
        elif p2card > p1card:
            p2.wins(pot.deck)
        else:
            print "WAR"
            war = True
            for i in range(war_ante):                
                pot.gets([p1.draw(),p2.draw()])
        print "p1=",len(p1.hand),"\np2=",len(p2.hand),"\n"
    if len(p1.hand) and len(p2.hand):
        print "p1" if len(p1) > len(p2) else "p2", "wins"
    else:
        print "p1" if len(p1.hand) > 0 else "p2", "wins"

war()