Sunday, November 15, 2015

ASCII X-in-Box (Ruby)

After taking the Codecademy course on Ruby, I have started reading Eloquent Ruby by Russ Olsen. I saw a problem posted on a community for another language about making a kind of shape. I thought it would make a good exercise to try out what I have learned.

The shape is a box of numbers, hollow except for the diagonals. For example, a box of side-length 5 would look like

1 2 3 4 5
1 2 4 5
1 3 5
1 2 4 5
1 2 3 4 5

and one of side length 6 like

1 2 3 4 5 6
1 2 5 6
1 3 4 6
1 3 4 6
1 2 5 6
1 2 3 4 5 6

The thinking behind my solution is to separate printing the outside of the box from the inside. A function named interior? — ending with a question mark in the spirit of a Ruby convention for boolean functions — calculates this. The local input went out-of-scope, so I used a global $input instead.

To allow multi-digit numbers, I convert missing numbers into a string to find out how many characters would have been taken up.

I have a feeling there's a neat way to draw all this with just one if but it hasn't come to me yet. As you may be able to tell, I have enjoyed Ruby's parentheses-optional attitude. Also, the implicit return, like the one I use in my function.

Here's the code:

#!/usr/bin/ruby2.1

print "Input: "
$input = gets.chomp.to_i

def interior? index 
  0 < index and index < $input-1
end

$input.times do |row| 
  $input.times do |col|
    if interior? row and interior? col  
      if col == row or ($input-1)-col == row 
        print col + 1 
      else
        print " " * (col+1).to_s.length
      end 
    else
      print col + 1 
    end 
    print " " 
  end 
  puts 
end 

Friday, March 6, 2015

Flood-It (Python / Pygame)

Flood-It is a game where you're given a 14x14 grid of blocks randomly colored from a palette of 6 colors.



The objective is to get the whole grid to be one color. You start at the top-left corner, red/orange here, and progress your way across by clicking the palette on the right. My next move was to click green, which earned me two more pieces. For an added challenge the player must do this in 25 (or fewer) turns.

It's difficult but it can be done:

I keep track of tiles the user gains in a list called "blob." The tiles adjacent to ones in blob are called "neighbors." When the user chooses a color, I iterate over each tile in blob and find all its neighbors (if valid: corners, for example, only have two neighbors). Each neighboring tile gets added to the blob if it's the same color; otherwise it is added to the neighbor list.

I toyed with Pygame subsurfaces to make the panel. (Compare this to my 15 puzzle, where the score and restart button were added "on the same layer" as the game.) The code is a little messy here, but it's a nifty enough method of placing buttons that I'll work on cleaning it up in future projects.

Here's the code:

'''Flood It'''
import pygame as pyg, random

pyg.font.init()

YELLOW = (243,246,29)
PURPLE = (96,92,168)
GREEN = (126,157,30)
PINK = (237,112,161)
BLUE = (70,177,226)
RED = (220,74,32)
colors = [YELLOW, PURPLE, GREEN, PINK, BLUE, RED]

m,n = 14,14
pad = 15
size = 25
most = 25
area = (size,size)
mid = round(len(colors)/2)
p_wid = mid*(pad+size)+pad   # panel (non-game area) width
height,width = m*size,n*size+p_wid
window = pyg.display.set_mode( (width,height) )
pyg.display.set_caption("Flood-It!")

