Created
September 12, 2015 00:16
-
-
Save gulan/076cb442bf500ac54603 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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