#!/usr/local/cpython-3.6/bin/python3 """Ask questions in a way that facilitates memorization.""" import re import sys import time import random import typing import readline # port importlib import subprocess REPS = 2 MULTILINE = False PRODUCE_SPEECH = False AUTOREPEAT = False def make_used(variable: typing.Any) -> None: """Convince pyflakes that variable is used.""" assert True or variable def clear_history() -> None: """Erase input history, so the user can't just uparrow to get back old answers.""" readline.clear_history() def speak(string: str) -> None: """Say something via flite.""" command = ["flite -voice rms -t '{}' > /dev/null 2>&1".format(string)] subprocess.call(command, shell=True) if AUTOREPEAT: time.sleep(0.5) subprocess.call(command, shell=True) def positive_reinforcement() -> None: """Output a nice message when the user gets a question right.""" list_ = [ 'That is correct!', 'Good job!', 'Well done!', 'Yay!', 'Terrific!', 'Very nice!', 'You got it!', 'Very cool!', 'Great. Keep up the good work!', ] selection = random.choice(list_) print(selection) class QuestionAnswer: # pylint: disable=too-few-public-methods """ Hold a question and answer. Enable the ability to pose a question and get a response. """ def __init__(self, question_string: str, answer_string: str) -> None: """Initialize.""" self.question_string = question_string.strip() self.answer_string = answer_string.strip() self.correct_in_a_row = 0 def question_done(self) -> bool: """Ask the question, get the response, return True iff the question was answered correctly enough times in a row.""" if PRODUCE_SPEECH: speak(self.question_string) else: print('Question:') print(self.question_string) print('Type answer - from longer-term memory:') if MULTILINE: lines = [] while True: clear_history() line = input() if not line: break lines.append(line) user_answer = '\n'.join(lines) else: clear_history() user_answer = input().strip() if user_answer == self.answer_string: positive_reinforcement() self.correct_in_a_row += 1 print('Hit enter to continue.') make_used(input()) return self.correct_in_a_row == REPS else: self.correct_in_a_row = 0 while True: print('Sorry, that is incorrect') print() print('The question was:') print(self.question_string) print() print('The correct answer is:') print(self.answer_string) print() print('Hit enter to immediately type in the correct answer from short-term memory') make_used(input()) clear_screen() if PRODUCE_SPEECH: speak('The question was') speak(self.question_string) else: print('The question was:') print(self.question_string) print() print('Type the answer - from short-term memory:') re_answer = input() if re_answer == self.answer_string: print('Good. Hit enter to continue') make_used(input()) break return False def usage(retval: int) -> None: """Output a usage message.""" if retval == 0: writer = sys.stdout.write elif retval == 1: writer = sys.stderr.write else: sys.stderr.write('{}: Internal error: retval not 0 or 1\n'.format(sys.argv[0])) sys.exit(1) writer('Usage: {} --file filename --reps 2\n'.format(sys.argv[0])) writer('\n') writer('filename must contain an options line, followed by question-answer pairs separated by\n') writer('a %% token. Within this, the question is separated from the answer by a - token.\n') sys.exit(retval) def get_question_answers(filename: str) -> typing.Dict[str, QuestionAnswer]: # pylint: disable=global-statement """Read the options, questions and answers from filename.""" global MULTILINE global PRODUCE_SPEECH with open(filename, 'r', encoding='utf-8') as file_: options_line = file_.readline().strip() options = options_line.split(',') if len(options) == 1 and options[0] == '': # This means the options line was empty pass else: if 'multiline' in options: MULTILINE = True options.remove('multiline') if 'produce_speech' in options: PRODUCE_SPEECH = True options.remove('produce_speech') else: MULTILINE = False if options: sys.stderr.write('{}: One or more unrecognized options: {}\n'.format(sys.argv[0], options)) usage(1) data = file_.read() question_answer_strings = re.split('^%%$', data, flags=re.MULTILINE) question_answers = dict() for question_answer_string in question_answer_strings: sublist = re.split('^-$', question_answer_string, flags=re.MULTILINE) if len(sublist) != 2: print(sublist) raise ValueError('question has multi -') question_string = sublist[0] answer_string = sublist[1] question_answer = QuestionAnswer(question_string, answer_string) question_answers[question_answer.question_string] = question_answer return question_answers def maybe_plural(number: int) -> str: """Return an s if number is not 1.""" if number == 1: return '' else: return 's' def clear_screen() -> None: """Clear the screen using an ugly escape sequence, to avoid the complexity of curses.""" # We assume a vt100-like terminal emulator. # This clears the screen: sys.stdout.write(chr(27) + "[2J" + chr(27) + '[H') # This erases the scrollback buffer: sys.stdout.write(chr(27) + 'c') sys.stdout.flush() def main() -> None: # pylint: disable=global-statement """Drill on quiz.""" global REPS while sys.argv[1:]: if sys.argv[1] == '--file': filename = sys.argv[2] del sys.argv[1] elif sys.argv[1] == '--reps': REPS = int(sys.argv[2]) del sys.argv[1] elif sys.argv[1] in ('-h', '--help'): usage(0) else: sys.stderr.write('{}: Unrecognized command line option: {}\n'.format(sys.argv[0], sys.argv[1])) usage(1) del sys.argv[1] question_answers = get_question_answers(filename) original_num_qa = len(question_answers) prior_key = None time0 = time.time() while True: keys = list(question_answers) len_keys = len(keys) if len_keys == 0: # Good, we're all done break clear_screen() print('{} question{} left'.format(len_keys, maybe_plural(len_keys))) # Get a random question - except disallow asking the same # question twice in a row, except one there is only one question # remaining. if keys[1:]: while True: key = random.choice(keys) if key != prior_key: prior_key = key break else: key = keys[0] prior_key = key if question_answers[key].question_done(): del question_answers[key] time1 = time.time() print('Congratulations! You got all {} answers right {} time{} in a row!'.format(original_num_qa, REPS, maybe_plural(REPS))) duration_in_seconds = (time1 - time0) duration_in_minutes = duration_in_seconds / 60.0 print('It took you {} minutes to complete the whole test.'.format(round(duration_in_minutes, 1))) print("That's an average of {} questions per minute!".format(round(original_num_qa / duration_in_minutes, 2))) main()