# PyGame based flash-card Application # # LM - himself@lloydm.net - Anything not covered by other copyrights is cc by attribution: # https://creativecommons.org/licenses/by/4.0/ # import sys import pygame as pg from pygame.locals import * import time import numpy as np import pygame_widgets from pygame_widgets.button import Button application_title = 'Flash Cards' # default app_image_folder = 'PNGs/' import argparse # Parse command line arguments parser = argparse.ArgumentParser() app_width = 400 # Dimensions of display surface in pixels app_height = 400 front_image_width = 200 # Pixels front_image_height = 190 # Pixels front_image_ver_margin = 20 selected_card_number = -1 fps = 30 # Frames per second CLOCK = pg.time.Clock() screen = pg.display.set_mode((app_width, app_height), 0, 32) app_background = pg.image.load(app_image_folder + "app_background.png") btn_background = pg.image.load(app_image_folder + "btn_background_thin_bevel.png") sm_btn_backround = pg.image.load(app_image_folder + "sm_btn_background_beveled.png") correctSelection = pg.image.load(app_image_folder + "correct-36x36.png") incorrectSelection = pg.image.load(app_image_folder + "incorrect-36x36.png") smallErase = pg.image.load(app_image_folder + "Erase-40x40.png") mediumErase = pg.image.load(app_image_folder + "Erase-80x40.png") cardImageErase = pg.image.load(app_image_folder + "Erase-300x200.png") lastImage = cardImageErase # Choice Buttons button_width = 120 button_height = 60 button_hor_space = 10 button_image_margin = 30 button_ver_space = 0 button_x_inset = 10 button_y = app_height/2 + button_ver_space number_of_buttons = 6 btn_caption = ['', '', '', '', '', ''] correct_button = -1 # Next button app_button_width = 60 app_button_height = 40 app_button_xy_inset = 10 app_button_y = app_height - app_button_height - app_button_xy_inset # Scoring mark_width = 36 sm_erase_width = 40 med_erase_width = 80 score_numerator = 0 score_denominator = 0 pass_score = 70 # Colors white = (255, 255, 255) green = (0, 102, 34) red = (204, 0, 0) black = (0, 0, 0) def initDataSources(): global max_input_sets, num_input_sets global img_folder, img_fileSpec, img_data global lbl_fileSpec, back_text, btn_text, set_presentation_weight num_input_sets = 0 # Set counter img_fileSpec = [[] for _ in range(max_input_sets)] lbl_fileSpec = [[] for _ in range(max_input_sets)] img_data = [[] for _ in range(max_input_sets)] back_text = [[] for _ in range(max_input_sets)] btn_text = [[] for _ in range(max_input_sets)] set_presentation_weight = np.zeros(max_input_sets) img_folder = ["" for x in range(max_input_sets)] # for i in range(0, max_input_sets): img_fileSpec[i] = list() lbl_fileSpec[i] = list() img_data[i] = list() back_text[i] = list() btn_text[i] = list() return def initGUI(): # display application background panel pg.init() pg.display.set_caption(application_title) pg.display.set_allow_screensaver(True) screen.blit(app_background, (0, 0)) def normalizeWeights(): # Convert weights to cumulative proportions global num_input_sets, set_presentation_weight sum = 0 for i in range(0, num_input_sets): sum += set_presentation_weight[i] cum = 0 if sum == 0: for i in range(0, num_input_sets): set_presentation_weight[i] = 1/num_input_sets + cum cum = set_presentation_weight[i] return for i in range(0, num_input_sets): set_presentation_weight[i] = set_presentation_weight[i]/sum + cum cum = set_presentation_weight[i] return def displayAppButtons(): # 1 - Lower right corner button x = app_width - app_button_width - app_button_xy_inset y = app_button_y font = pg.font.SysFont('calibri', 16) # Caption screen.blit(sm_btn_backround, (x, y)) text = font.render('Next', 1, white) text_rect = text.get_rect(center=(x + (app_button_width / 2), y + (app_button_height/2))) screen.blit(text, text_rect) def displayApp(): initGUI() displayAppButtons() pg.display.update() def initCardArrays(): global img_fileSpec, img_folder, lbl_fileSpec, img_data, back_text, btn_text, num_input_sets image_filename = list() card_description = list() for set in range(0, num_input_sets): try: with open(img_fileSpec[set], mode="r") as f0: image_filename = f0.read().splitlines() except: print('Failed to load image filenames!') return for i in range(0, len(image_filename)): if len(image_filename[i]) > 0: img_data[set].append(pg.image.load(img_folder[set] + image_filename[i])) # try: with open(lbl_fileSpec[set], mode="r", encoding='utf-8') as f1: card_description = f1.read().splitlines() except: print('Failed to load back of card descriptions!') return # for i in range(0, len(card_description)): text = card_description[i].split('\t') btn_text[set].append(text[0]) return def displayCardImage(image): x = (app_width - front_image_width) / 2 y = front_image_ver_margin screen.blit(cardImageErase, (x, y)) # Previous displayed image screen.blit(image, (x, y)) pg.display.update() def displayChoiceButtons(captionsList): x = button_x_inset y = button_y + button_image_margin #font = pg.font.Font(None, 20) font = pg.font.SysFont('calibri', 16) for i in range (0, number_of_buttons): screen.blit(btn_background, (x, y)) text = font.render(captionsList[i], 1, white) text_rect = text.get_rect(center=(x + (button_width / 2), y + (button_height/3))) screen.blit(text, text_rect) x += (button_width + button_hor_space) if x + button_width > app_width: x = button_x_inset y += (button_height + button_ver_space) pg.display.update() return def eraseMark(): # Check mark or x mark to right of displayed image x = app_width - (app_width - front_image_width)/4 - sm_erase_width/2 y = front_image_height/2 + front_image_ver_margin screen.blit(smallErase, (x, y)) pg.display.update() def eraseScore(): x = (app_width - med_erase_width)/2 y = app_button_y screen.blit(mediumErase, (x, y)) # Middle bottom (percent correct) x = app_button_xy_inset screen.blit(mediumErase, (x, y)) # Left bottom (total tries) pg.display.update() def displayScore(score): x = (app_width - med_erase_width)/2 y = app_button_y text_color = green if score < pass_score: text_color = red font = pg.font.SysFont('calibri', 16) text = font.render(str(score) + '%', 1, text_color) text_rect = text.get_rect(center=(x + (med_erase_width / 2), y + (app_button_height/2))) screen.blit(text, text_rect) # Clear lower left corner for total count app_button_xy_inset text_color = black text = font.render(str(score_denominator), 1, text_color) x = app_button_xy_inset + med_erase_width/3 text_rect = text.get_rect(center=(x, y + (app_button_height/2))) screen.blit(text, text_rect) pg.display.update() return def displayFeedback(mark): # mark is boolean true or false x = app_width - (app_width - front_image_width)/4 - mark_width/2 y = front_image_height/2 + front_image_ver_margin if mark: img = correctSelection else: img = incorrectSelection eraseMark() screen.blit(img, (x, y)) pg.display.update() return def loadChoiceBtnCaptions(n, btn_text): # n is the correct (valid) selection corresponding to the displayed image global btn_caption, correct_button btn_caption = ['', '', '', '', '', ''] used = list() # Track buttons that have been assigned a caption correct_button = np.random.randint(0, number_of_buttons) btn_caption[correct_button] = btn_text[n] used.append(btn_caption[correct_button]) for i in range(0, number_of_buttons): if len(btn_caption[i]) == 0: while (True): j = np.random.randint(0, len(btn_text)) if not btn_text[j] in used: break btn_caption[i] = btn_text[j] used.append(btn_caption[i]) return def scoreSelected(x, y): global score_numerator, score_denominator if correct_button < 0: return x0 = button_x_inset # Left edge of buttons 0 and 3 x1 = x0 + button_width # Right edge of button 0 and 3 x2 = x1 + button_hor_space # Etc. buttons 1 and 4 x3 = x2 + button_width x4 = x3 + button_hor_space # Etc. buttons 2 and 5 x5 = x4 + button_width y0 = button_y + button_image_margin # Top edge of top button row y1 = y0 + button_height # Bottom edge of top button row y2 = y0 + button_height + button_ver_space # Etc. second button row y3 = y2 + button_height selBtn = -1 if y >= y0 and y <= y1: # Top row of 3 selection buttons if x >= x0 and x <= x1: selBtn = 0 elif x >= x2 and x <= x3: selBtn = 1 elif x >= x4 and x <= x5: selBtn = 2 elif y >= y2 and y <= y3: # Second row of 3 selection buttons if x >= x0 and x <= x1: selBtn = 3 elif x >= x2 and x <= x3: selBtn = 4 elif x >= x4 and x <= x5: selBtn = 5 score_denominator += 1 if selBtn == correct_button: displayFeedback(True) score_numerator += 1 else: displayFeedback(False) if score_denominator > 0: # Sure to be True here eraseScore() displayScore(round(score_numerator*100/score_denominator)) return def nextGenericCard(): global lastImage eraseMark() # heads = (np.random.randint(0,2) == 0) prob = np.random.rand() # Adjust proportion of card-presentations from each image set selectedSet = 0 for set in range(num_input_sets): if prob < set_presentation_weight[set]: selectedSet = set break image = lastImage while image == lastImage: # Avoid successive presentation of same image n = np.random.randint(0,len(img_data[selectedSet])) # Featured image subscript number image = img_data[selectedSet][n] lastImage = image displayCardImage(image) loadChoiceBtnCaptions(n, btn_text[selectedSet]) displayChoiceButtons(btn_caption) # Pre-loaded captions for display return; def mainLoop(): MOUSEBUTTONDOWN = 1025 # LM: Empirically determined value while (True): for event in pg.event.get(): if event.type == QUIT: pg.quit() sys.exit() elif event.type == MOUSEBUTTONDOWN: x, y = pg.mouse.get_pos() if x > app_width - app_button_width - app_button_xy_inset and y > app_button_y: nextGenericCard() else: scoreSelected(x, y) # pg.display.update() CLOCK.tick(fps) return def processIniFile(iniFileSpec): global img_fileSpec, img_folder, lbl_fileSpec, back_text, btn_text, max_input_sets, num_input_sets global application_title, set_presentation_weight # try: iniFile = open(iniFileSpec, mode = 'r') except: return # No .ini file set = 0 for line in iniFile: L = line.split('#', 1) # .ini file lines may be commented p = L[0].split(chr(9)) p0 = p[0].lower() if p0.strip() == 'caption' and len(p) > 1: application_title = p[1] elif p0 == 'files' and len(p) > 4: if set < max_input_sets: img_fileSpec[set] = p[1] + p[2] lbl_fileSpec[set] = p[1] + p[4] img_folder[set] = p[3] if len(p) > 5: set_presentation_weight[set] = float(p[5]) set += 1 num_input_sets = set normalizeWeights() return if __name__ == "__main__": # Initialize global data structures # Parse command line parameters to specify .ini file, etc. # Launch application global max_input_sets, num_input_sets parser.add_argument('-ini', type=str, required=False) parser.add_argument('-max', type=int, required=False) args = parser.parse_args() user_specified_ini = False ini_fileSpec = 'fc.ini' if args.ini is not None: ini_fileSpec = args.ini user_specified_ini = True max_input_sets = 20 if args.max is not None: max = args.max if max > 20 and max < 250: # Upper limit to protect against potential memory leak max_input_sets = max # Other command line arguments here initDataSources() # Must precede processIniFile() processIniFile(ini_fileSpec) if num_input_sets == 0: # If no default fc.ini or user-specified init if user_specified_ini: print(ini_fileSpec + ' not found!') exit(1) processIniFile('shogi.ini') if num_input_sets == 0: print('No .ini file found.') exit(1) # Initialization succeeded initCardArrays() # Generalized arrays for any flash card application displayApp() mainLoop()