class Tile:
    board = window.subsurface(pyg.Rect(0,0,n*size,m*size))
    def __init__(self,i):
        self.i = i
        self.color = random.choice(colors)
        self.rect = pyg.Rect( ((i%m)*size,(i//n)*size), area)
        self.draw()
    def __repr__(self):
        return str(self.i)
    def draw(self):
        pyg.draw.rect(Tile.board,self.color,self.rect)

class Button:
    def __init__(self,i,color):
        gap = lambda x: x * (size+pad)
        self.color = color
        self.drect = pyg.Rect( (gap(i%mid),gap(i//mid)), area)
        self.rect = self.drect.move(*Pane.bpane.get_abs_offset())
        self.draw()
    def draw(self):
        pyg.draw.rect(Pane.bpane,self.color,self.drect)

class Board:    
    def __init__(self):
        '''
        A Board object has three lists:
          1) Board - a collection of ordered tiles
          2) Blob - tiles the player accrues
          3) Neighbors - tiles that may be added to blob
        '''
        self.board = [Tile(i) for i in range(m*n)]
        self.blob = [self.board[0]]
        self.neighbors = []
        self.make_blob(self.blob[0].color)
        
    def __getitem__(self,i):
        '''Board[i] gets ith item from Board.board'''
        return self.board[i]
    
    def get_neighbors(self,tile):
        '''For a given tile: fetches the ones beside, above,
           and below it'''
        i = tile.i
        dirs = [-n,1,n,-1] # north,east,south,west
        return [self[i+d] for d in dirs if self.is_valid(i+d,d)]

    def is_valid(self,i,j):
        '''Checks if index belongs to a valid neighbor'''
        if 0 <= i < m*n:
            # without these conditions, blob wraps around corners
            if j == 1:
                return i%m >= (i-j)%m
            elif j == -1:
                return i%m <= (i-j)%m 
            return True
        
    def make_blob(self,color):
        '''Adds more tiles to blob'''
        for cell in self.blob:
            cell.color = color
            for neighbor in self.get_neighbors(cell):
                if neighbor.color == cell.color:
                    if neighbor not in self.blob:
                        self.blob.append(self[neighbor.i])
                elif neighbor not in self.neighbors:
                    self.neighbors.append(self[neighbor.i])

class Text:
    '''A text block, optionally clickable'''
    def __init__(self,text,surf,font_size=25):
        surf.fill( (0,0,0) )
        font = pyg.font.SysFont('Arial',font_size)
        font_surf = font.render(text,font_size,(255,255,255))
        self.rect = font_surf.get_rect().move(*surf.get_abs_offset())
        surf.blit(font_surf, (0,0))
        
class Pane:
    a = size+pad
    b = p_wid-pad
    pane = window.subsurface(pyg.Rect(n*size,0,p_wid,height))
    bpane = pane.subsurface(pyg.Rect(pad,pad,b,2*a))
    click = pane.subsurface(pyg.Rect(pad,3*(size+pad),b,2*a))
    replay = pane.subsurface(pyg.Rect(pad,5*(size+pad),b,a))
    end = pane.subsurface(pyg.Rect(pad,7*(size+pad),b,a))
    def __init__(self):
        self.buttons = [Button(i,c) for i,c in enumerate(colors)]
        self.restart = Text('Replay',Pane.replay)
        self.end = Text('Quit',Pane.end)
        self.update()
    def update(self,count=0):
        self.click = Text('Clicks: %d/%d' % (count,most),Pane.click)
        

def flood_it():
    board = Board()
    pane = Pane()
    clicks = 0
    play = True
    while True:
        for event in pyg.event.get():
            if event.type == pyg.QUIT:
                pyg.quit()
                quit()
            if event.type == pyg.MOUSEBUTTONDOWN:                
                for button in pane.buttons:
                    if button.rect.collidepoint(event.pos):
                        if play:
                            board.make_blob(button.color)
                            [tile.draw() for tile in board.board]
                            clicks += 1
                            pane.update(clicks)
                        if len(board.blob) == m*n or clicks >= most:
                            play = False
                    elif pane.restart.rect.collidepoint(event.pos):
                        flood_it()
                    elif pane.end.rect.collidepoint(event.pos):
                        pyg.quit()
                        quit()
        pyg.display.update()

flood_it()

Friday, February 27, 2015

Simon (Python / Pygame)

This project is based on the Milton Bradley toy called Simon.  The toy adds a note/color to a sequence, which the player must play back. The original has 4 possible notes and colors. According to the Wiki, the originals are 
A link in the comments helped me associate these with the correct numbers. On that note, this StackOverflow answer by jose helped a lot to use pygame.midi.

I wanted to make a Simon that was simple but true to the original. Listed below are some features added to that end.

Levels of Difficulty

I have levels of difficulty that are considered a win when a sequence of a given length is repeated back. The levels 1-4 correspond to the Wiki's list of 8, 14, 20, and 31. For historical value (i.e., I used it to test) there is a level 0 that asks for a user to play back a note. There is also an unending level. Selection of difficulty is the first screen the user sees.


Sequences Gradually Quicken

I noticed in LuckyPennyShop.com's YouTube video Vintage Electronic Simon Game 1978 Milton Bradley Toys that the sequence not only got longer but played faster. This not only makes the game more difficult, but it also brings you to the new information faster. For this I used a variation of

where 0 < c < 1 is a non-negative scaling constant and ell is the length of the list. (c = 1 made sequences play too quickly for long sequences.) A benefit to this function is that it decreases, but levels out asymptotically to zero. We can add a minimum m not to go faster than.

Correct Tile Blinks On Miss

I noticed that the ending tile in the video above blinks several times when the sequence is endedcorrectly, if I followed the video right. I resort to an end screen. However, for incorrect tiles the tile that should have been pressed blinks 4 times. (I don't know if this happens in the original.)

Images of Gameplay

A board of unlit tiles looks like this:



Compare to this one, where the red button is lit:

The reason I have the number of tiles n as a constant was because I hoped to expand this to even more tiles. (I have tried with 9.) However, on top of the ideal case of having more colors and sounds for the tiles, the brightening feature (the "bright" tuple in Button.draw()) ought to be changed: Right now it naively turns any nonzeros into 255 and leaves zeros alone. If we had a tile of (132, 210, 0) for example, its bright color is the same as our yellow's, which is given by (150,150,0).

When the game is over this end screen comes up:


The "sequence" box is mostly for the edification of those who chose infinite mode. Here we see the second number is 31, which means the player was on difficulty 4. The 7 is the size of the sequence that the user didn't play back correctly.

Clicking Replay takes the user back to the difficulty screen and Quit exits. There are several places where I call the end() functions. An improvement might be adding threads, one that keeps track of when the window's "x" button is clicked and another for inside the game.

Here's the code:


'''Simple Simon'''

import math, time, pygame, pygame.midi, random

pygame.midi.init()
pygame.font.init()
audio = pygame.midi.Output(0)
audio.set_instrument(4,1)

# midi chart: http://www.electronics.dit.ie/staff/tscarff/Music_
#             technology/midi/midi_note_numbers_for_octaves.htm
BLACK = (255,255,255)
colors = [(0,150,0),(150,0,0),(150,150,0),(0,0,150)] #g,r,y ,b
sounds = [52,57,61,64]                               #e,a,c#,e_hi
hardness = {'0':1,'1':8, '2':14, '3':20, '4':31, '∞': -1}

size = 200
time_scale = .1
min_pause = .1
n = math.ceil(math.sqrt(len(colors)))
win_size = n*size
mid = round(n*size/2)
window = pygame.display.set_mode( (win_size, win_size) )
pygame.display.set_caption("Simple Simon")

class Pane:
    def __init__(self,text,pos,font_size=40,color=BLACK,fun=None):
        self.fun = fun
        self.text = text
        font = pygame.font.SysFont('Arial', font_size)
        surf = font.render(text, True, color)
        self.rect = surf.get_rect()
        self.rect.center = pos
        window.blit(surf, self.rect.topleft)        

class Button:
    def __init__(self,i):
        self.x = size * (i%n)
        self.y = size * (i//n)
        self.color = colors[i]
        self.sound = sounds[i]
        self.rect = pygame.Rect( (self.x,self.y), (size,size) )
        self.draw()
    def draw(self, lit=False, wait=0):
        bright = tuple(255 if i else 0 for i in self.color)
        color = bright if lit else self.color
        if lit:
            audio.note_on(self.sound,127,1)
        else:
            audio.note_off(self.sound,127,1)
        pygame.draw.rect(window,color,self.rect)        
        pygame.display.update()
        time.sleep(wait)

def end():
    audio.close()
    pygame.midi.quit()
    pygame.quit()
    quit()

def choose_mode(ts=60,ms=40):
    window.fill( (0,0,0) )
    while True:
        modes = enumerate(sorted(hardness.keys()))
        title = Pane('Select hardness', (mid,ts/2), ts)
        diffs = [Pane(x,(mid,ms*i+1.5*ts),ms,) for i,x in modes]
        pygame.display.update()
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                end()
            if event.type == pygame.MOUSEBUTTONDOWN:
                for diff in diffs:
                    if diff.rect.collidepoint(event.pos):
                        return diff.text
                        
def simon():
    player,wrong = False,False
    sequence = []
    correct = 0
    mode = choose_mode()    
    tiles = [Button(i) for i in range(len(colors))]
    while True:
        # player's turn
        if player:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    end()
                if event.type == pygame.MOUSEBUTTONDOWN:
                    for tile in tiles:
                        if tile.rect.collidepoint(event.pos):
                            tile.draw(lit=True)
                            if tile == sequence[correct]:
                                correct += 1
                            else:
                                wrong = True
                # letting go of mouse button redraws all tiles
                elif event.type == pygame.MOUSEBUTTONUP:
                    [tile.draw() for tile in tiles]
                    # correct tile blinks on miss
                    if wrong:
                        right = sequence[correct]
                        for i in range(4):
                            right.draw(lit=True,wait=.1)
                            right.draw(wait=.1)
                        end_screen(len(sequence),mode,'Lost')
                    # player used correct sequence 
                    elif correct == len(sequence):
                        correct = 0
                        player = False
        # computer's turn
        else:
            chain = len(sequence)
            pause = math.exp(-time_scale*chain)
            # player wins, if not in infinite mode
            if chain == hardness[mode]:
                end_screen(chain, mode)
            # pause before showing next sequence
            time.sleep(min_pause + pause)
            sequence.append(random.choice(tiles))
            for tile in sequence:
                tile.draw(lit=True,wait=pause)
                tile.draw(wait=min_pause*pause)
                # player can quit mid-sequence
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        end()
            player = True
        pygame.display.update()
                    
def end_screen(streak, mode, result='Won', fsize=40):    
    funs = [None, None, simon, end]
    most = str(hardness[mode]) if mode.isalnum() else '∞'
    texts = ['You %s!' % result,
             'Sequence: %d/%s' % (streak,most),
             'Replay', 'Quit']
    data = enumerate(zip(texts,funs))
    panes = [Pane(x[0],(mid,(i+1)*1.5*fsize),fsize,fun=x[1]) for i,x in data]
    pygame.display.update()
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                end()
            if event.type == pygame.MOUSEBUTTONDOWN:
                for pane in panes:
                    if pane.rect.collidepoint(event.pos):
                        if pane.fun:
                            pane.fun()

simon()

Sunday, February 22, 2015

15 Puzzle (Python / Pygame)

In the 15 Puzzle you are given a permutation of 15 numbers in 16 slots. The puzzle is solved when the numbers are put in order with an empty spot in the 16th. Some papers I read generalized this to be "The (n**2 - 1) Puzzle." This code allows for that, though some resizing of globals may be in order to ensure that the game fits on the screen.

Not every permutation is solvable. The check in Board.permute()is found on WolframAlpha's page for the 15 puzzle.The idea is: For each tile, read left-to-right and top-to-bottom, you count the number of tiles smaller than it. Then you add the row the blank is on. If this number is even, we have a solvable permutation.

My last addition allows Multi-Tile Moves (MTM), which means that you can move more than one tile with one click if they are in a straight line.

Here is an example for n = 4. I have found a way to center the numbers in their tiles since Memory






A brief example of the WolframAlpha method:
  • 12 has no numbers before it, so we add 0
  • 1 has no numbers before it that are less than 1, so we add 0
  • 2 has 1 before it, so we add 1
  • 7 has 1 and 2 before it, so we add 2. Our tally is 3
  • ...
  • 13 has the numbers 1-12 before it, so we add 12 to our tally
  • The blank is on the fourth row, so we add 4 to our tally
  • The tally must be divisible by 2, because Python chose this as a valid permutation
As a demonstration of the MTM, note that the 13 block in the following image was clicked to move both the 9 and the 13 to the left. The `Click` tally is therefore 1 instead of 2, because only one tile was clicked even though two were moved.



I solved the above in 58 clicks using a method similar to the one shown on Wikihow. I would like to make the Game Won screen a little less anticlimactic: At the moment it looks like a regular screen. The user is expected to press Restart if they want to play again or the window exit button if they wish to quit.



Here is the code:
# 16-blocks

import pygame, random

pygame.init()
pygame.font.init()

n = 4
pad = 50
toolbar = 100
box_size = 150
side = n*box_size + (n + 1)*pad 
window = pygame.display.set_mode( (side, side+toolbar) )
pygame.display.set_caption("15-Blox")
font = pygame.font.SysFont('Verdana', 60)

WHITE = (255,255,255)
BLACK = (0,0,0)

clicks = 0

class Tile:
    def __init__(self, i, j):
        self.x = (i+1)*pad + i*box_size
        self.y = (j+1)*pad + j*box_size 
        self.id = i + j * n
        self.val = self.id
        self.color = WHITE
        self.surf = pygame.Surface( (box_size, box_size) )
        self.rect = pygame.Rect((self.x, self.y),(box_size,box_size))
        self.draw()
    def draw(self):
        if self.val != 0:            
            pygame.draw.rect(window, WHITE, self.rect)
            num_surf = font.render(str(self.val), True, BLACK)
            num_rect = num_surf.get_rect()
            num_rect.center = self.rect.center
            window.blit(num_surf, (num_rect.x, num_rect.y) )
        else:
            pygame.draw.rect(window, BLACK, self.rect)
    def __repr__(self):
        return str(self.val)

class Board:
    win_state = [i for i in range(1,n*n)] + [0]
    def __init__(self):
        # tiles in order 0-15
        self.board = [Tile(i,j) for j in range(n) for i in range(n)]
        self.vals = [i for i in range(n*n)]
        self.permute()
    def draw(self):
        window.fill(BLACK)
        for tile in self.board:
            tile.draw()
    def swap(self, clicked, index):
        global clicks
        if clicked.val != 0:
            way = 0
            if self.zero//n == index//n: # same row
                way = 1 if self.zero < index else -1
            elif self.zero%n == index%n: # same col
                way = n if self.zero < index else -n
            if way:
                for i in range(self.zero, index+way, way):
                    self.vals[self.zero] = self.vals[i]
                    self.vals[i] = 0
                    self.update()
                clicks += 1
    def update(self):
        self.zero = self.vals.index(0)
        for i,tile in enumerate(self.board):
            tile.val = self.vals[i]
    def permute(self):
        while True:
            random.shuffle(self.vals)
            self.update()
            e = self.vals.index(0) // n # row of zero
            counts = []
            for i,tile in enumerate(self.board):
                for j in range(i):
                    if 0 < self.vals[j] < tile.val:
                        counts.append(1)
            if (sum(counts) + e) % 2 == 0:
                return
    
def blox():
    global clicks
    clicks = 0
    board = Board()
    while True:
        board.draw()
        
        # stats pane
        surf = font.render('Clicks: ' + str(clicks), True, WHITE)
        rect = surf.get_rect()
        rect.bottomleft = (0, side+toolbar)
        window.blit(surf, (rect.x, rect.y) )
        
        # restart button
        restart_surf = font.render('Restart', True, WHITE)
        restart_rect = restart_surf.get_rect()
        restart_rect.bottomright = (side, side+toolbar)
        window.blit(restart_surf, (restart_rect.x, restart_rect.y) )

        # event loop
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                pygame.font.quit()
                quit()
            if event.type == pygame.MOUSEBUTTONDOWN:
                if restart_rect.collidepoint(event.pos):
                    blox()
                for index,tile in enumerate(board.board):
                    if tile.rect.collidepoint(event.pos):
                        board.swap(tile, index)
                    if board.vals == Board.win_state:
                        break

        pygame.display.update()

blox()


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()