Skip to content

Instantly share code, notes, and snippets.

@gulan
Created September 12, 2015 00:16
Show Gist options
  • Select an option

  • Save gulan/076cb442bf500ac54603 to your computer and use it in GitHub Desktop.

Select an option

Save gulan/076cb442bf500ac54603 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
import fcntl
import os
import random
import sqlite3
import sys
import termios
"""
Work though a deck of flashcards. Learned cards are discarded. Missed
cards are saved to a retry deck. When the draw deck is empty, it is
replaced by a shuffled retry deck. The game ends when all the cards
have been learned.
This script is a simplified variant of another flashcard program that
I wrote. Here, the user responses are restricted to just show-card,
save-card and toss-card. In this version the users cannot request that
the deck be restacked nor can they get a progress report. These
restrictions made it simple to code the dialog in block-structured
form, rather than as a state machine.
Version 2 changes
-----------------
I made the game state a class instance that conforms to an API. The
API can hide the implementation details of how the cards are
represented. For example, the implementation seen here loads card from
an SQL data base into python data structures, and implements the game
operations on those in-memory structures. An alternative
implementation would have all those operations be implemented as SQL
queries on the database.
Beyond insulation from implementation details, the API is abstract
enough that dialog works on any kind of subject deck, as long as the
cards may be seen as having question and answer properties.
The big payoff, though, is that the code is for dialog is clear and
simple.
"""
VERSION = '2.0.0'
ENTER,DELETE = 10,127
def dialog(gs):
while not gs.gameover:
while gs.more:
if learnt(gs.question,gs.answer):
gs.toss()
else:
gs.keep()
gs.restack()
gs.check_endgame()
class Chinese(object):
"""Operations on a flashcard deck"""
@property
def question(self):
"""Return a formatted question string derived from the card on
to of the draw deck. The proper formatting depends on the
subject. The formatting for Chinese vocabulary would likely
differ from multiplation tables."""
(chinese,pinyin,_) = self.deck[-1]
return '\n'.join([chinese,pinyin])
@property
def answer(self):
"""Return a formatted answer string."""
(_,_,english) = self.deck[-1]
return english
def toss(self):
"""Remove the card from the game. This operation is also known
as discard. For testing purposes only, the removed cards are
kept in the trash."""
card = self.deck.pop()
self.trash.add(card)
def keep(self):
"""Save the card to the retry deck. The user may put these
cards back into play with the restack()."""
card = self.deck.pop()
self.retry.append(card)
def restack(self):
"""Shuffle and stack any kept cards to top of the play deck."""
# Make sure that the recently played cards are placed at the
# end of the new deck.
n = len(self.retry) / 2
r1,r2 = self.retry[:n],self.retry[n:]
random.shuffle(r1)
random.shuffle(r2)
self.deck,self.retry = r2+r1,[]
# r1,r2 are flipped cards are drawn from the end of the list.
@property
def more(self):
"""True if more cards in the draw deck."""
return len(self.deck) > 0
@property
def gameover(self):
"""True if both the draw deck and save deck are empty."""
return len(self.deck) == 0 and len(self.retry) == 0
def check_endgame(self):
assert set(self.cards) == self.trash
def load(self,dbpath,card_count):
"""Create game state."""
q = """
select distinct chinese,pinyin,english
from hsk
order by random()
limit ?;"""
cx = sqlite3.connect(dbpath)
cur = cx.cursor()
r = cur.execute(q,(card_count,))
self.cards = list(r)
self.deck = self.cards[:]
self.retry = []
self.trash = set()
def __init__(self,card_count=30):
self.load('card.db',card_count)
def learnt(question,answer):
# Show question and prompt to reveal answer:
clear()
print question
ch = getch()
while ord(ch) != ENTER:
print 'Press enter to show answer'
ch = getch()
# Show answer and prompt for self-score:
print answer
ch = getch()
while ord(ch) not in (ENTER,DELETE):
print 'Press enter to discard, delete to keep for replay'
ch = getch()
# True to discard, and false to keep
return ord(ch) == DELETE
def getch():
"""Accept keystroke immediately. The delete key may generate a
sequence of charaters, depending on the terminal settings. No
doubt there is a better way to handle these variations than my
hack below."""
fd = sys.stdin.fileno()
oldterm = termios.tcgetattr(fd)
newattr = termios.tcgetattr(fd)
newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
termios.tcsetattr(fd, termios.TCSANOW, newattr)
oldflags = fcntl.fcntl(fd, fcntl.F_GETFL)
try:
while 1:
try:
c = sys.stdin.read(1)
break
except IOError:
pass
if ord(c) == 27: # ANSI escape sequence
if (ord(sys.stdin.read(1)) == 91 and
ord(sys.stdin.read(1)) == 51 and
ord(sys.stdin.read(1)) == 126):
c = chr(127)
else:
c = chr(255) # error
elif ord(c) == 8: # Control-H
c = chr(127)
finally:
termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
fcntl.fcntl(fd, fcntl.F_SETFL, oldflags)
return c
def clear():
os.system('clear')
if __name__ == '__main__':
dialog(Chinese(30))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment