All computer source code presented on this page, unless it includes attribution to another author, is provided by Ed Halley under the Artistic License. Use such code freely and without any expectation of support. I would like to know if you make anything cool with the code, or need questions answered.

__all__ = [ 'Card', 'Deck' ]

import ucsv as csv
import random
import time

class Card (object):

    def __init__(self, sides, **kwargs):
        if hasattr(sides, 'items'):
            self.names = [ pair[0] for pair in sides.items() if pair[1] ]
            self.sides = [ pair[1] for pair in sides.items() if pair[1] ]
            self.names = [ ('SIDE%d' % n) for n in range(len(sides)) ]
            self.sides = sides
        self.index = dict(zip( self.names, range(len(self.names)) ))
        self.stats = kwargs
        self.shown = 0

    def show(self, side):
        if side in self.index: side = self.index[side]
        if side < 0 or side >= len(self.sides):
            raise ValueError, 'index out of range'
        self.shown = side

    def side(self, side):
        if side in self.index: side = self.index[side]
        if side < 0 or side >= len(self.sides):
            return None
        return self.sides[side]

    def has(self, side):
        return side in self.index

    def stat(self, var, val=None):
        if var in self.stats:
            if val is not None:
                self.stats[var] = val
            return self.stats[var]
        return val

    def __repr__(self):
        t = 'Card(names=%r, sides=%r, stats=%r)' % (self.names, self.sides, self.stats)
        return t

    def __str__(self):
        return u'%s' % self.sides[self.shown]

class Deck (object):

    def __init__(self, cards=None):
        if cards is None: cards = [ ] = cards
        self.heads = [ ]

    def __len__(self):
        return len(

    def __nonzero__(self):
        return len( > 0

    def __getitem__(self, n):

    def shuffle(self):

    def discard(self, count=1):
        count = min(count, len( =[:-count]

    def keep(self, count=1):
        count = min(count, len( =[:count]

    def loadCsv(self, filename, stats=None):
        if stats is None: stats = { }
        if isinstance(stats, (list,set,tuple)):
            stats = dict(zip( stats, [0,]*len(stats) ))
        f = file(filename, 'rb')
        c = csv.DictReader(f)
        for row in c:
            if not self.heads: self.heads = c.fieldnames
            s = { }
            for stat in stats:
                s[stat] = row.pop(stat, None) or stats[stat]
            c = Card(row, **s)
            if c.sides:
        return len(

    def saveCsv(self, filename):
        f = file(filename, 'wb')
        c = csv.DictWriter(f, self.heads, restval=u'')
        for card in
            row = card.stats.copy()
            for n in range(len(card.sides)):
                row[card.names[n]] = card.sides[n]
        return len(

    def similar(self, common=None, varies=None, tested=None):
        if common is None: common = self.heads
        if isinstance(common, (list, tuple, set)):
            common = random.choice(common)
        match = None
        tries = 0
        while not match and tries < 100:
            tries += 1
            match = random.choice(
            if not match.has(tested): continue
            if not match.has(varies): continue
            if not match.has(common): continue
            match = match.side(common)
        if not match: return None
        hand = [ card for card in
                 if card.side(common) == match
                 and card.has(varies)
                 and card.has(tested) ]
        if not hand: return None
        return Deck(hand)

def __quiz(deck, common, varies, tested, count=4):
    hand = None
    tries = 0
    while not hand:
        tries += 1
        if tries > 100: return
        hand = deck.similar(common, varies, tested)
        if not hand: continue
        if len(hand) < count:
            hand = None
    quiz = random.randrange(0, count)
    print 'QUIZ:', hand[quiz].side(tested), '-'
    for n in range(count):
        card = hand[n]
        print u"(%s):  %s" % (chr(ord('a')+n),
    hit = False
    choices = ', '.join( [ '(%s)' % chr(ord('a')+n) for n in range(count) ] )
    guess = raw_input('Answer %s, or (Enter) to skip: ' % choices)
    if len(guess) > 0:
        hand[quiz].stats['SEEN'] += 1
        hand[quiz].stats['LAST'] = int(time.time())
        guess = ord(guess[0])-ord('a')
        if guess == quiz:
            print 'Correct!'
            hit = True
            print 'Incorrect.'
        hand[quiz].stats['HITS'] += 1
    shown = [ hand[quiz].side(x) for x in hand[quiz].names if x != common ]
    print ' = '.join(shown)
    return hit

if __name__ == '__main__':

    filename = 'vocab.csv'
    d = Deck()
    d.loadCsv(filename, 'SEEN,HITS,LAST'.split(','))
    dirty = False

    import os
    import sys
    import romaji

    args = sys.argv
    while args:

        arg = args.pop(0)

        if arg in ['-q','--quiz']:

            count = 10
                count = args.pop(0)
                count = int(count)
            quizzes = [ {'common':'POS','varies':'KANA','tested':'ENGLISH'},
                        {'common':'POS','varies':'KANA','tested':'KANJI'} ]
            for n in range(count):
                q = random.choice(quizzes)
                __quiz(d, **q)
            dirty = True

        if arg in ['-a','--add']:
            dirty = True

    if dirty:
        os.rename(filename, filename+'~')
        print 'Updated %s (%d cards).' % (filename, len(

Contact Ed Halley by email at
Text, code, layout and artwork are Copyright © 1996-2013 Ed Halley.
Copying in whole or in part, with author attribution, is expressly allowed.
Any references to trademarks are illustrative and are controlled by their respective owners.
Make donations with PayPal - it's fast, free and secure!