1#!/usr/local/bin/python3.8 2# This Python file uses the following encoding: utf-8 3#------------------------------------------------------------------------------ 4# Brain Workshop: a Dual N-Back game in Python 5# 6# Tutorial, installation instructions & links to the dual n-back community 7# are available at the Brain Workshop web site: 8# 9# http://brainworkshop.net/ 10# 11# Also see Readme.txt. 12# 13# Copyright (C) 2009-2011: Paul Hoskinson (plhosk@gmail.com) 14# 15# The code is GPL licensed (http://www.gnu.org/copyleft/gpl.html) 16#------------------------------------------------------------------------------ 17# Use python3 style division for consistency 18from __future__ import division 19VERSION = '5.0-beta' 20def debug_msg(msg): 21 if DEBUG: 22 if isinstance(msg, Exception): 23 exc_type, exc_obj, exc_tb = sys.exc_info() 24 fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 25 print('debug: %s Line %i' % (str(msg), exc_tb.tb_lineno)) 26 else: 27 print('debug: %s' % str(msg)) 28def error_msg(msg, e = None): 29 if DEBUG and e: 30 exc_type, exc_obj, exc_tb = sys.exc_info() 31 fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 32 print("ERROR: %s\n\t%s Line %i" % (msg, e, exc_tb.tb_lineno)) 33 else: 34 print("ERROR: %s" % msg) 35def get_argv(arg): 36 if arg in sys.argv: 37 index = sys.argv.index(arg) 38 if index + 1 < len(sys.argv): 39 return sys.argv[index + 1] 40 else: 41 error_msg("Expected an argument following %s" % arg) 42 exit(1) 43 44import random, os, sys, imp, socket, webbrowser, time, math, traceback, datetime, errno 45if sys.version_info >= (3,0): 46 import urllib.request, configparser as ConfigParser 47 from io import StringIO 48 import pickle 49else: 50 import urllib2 as urllib, ConfigParser, StringIO 51 import cPickle as pickle 52 53from decimal import Decimal 54from time import strftime 55from datetime import date 56import gettext 57if sys.version_info >= (3,0): 58 # TODO check if this is right 59 gettext.install('messages', localedir='/usr/local/share/brainworkshop/i18n') 60else: 61 gettext.install('messages', localedir='/usr/local/share/brainworkshop/i18n', unicode=True) 62 63# Clinical mode? Clinical mode sets cfg.JAEGGI_MODE = True, enforces a minimal user 64# interface, and saves results into a binary file (default 'logfile.dat') which 65# should be more difficult to tamper with. 66CLINICAL_MODE = False 67 68# Internal static options not available in config file. 69CONFIG_OVERWRITE_IF_OLDER_THAN = '4.8' 70NOVBO = True 71VSYNC = False 72DEBUG = False 73FOLDER_RES = 'res' 74FOLDER_DATA = 'data' 75CONFIGFILE = 'config.ini' 76STATS_BINARY = 'logfile.dat' 77USER = 'default' 78#CHARTFILE = {2:'chart-02-dnb.txt', 3:'chart-03-tnb.txt', 4:'chart-04-dlnb.txt', 5:'chart-05-tlnb.txt', 79 #6:'chart-06-qlnb.txt',7:'chart-07-anb.txt', 8:'chart-08-danb.txt', 9:'chart-09-tanb.txt', 80 #10:'chart-10-ponb.txt', 11:'chart-11-aunb.txt'} 81ATTEMPT_TO_SAVE_STATS = True 82STATS_SEPARATOR = ',' 83WEB_SITE = 'http://brainworkshop.net/' 84WEB_TUTORIAL = 'http://brainworkshop.net/#tutorial' 85CLINICAL_TUTORIAL = WEB_TUTORIAL # FIXME: Add tutorial catered to clinical trials 86WEB_DONATE = 'http://brainworkshop.net/donate.html' 87WEB_VERSION_CHECK = 'http://brainworkshop.net/version.txt' 88WEB_PYGLET_DOWNLOAD = 'http://pyglet.org/download.html' 89WEB_FORUM = 'http://groups.google.com/group/brain-training' 90WEB_MORSE = 'http://en.wikipedia.org/wiki/Morse_code' 91TIMEOUT_SILENT = 3 92TICKS_MIN = 3 93TICKS_MAX = 50 94TICK_DURATION = 0.1 95DEFAULT_WINDOW_WIDTH = 912 96DEFAULT_WINDOW_HEIGHT = 684 97preventMusicSkipping = True 98 99def from_width_center(offset): 100 return int( (window.width/2) + offset * (window.width / DEFAULT_WINDOW_WIDTH) ) 101def from_height_center(offset): 102 return int( (window.height/2) + offset * (window.height / DEFAULT_WINDOW_HEIGHT) ) 103def width_center(): 104 return int(window.width/2) 105def height_center(): 106 return int(window.height/2) 107 108def from_top_edge(from_edge): 109 return int(window.height - (from_edge * window.height/DEFAULT_WINDOW_HEIGHT)) 110 111def from_bottom_edge(from_edge): 112 return int(from_edge * (window.height/DEFAULT_WINDOW_HEIGHT)) 113 114def from_right_edge(from_edge): 115 return int(window.width - (from_edge * window.width/DEFAULT_WINDOW_WIDTH)) 116 117def from_left_edge(from_edge): 118 return int(from_edge * window.width/DEFAULT_WINDOW_WIDTH) 119 120def scale_to_width(fraction): 121 return int(fraction * window.width/DEFAULT_WINDOW_WIDTH) 122 123def scale_to_height(fraction): 124 return int(fraction * window.height/DEFAULT_WINDOW_HEIGHT) 125 126def calc_fontsize(size): 127 return size * (window.height/DEFAULT_WINDOW_HEIGHT) 128def calc_dpi(size = 100): 129 return int(size * ((window.width + window.height)/(DEFAULT_WINDOW_WIDTH + DEFAULT_WINDOW_HEIGHT))) 130 131def get_pyglet_media_Player(): 132 try: 133 my_player = pyglet.media.Player() 134 except Exception as e: 135 debug_msg(e) 136 my_player = pyglet.media.ManagedSoundPlayer() 137 return my_player 138 139# some functions to assist in path determination 140def main_is_frozen(): 141 return (hasattr(sys, "frozen") or # new py2exe 142 hasattr(sys, "importers") # old py2exe 143 or imp.is_frozen("__main__")) # tools/freeze 144def get_main_dir(): 145 return '/usr/local/share/brainworkshop' 146 147def get_settings_path(name): 148 '''Get a directory to save user preferences. 149 Copied from pyglet.resource so we don't have to load that module 150 (which recursively indexes . on loading -- wtf?).''' 151 if sys.platform in ('cygwin', 'win32'): 152 if 'APPDATA' in os.environ: 153 return os.path.join(os.environ['APPDATA'], name) 154 else: 155 return os.path.expanduser('~/%s' % name) 156 elif sys.platform == 'darwin': 157 return os.path.expanduser('~/Library/Application Support/%s' % name) 158 else: # on *nix, we want it to be lowercase and without spaces (~/.brainworkshop/data) 159 return os.path.expanduser('~/.%s' % (name.lower().replace(' ', ''))) 160 161def get_old_data_dir(): 162 return os.path.join(get_main_dir(), FOLDER_DATA) 163def get_data_dir(): 164 rtrn = get_argv('--datadir') 165 if rtrn: 166 return rtrn 167 else: 168 return os.path.join(get_settings_path('Brain Workshop'), FOLDER_DATA) 169def get_res_dir(): 170 rtrn = get_argv('--resdir') 171 if rtrn: 172 return rtrn 173 else: 174 return os.path.join(get_main_dir(), FOLDER_RES) 175def edit_config_ini(): 176 if sys.platform == 'win32': 177 cmd = 'notepad' 178 elif sys.platform == 'darwin': 179 cmd = 'open' 180 else: 181 cmd = 'xdg-open' 182 print(cmd + ' "' + os.path.join(get_data_dir(), CONFIGFILE) + '"') 183 window.on_close() 184 import subprocess 185 subprocess.call((cmd + ' "' + os.path.join(get_data_dir(), CONFIGFILE) + '"'), shell=True) 186 sys.exit(0) 187 188def quit_with_error(message='', postmessage='', quit=True, trace=True): 189 if message: 190 sys.stderr.write(message + '\n') 191 if trace: 192 sys.stderr.write(_("Full text of error:\n")) 193 traceback.print_exc() 194 if postmessage: 195 sys.stderr.write('\n\n' + postmessage) 196 if quit: 197 sys.exit(1) 198 199CONFIGFILE_DEFAULT_CONTENTS = """ 200###################################################################### 201# Brain Workshop configuration file 202# generated by Brain Workshop """ + VERSION + """ 203# 204# To change configuration options: 205# 1. Edit this file as desired, 206# 2. Save the file, 207# 3. Launch Brain Workshop to see the changes. 208# 209# Every line beginning with # is ignored by the program. 210# 211# Please see the Brain Workshop web site for more information: 212# http://brainworkshop.net 213# 214# The configuration options begin below. 215###################################################################### 216 217[DEFAULT] 218 219# Jaeggi-style interface with default scoring model? 220# Choose either this option or JAEGGI_MODE but not both. 221# This mode allows access to Manual mode, the extra sound sets, and the 222# additional game modes of Brain Workshop while presenting the game in 223# the more challenging Jaeggi-style interface featured in the original study. 224# With the default BW sequence generation model, the visual and auditory 225# sequences are more randomized and unpredictable than they are in Jaeggi 226# mode. The only effect of this option is to set the following options: 227# ANIMATE_SQUARES = False, OLD_STYLE_SQUARES = True, 228# OLD_STYLE_SHARP_CORNERS = True, SHOW_FEEDBACK = False, 229# GRIDLINES = False, CROSSHAIRS = True, BLACK_BACKGROUND = True, 230# WINDOW_FULLSCREEN = True, HIDE_TEXT = True, FIELD_EXPAND = True 231# Default: False 232JAEGGI_INTERFACE_DEFAULT_SCORING = False 233 234# Jaeggi mode? 235# Choose either this option or JAEGGI_INTERFACE_DEFAULT_SCORING but not both. 236# This mode emulates the scoring model used in the original study protocol. 237# It counts non-matches with no inputs as correct (instead of ignoring them). 238# It also forces 4 visual matches, 4 auditory matches, and 2 simultaneous 239# matches per session, resulting in less randomized and more predictable 240# sequences than in the default BW sequence generation model. 241# Different thresholds are used to reflect the modified scoring system 242# (see below). Access to Manual mode, additional game modes and sound sets 243# is disabled in Jaeggi mode. 244# Default: False 245JAEGGI_MODE = False 246 247# The default BW scoring system uses the following formula: 248# score = TP / (TP + FP + FN) 249# where TP is a true positive response, FN is a false negative, etc. All 250# stimulus modalities are summed together for this formula. 251# The Jaeggi mode scoring system scores uses the following formula: 252# score = (TP + TN) / (TP + TN + FP + FN) 253# Each modality is scored separately, and the score for the whole session 254# is equal to the lowest score of any modality. 255# Default: False 256JAEGGI_SCORING = False 257 258# In Jaeggi Mode, adjust the default appearance and sounds of Brain Workshop 259# to emulate the original software used in the study? 260# If this is enabled, the following options will be set: 261# AUDIO1_SETS = ['letters'], ANIMATE_SQUARES = False, 262# OLD_STYLE_SQUARES = True, OLD_STYLE_SHARP_CORNERS = True, 263# SHOW_FEEDBACK = False, GRIDLINES = False, CROSSHAIRS = True 264# (note: this option only takes effect if JAEGGI_MODE is set to True) 265# Default: True 266JAEGGI_FORCE_OPTIONS = True 267 268# In Jaeggi Mode, further adjust the appearance to match the original 269# software as closely as possible? 270# If this is enabled, the following options will be set: 271# BLACK_BACKGROUND = True, WINDOW_FULLSCREEN = True, 272# HIDE_TEXT = True, FIELD_EXPAND = True 273# (note: this option only takes effect if JAEGGI_MODE is set to True) 274# Default: True 275JAEGGI_FORCE_OPTIONS_ADDITIONAL = True 276 277# Allow Mouse to be used for input? 278# Only for dual n-back. Automatically disabled in JAEGGI_MODE. 279ENABLE_MOUSE = True 280 281# Background color: True = black, False = white. 282# Default: False 283BLACK_BACKGROUND = False 284 285# Begin in full screen mode? 286# Setting this to False will begin in windowed mode. 287# Default: False 288WINDOW_FULLSCREEN = False 289 290# Window size in windowed mode. 291# Minimum recommended values: width = 800, height = 600 292WINDOW_WIDTH = 912 293WINDOW_HEIGHT = 684 294 295# Skip title screen? 296SKIP_TITLE_SCREEN = False 297 298# Display feedback of correct/incorrect input? 299# Default: True 300SHOW_FEEDBACK = True 301 302# Hide text during game? (this can be toggled in-game by pressing F8) 303# Default: False 304HIDE_TEXT = False 305 306# Expand the field (squares) to fill the entire height of the screen? 307# Note: this should only be used with HIDE_TEXT = True. 308FIELD_EXPAND = False 309 310# Show grid lines and crosshairs? 311GRIDLINES = True 312CROSSHAIRS = True 313 314# Set the color of the square in non-Color N-Back modes. 315# This also affects Dual Combination N-Back and Arithmetic N-Back. 316# 1 = blue, 2 = cyan, 3 = green, 4 = grey, 317# 5 = magenta, 6 = red, 7 = white, 8 = yellow 318# Default: [1, 3, 8, 6] 319VISUAL_COLORS = [1, 3, 8, 6] 320 321# Specify image sets here. This is a list of subfolders in the res\sprites\ 322# folder which may be selected in Image mode. 323# The first item in the list is the default which is loaded on startup. 324IMAGE_SETS = ['polygons-basic', 'national-park-service', 'pentominoes', 325 'tetrominoes-fixed', 'cartoon-faces'] 326 327# This selects which sounds to use for audio n-back tasks. 328# Select any combination of letters, numbers, the NATO Phonetic Alphabet 329# (Alpha, Bravo, Charlie, etc), the C scale on piano, and morse code. 330# AUDIO1_SETS = ['letters', 'morse', 'nato', 'numbers', 'piano'] 331AUDIO1_SETS = ['letters'] 332 333# Sound configuration for the Dual Audio (A-A) task. 334# Possible values for CHANNEL_AUDIO1 and CHANNEL_AUDIO2: 335# 'left' 'right' 'center' 336AUDIO2_SETS = ['letters'] 337CHANNEL_AUDIO1 = 'left' 338CHANNEL_AUDIO2 = 'right' 339 340# In multiple-stimulus modes, more than one visual stimulus is presented at 341# the same time. Each of the simultaneous visual stimuli has an ID number 342# associated with either its color or its image. Which should we use, by 343# default? 344# Options: 'color' or 'image' 345MULTI_MODE = 'color' 346 347# Animate squares? 348ANIMATE_SQUARES = False 349 350# Use the flat, single-color squares like in versions prior to 4.1? 351# Also, use sharp corners or rounded corners? 352OLD_STYLE_SQUARES = False 353OLD_STYLE_SHARP_CORNERS = False 354 355# Start in Manual mode? 356# If this is False, the game will start in standard mode. 357# Default: False 358MANUAL = False 359USE_MUSIC_MANUAL = False 360 361# Starting game mode. 362# Possible values: 363# 2:'Dual', 364# 3:'P-C-A', 365# 4:'Dual Combination', 366# 5:'Tri Combination', 367# 6:'Quad Combination', 368# 7:'Arithmetic', 369# 8:'Dual Arithmetic', 370# 9:'Triple Arithmetic', 371# 10:'Position', 372# 11:'Sound', 373# 20:'P-C', 374# 21:'P-I', 375# 22:'C-A', 376# 23:'I-A', 377# 24:'C-I', 378# 25:'P-C-I', 379# 26:'P-I-A', 380# 27:'C-I-A', 381# 28:'Quad', 382# 100:'A-A', 383# 101:'P-A-A', 384# 102:'C-A-A', 385# 103:'I-A-A', 386# 104:'P-C-A-A', 387# 105:'P-I-A-A', 388# 106:'C-I-A-A', 389# 107:'P-C-I-A-A' (Pentuple) 390# 128+x: Crab mode 391# 256+x: Double mode (can be combined with crab mode) 392# 512+x: Triple mode 393# 768+x: Quadruple mode 394 395# Note: if JAEGGI_MODE is True, only Dual N-Back will be available. 396# Default: 2 397GAME_MODE = 2 398 399# Default starting n-back levels. 400# must be greater than or equal to 1. 401# Look above to find the corresponding mode number. Add a line for the mode 402# if it doesn't already exist. Modes not specifically listed here will 403# use BACK_DEFAULT instead. 404# 405# Crab and multi-modes will default to the level associated with the modes 406# they're based on (if it's listed) or to BACK_DEFAULT (if it's not listed). 407 408BACK_DEFAULT = 2 409 410BACK_4 = 1 411BACK_5 = 1 412BACK_6 = 1 413BACK_7 = 1 414BACK_8 = 1 415BACK_9 = 1 416 417# N-back level resetting: 418# Should we start at the default N-back level for that game mode every 419# day, or should we resume at the last day's level? 420RESET_LEVEL = False 421 422# Use Variable N-Back by default? 423# 0 = static n-back (default) 424# 1 = variable n-back 425VARIABLE_NBACK = 0 426 427# Number of 0.1 second intervals per trial. 428# Must be greater than or equal to 4 (ie, 0.4 seconds) 429# Look above to find the corresponding mode number. Add a line for the mode 430# if it doesn't already exist. Modes not specifically listed here will 431# use TICKS_DEFAULT instead. 432# 433# Crab and multi-modes will default to the ticks associated with the modes 434# they're based on, *plus an optional bonus*, unless you add a line here to 435# give it a specific value. Any bonuses will be ignored for specified modes. 436TICKS_DEFAULT = 30 437TICKS_4 = 35 438TICKS_5 = 35 439TICKS_6 = 35 440TICKS_7 = 40 441TICKS_8 = 40 442TICKS_9 = 40 443 444# Tick bonuses for crab and multi-modes not listed above. Can be negative 445# if you're a masochist. 446 447BONUS_TICKS_CRAB = 0 448BONUS_TICKS_MULTI_2 = 5 449BONUS_TICKS_MULTI_3 = 10 450BONUS_TICKS_MULTI_4 = 15 451 452# The number of trials per session equals 453# NUM_TRIALS + NUM_TRIALS_FACTOR * n ^ NUM_TRIALS_EXPONENT, 454# where n is the current n-back level. 455 456# Default base number of trials per session. 457# Must be greater than or equal to 1. 458# Default: 20 459NUM_TRIALS = 20 460 461NUM_TRIALS_FACTOR = 1 462NUM_TRIALS_EXPONENT = 2 463 464# Thresholds for n-back level advancing & fallback. 465# Values are 0-100. 466# Set THRESHOLD_ADVANCE to 101 to disable automatic level advance. 467# Set THRESHOLD_FALLBACK to 0 to disable fallback. 468# FALLBACK_SESSIONS controls the number of sessions below 469# the fallback threshold that will trigger a level decrease. 470# Note: in Jaeggi mode, only JAEGGI_ADVANCE and JAEGGI_FALLBACK 471# are used. 472# Defaults: 80, 50, 3, 90, 75 473THRESHOLD_ADVANCE = 80 474THRESHOLD_FALLBACK = 50 475THRESHOLD_FALLBACK_SESSIONS = 3 476JAEGGI_ADVANCE = 90 477JAEGGI_FALLBACK = 75 478 479# Show feedback regarding session performance. 480# If False, forces USE_MUSIC and USE_APPLAUSE to also be False. 481USE_SESSION_FEEDBACK = True 482 483# Music/SFX options. 484# Volumes are from 0.0 (silent) to 1.0 (full) 485# Defaults: True, True, 1.0, 1.0 486USE_MUSIC = True 487USE_APPLAUSE = True 488MUSIC_VOLUME = 1.0 489SFX_VOLUME = 1.0 490 491# Specify an alternate stats file. 492# Default: stats.txt 493STATSFILE = stats.txt 494 495# Specify the hour the stats will roll over to a new day [0-23] 496ROLLOVER_HOUR = 4 497 498# Version check on startup (http protocol)? 499# Default: False 500VERSION_CHECK_ON_STARTUP = False 501 502# The chance that a match will be generated by force, in addition to the 503# inherent 1/8 chance. High settings will cause repetitive sequences to be 504# generated. Increasing this value will make the n-back task significantly 505# easier if you're using JAGGI_SCORING = False. 506# The value must be a decimal from 0 to 1. 507# Note: this option has no effect in Jaeggi mode. 508# Default: 0.125 509CHANCE_OF_GUARANTEED_MATCH = 0.125 510 511# The chance that a near-miss will be generated to help train resolution of 512# cognitive interference. For example, in 5-back, a near-miss might be 513# ABCDE-FGDJK--the "D" comes one trial earlier than would be necessary 514# for a correct match. Near-misses can be one trial short of a match, 515# one trial late, or N trials late (would have been a match if it was one 516# "cycle" ago). This setting will never accidentally generate a correct match 517# in the case of repeating stimuli if it can be avoided. 518# Default: 0.125 519 520DEFAULT_CHANCE_OF_INTERFERENCE = 0.125 521 522# How often should Brain Workshop panhandle for a donation? After every 523# PANHANDLE_FREQUENCY sessions, Brain Workshop will annoy you slightly by 524# asking for money. Set this to 0 if you have a clear conscience. 525# Default: 100 526PANHANDLE_FREQUENCY = 100 527 528# Arithmetic mode settings. 529ARITHMETIC_MAX_NUMBER = 12 530ARITHMETIC_USE_NEGATIVES = False 531ARITHMETIC_USE_ADDITION = True 532ARITHMETIC_USE_SUBTRACTION = True 533ARITHMETIC_USE_MULTIPLICATION = True 534ARITHMETIC_USE_DIVISION = True 535ARITHMETIC_ACCEPTABLE_DECIMALS = ['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', 536 '0.7', '0.8', '0.9', '0.125', '0.25', '0.375', '0.625', '0.75', '0.875', 537 '0.15', '0.35', '0.45', '0.55', '0.65', '0.85', '0.95',] 538 539# Colors for the color n-back task 540# format: (red, green, blue, 255) 541# Note: Changing these colors will have no effect in Dual or 542# Triple N-Back unless OLD_STYLE_SQUARES is set to True. 543# the _BLK colors are used when BLACK_BACKGROUND is set to True. 544COLOR_1 = (0, 0, 255, 255) 545COLOR_2 = (0, 255, 255, 255) 546COLOR_3 = (0, 255, 0, 255) 547COLOR_4 = (48, 48, 48, 255) 548COLOR_4_BLK = (255, 255, 255, 255) 549COLOR_5 = (255, 0, 255, 255) 550COLOR_6 = (255, 0, 0, 255) 551COLOR_7 = (208, 208, 208, 255) 552COLOR_7_BLK = (64, 64, 64, 255) 553COLOR_8 = (255, 255, 0, 255) 554 555# text color 556COLOR_TEXT = (0, 0, 0, 255) 557COLOR_TEXT_BLK = (240, 240, 240, 255) 558 559# input label color 560COLOR_LABEL_CORRECT = (64, 255, 64, 255) 561COLOR_LABEL_OOPS = (64, 64, 255, 255) 562COLOR_LABEL_INCORRECT = (255, 64, 64, 255) 563 564 565# Saccadic eye movement options. 566# Delay = number of seconds to wait before switching the dot 567# Repetitions = number of times to switch the dot 568SACCADIC_DELAY = 0.5 569SACCADIC_REPETITIONS = 60 570 571###################################################################### 572# Keyboard definitions. 573# The following keys cannot be used: ESC, X, P, F8, F10. 574# Look up the key codes here: 575# http://pyglet.org/doc/api/pyglet.window.key-module.html 576###################################################################### 577 578# Position match. Default: 97 (A) 579KEY_POSITION1 = 97 580 581# Sound match. Default: 108 (L) 582KEY_AUDIO = 108 583 584# Sound2 match. Default: 59 (Semicolon ;) 585KEY_AUDIO2 = 59 586 587# Color match. Default: 102 (F) 588KEY_COLOR = 102 589# Image match. Default: 106 (J) 590KEY_IMAGE = 106 591 592# Position match, multiple-stimulus mode. 593# Defaults: 115 (S), 100 (D), 102 (F) 594KEY_POSITION2 = 115 595KEY_POSITION3 = 100 596KEY_POSITION4 = 102 597 598# Color/image match, multiple-stimulus mode. KEY_VIS1 will be used instead 599# of KEY_COLOR or KEY_IMAGE. 600# Defaults: 103 (G), 104 (H), 106 (J), 107 (K) 601KEY_VIS1 = 103 602KEY_VIS2 = 104 603KEY_VIS3 = 106 604KEY_VIS4 = 107 605 606 607# These are used in the Combination N-Back modes. 608# Visual & n-visual match. Default: 115 (S) 609KEY_VISVIS = 115 610# Visual & n-audio match. Default: 100 (D) 611KEY_VISAUDIO = 100 612# Sound & n-visual match. Default: 106 (J) 613KEY_AUDIOVIS = 106 614 615# Advance to the next trial in self-paced mode. Default: 65293 (return/enter). 616# You may also like space (32). 617KEY_ADVANCE = 65293 618 619###################################################################### 620# This is the end of the configuration file. 621###################################################################### 622""" 623 624class dotdict(dict): 625 def __getattr__(self, attr): 626 return self.get(attr, None) 627 __setattr__= dict.__setitem__ 628 __delattr__= dict.__delitem__ 629 630def dump_pyglet_info(): 631 from pyglet import info 632 oldStdout = sys.stdout 633 pygletDumpPath = os.path.join(get_data_dir(), 'dump.txt') 634 sys.stdout = open(pygletDumpPath, 'w') 635 info.dump() 636 sys.stdout.close() 637 sys.stdout = oldStdout 638 print("pyglet info dumped to %s" % pygletDumpPath) 639 sys.exit() 640 641# parse config file & command line options 642if '--debug' in sys.argv: 643 DEBUG = True 644if '--vsync' in sys.argv or sys.platform == 'darwin': 645 VSYNC = True 646if '--dump' in sys.argv: 647 dump_pyglet_info() 648if get_argv('--configfile'): 649 CONFIGFILE = get_argv('--configfile') 650 651messagequeue = [] # add messages generated during loading here 652class Message: 653 def __init__(self, msg): 654 if not 'window' in globals(): 655 print(msg) # dump it to console just in case 656 messagequeue.append(msg) # but we'll display this later 657 return 658 self.batch = pyglet.graphics.Batch() 659 self.label = pyglet.text.Label(msg, 660 font_name=self.fontlist_serif, 661 color=cfg.COLOR_TEXT, 662 batch=self.batch, 663 multiline=True, 664 width=(4*window.width)/5, 665 font_size=calc_fontsize(14), 666 x=width_center(), y=height_center(), 667 anchor_x='center', anchor_y='center') 668 window.push_handlers(self.on_key_press, self.on_draw) 669 self.on_draw() 670 671 def on_key_press(self, sym, mod): 672 if sym: 673 self.close() 674 return pyglet.event.EVENT_HANDLED 675 676 def close(self): 677 return window.remove_handlers(self.on_key_press, self.on_draw) 678 679 def on_draw(self): 680 window.clear() 681 self.batch.draw() 682 return pyglet.event.EVENT_HANDLED 683 684def check_and_move_user_data(): 685 if not '--datadir' in sys.argv and \ 686 (not os.path.exists(get_data_dir()) or len(os.listdir(get_data_dir())) < 1): 687 import shutil 688 shutil.copytree(get_old_data_dir(), get_data_dir()) 689 if len(os.listdir(get_old_data_dir())) > 2: 690 Message( 691"""Starting with version 4.8.2, Brain Workshop stores its user profiles \ 692(statistics and configuration data) in "%s", rather than the old location, "%s". \ 693Your configuration data has been copied to the new location. The files in the \ 694old location have not been deleted. If you want to edit your config.ini, \ 695make sure you look in "%s". 696 697Press space to continue.""" % (get_data_dir(), get_old_data_dir(), get_data_dir())) 698 699def load_last_user(lastuserpath): 700 path = os.path.join(get_data_dir(), lastuserpath) 701 if os.path.isfile(path): 702 debug_msg("Trying to load '%s'" % (path)) 703 try: 704 f = open(path, 'rb') 705 p = pickle.Unpickler(f) 706 options = p.load() 707 del p 708 f.close() 709 except Exception as e: 710 print("%s\nDue to error, continuing as user 'default'" % e) 711 # Delete the pickle file, since it wasn't able to be loaded. 712 os.remove(path) 713 return 714 if options['USER'] == '': 715 print("Last loaded user is an empty string! Setting it to default instead") 716 options['USER'] = "default" 717 if not options['USER'].lower() == 'default': 718 global USER 719 global STATS_BINARY 720 global CONFIGFILE 721 USER = options['USER'] 722 CONFIGFILE = USER + '-config.ini' 723 STATS_BINARY = USER + '-logfile.dat' 724 725def save_last_user(lastuserpath): 726 try: 727 f = open(os.path.join(get_data_dir(), lastuserpath), 'wb') 728 p = pickle.Pickler(f) 729 p.dump({'USER': USER}) 730 # also do date of last session? 731 except Exception as e: 732 error_msg("Could not save last user", e) 733 pass 734 735def parse_config(configpath): 736 if not (CLINICAL_MODE and configpath == 'config.ini'): 737 fullpath = os.path.join(get_data_dir(), configpath) 738 if not os.path.isfile(fullpath): 739 rewrite_configfile(configpath, overwrite=False) 740 741 # The following is a routine to overwrite older config files with the new one. 742 oldconfigfile = open(fullpath, 'r+') 743 while oldconfigfile: 744 line = oldconfigfile.readline() 745 if line == '': # EOF reached. string 'generated by Brain Workshop' not found 746 oldconfigfile.close() 747 rewrite_configfile(configpath, overwrite=True) 748 break 749 if line.find('generated by Brain Workshop') > -1: 750 splitline = line.split() 751 version = splitline[5] 752 if version < CONFIG_OVERWRITE_IF_OLDER_THAN: 753 oldconfigfile.close() 754 os.rename(fullpath, fullpath + '.' + version + '.bak') 755 rewrite_configfile(configpath, overwrite=True) 756 break 757 oldconfigfile.close() 758 759 try: 760 config = ConfigParser.ConfigParser() 761 config.read(os.path.join(get_data_dir(), configpath)) 762 except Exception as e: 763 debug_msg(e) 764 if configpath != 'config.ini': 765 quit_with_error(_('Unable to load config file: %s') % 766 os.path.join(get_data_dir(), configpath)) 767 768 defaultconfig = ConfigParser.ConfigParser() 769 if sys.version_info >= (3,): 770 defaultconfig.read_file(StringIO(CONFIGFILE_DEFAULT_CONTENTS)) 771 else: 772 defaultconfig.readfp(StringIO.StringIO(CONFIGFILE_DEFAULT_CONTENTS)) 773 774 def try_eval(text): # this is a one-use function for config parsing 775 try: return eval(text) 776 except: return text 777 778 cfg = dotdict() 779 if CLINICAL_MODE and CONFIGFILE == 'config.ini': configs = (defaultconfig,) 780 else: configs = (defaultconfig, config) 781 for config in configs: # load defaultconfig first, in case of incomplete user's config.ini 782 config_items = [(k.upper(), try_eval(v)) for k, v in config.items('DEFAULT')] 783 cfg.update(config_items) 784 785 if not 'CHANCE_OF_INTERFERENCE' in cfg: 786 cfg.CHANCE_OF_INTERFERENCE = cfg.DEFAULT_CHANCE_OF_INTERFERENCE 787 rtrn = get_argv('--statsfile') 788 if rtrn: 789 cfg.STATSFILE = rtrn 790 return cfg 791 792def rewrite_configfile(configfile, overwrite=False): 793 global STATS_BINARY 794 if USER.lower() == 'default': 795 statsfile = 'stats.txt' 796 STATS_BINARY = 'logfile.dat' # or cmd-line-opts use non-default files 797 else: 798 statsfile = USER + '-stats.txt' 799 try: 800 os.stat(os.path.join(get_data_dir(), configfile)) 801 except OSError as e: 802 debug_msg(e) 803 overwrite = True 804 if overwrite: 805 f = open(os.path.join(get_data_dir(), configfile), 'w') 806 newconfigfile_contents = CONFIGFILE_DEFAULT_CONTENTS.replace( 807 'stats.txt', statsfile) 808 f.write(newconfigfile_contents) 809 f.close() 810 # let's hope nobody uses '-stats.txt' in their username 811 STATS_BINARY = statsfile.replace('-stats.txt', '-logfile.dat') 812 try: 813 os.stat(os.path.join(get_data_dir(), statsfile)) 814 except OSError as e: 815 debug_msg(e) 816 f = open(os.path.join(get_data_dir(), statsfile), 'w') 817 f.close() 818 try: 819 os.stat(os.path.join(get_data_dir(), STATS_BINARY)) 820 except OSError: 821 f = open(os.path.join(get_data_dir(), STATS_BINARY), 'w') 822 f.close() 823 824check_and_move_user_data() 825 826try: 827 path = get_data_dir() 828 os.makedirs(path) 829except OSError as e: 830 if e.errno == errno.EEXIST and os.path.isdir(path): 831 pass 832 else: 833 raise 834 835load_last_user('defaults.ini') 836 837cfg = parse_config(CONFIGFILE) 838 839if CLINICAL_MODE: 840 cfg.JAEGGI_INTERFACE_DEFAULT_SCORING = False 841 cfg.JAEGGI_MODE = True 842 cfg.JAEGGI_FORCE_OPTIONS = True 843 cfg.JAEGGI_FORCE_OPTIONS_ADDITIONAL = True 844 cfg.SKIP_TITLE_SCREEN = True 845 cfg.USE_MUSIC = False 846elif cfg.JAEGGI_INTERFACE_DEFAULT_SCORING: 847 cfg.ANIMATE_SQUARES = False 848 cfg.OLD_STYLE_SQUARES = True 849 cfg.OLD_STYLE_SHARP_CORNERS = True 850 cfg.GRIDLINES = False 851 cfg.CROSSHAIRS = True 852 cfg.SHOW_FEEDBACK = False 853 cfg.BLACK_BACKGROUND = True 854 cfg.WINDOW_FULLSCREEN = True 855 cfg.HIDE_TEXT = True 856 cfg.FIELD_EXPAND = True 857 858if cfg.JAEGGI_MODE and not cfg.JAEGGI_INTERFACE_DEFAULT_SCORING: 859 cfg.GAME_MODE = 2 860 cfg.VARIABLE_NBACK = 0 861 cfg.JAEGGI_SCORING = True 862 if cfg.JAEGGI_FORCE_OPTIONS: 863 cfg.AUDIO1_SETS = ['letters'] 864 cfg.ANIMATE_SQUARES = False 865 cfg.OLD_STYLE_SQUARES = True 866 cfg.OLD_STYLE_SHARP_CORNERS = True 867 cfg.GRIDLINES = False 868 cfg.CROSSHAIRS = True 869 cfg.SHOW_FEEDBACK = False 870 cfg.THRESHOLD_FALLBACK_SESSIONS = 1 871 cfg.NUM_TRIALS_FACTOR = 1 872 cfg.NUM_TRIALS_EXPONENT = 1 873 if cfg.JAEGGI_FORCE_OPTIONS_ADDITIONAL: 874 cfg.BLACK_BACKGROUND = True 875 cfg.WINDOW_FULLSCREEN = True 876 cfg.HIDE_TEXT = True 877 cfg.FIELD_EXPAND = True 878 879if not cfg.USE_SESSION_FEEDBACK: 880 cfg.USE_MUSIC = False 881 cfg.USE_APPLAUSE = False 882 883if cfg.BLACK_BACKGROUND: 884 cfg.COLOR_TEXT = cfg.COLOR_TEXT_BLK 885def get_threshold_advance(): 886 if cfg.JAEGGI_SCORING: 887 return cfg.JAEGGI_ADVANCE 888 return cfg.THRESHOLD_ADVANCE 889def get_threshold_fallback(): 890 if cfg.JAEGGI_SCORING: 891 return cfg.JAEGGI_FALLBACK 892 return cfg.THRESHOLD_FALLBACK 893 894# this function checks if a new update for Brain Workshop is available. 895update_available = False 896update_version = 0 897def update_check(): 898 global update_available 899 global update_version 900 socket.setdefaulttimeout(TIMEOUT_SILENT) 901 if sys.version_info >= (3,0): 902 req = urllib.request.Request(WEB_VERSION_CHECK) 903 else: 904 req = urllib.Request(WEB_VERSION_CHECK) 905 try: 906 response = urllib.urlopen(req) 907 version = response.readline().strip() 908 except Exception as e: 909 debug_msg(e) 910 return 911 if version > VERSION: # simply comparing strings works just fine 912 update_available = True 913 update_version = version 914 915if cfg.VERSION_CHECK_ON_STARTUP and not CLINICAL_MODE: 916 update_check() 917try: 918 # workaround for pyglet.gl.ContextException error on certain video cards. 919 os.environ["PYGLET_SHADOW_WINDOW"] = "0" 920 # import pyglet 921 import pyglet 922 from pyglet.gl import * 923 if NOVBO: pyglet.options['graphics_vbo'] = False 924 from pyglet.window import key 925except Exception as e: 926 debug_msg(e) 927 quit_with_error(_('Error: unable to load pyglet. If you already installed pyglet, please ensure ctypes is installed. Please visit %s') % WEB_PYGLET_DOWNLOAD) 928try: 929 pyglet.options['audio'] = ('directsound', 'openal', 'alsa', ) 930 # use in pyglet 1.2: pyglet.options['audio'] = ('directsound', 'pulse', 'openal', ) 931 import pyglet.media 932except Exception as e: 933 debug_msg(e) 934 quit_with_error(_('No suitable audio driver could be loaded.')) 935 936# Initialize resources (sounds and images) 937# 938# --- BEGIN RESOURCE INITIALIZATION SECTION ---------------------------------- 939# 940 941res_path = get_res_dir() 942if not os.access(res_path, os.F_OK): 943 quit_with_error(_('Error: the resource folder\n%s') % res_path + 944 _(' does not exist or is not readable. Exiting'), trace=False) 945 946if pyglet.version < '1.1': 947 quit_with_error(_('Error: pyglet 1.1 or greater is required.\n') + 948 _('You probably have an older version of pyglet installed.\n') + 949 _('Please visit %s') % WEB_PYGLET_DOWNLOAD, trace=False) 950 951supportedtypes = {'sounds' :['wav'], 952 'music' :['wav', 'ogg', 'mp3', 'aac', 'mp2', 'ac3', 'm4a'], # what else? 953 'sprites':['png', 'jpg', 'bmp']} 954 955def test_music(): 956 try: 957 import pyglet 958 if pyglet.version >= '1.4': 959 from pyglet.media import have_ffmpeg 960 pyglet.media.have_avbin = have_ffmpeg() 961 if not pyglet.media.have_avbin: 962 cfg.USE_MUSIC = False 963 else: 964 try: 965 from pyglet.media import avbin 966 except Exception as e: 967 debug_msg(e) 968 pyglet.lib.load_library('avbin') 969 if pyglet.version >= '1.2': # temporary workaround for defect in pyglet svn 2445 970 pyglet.media.have_avbin = True 971 972 # On Windows with Data Execution Protection enabled (on by default on Vista), 973 # an exception will be raised when use of avbin is attempted: 974 # WindowsError: exception: access violation writing [ADDRESS] 975 # The file doesn't need to be in a avbin-specific format, 976 # since pyglet will use avbin over riff whenever it's detected. 977 # Let's find an audio file and try to load it to see if avbin works. 978 opj = os.path.join 979 opj = os.path.join 980 def look_for_music(path): 981 files = [p for p in os.listdir(path) if not p.startswith('.') and not os.path.isdir(opj(path, p))] 982 for f in files: 983 ext = f.lower()[-3:] 984 if ext in ['wav', 'ogg', 'mp3', 'aac', 'mp2', 'ac3', 'm4a'] and not ext in ('wav'): 985 return [opj(path, f)] 986 dirs = [opj(path, p) for p in os.listdir(path) if not p.startswith('.') and os.path.isdir(opj(path, p))] 987 results = [] 988 for d in dirs: 989 results.extend(look_for_music(d)) 990 if results: return results 991 return results 992 music_file = look_for_music(res_path) 993 if music_file: 994 # The first time we load a file should trigger the exception 995 music_file = music_file[0] 996 loaded_music = pyglet.media.load(music_file, streaming=False) 997 del loaded_music 998 else: 999 cfg.USE_MUSIC = False 1000 1001 except ImportError as e: 1002 debug_msg(e) 1003 cfg.USE_MUSIC = False 1004 if pyglet.version >= '1.2': 1005 pyglet.media.have_avbin = False 1006 print( _('AVBin not detected. Music disabled.')) 1007 print( _('Download AVBin from: https://avbin.github.io')) 1008 1009 except Exception as e: # WindowsError 1010 debug_msg(e) 1011 cfg.USE_MUSIC = False 1012 pyglet.media.have_avbin = False 1013 if hasattr(pyglet.media, '_source_class'): # pyglet v1.1 1014 import pyglet.media.riff 1015 pyglet.media._source_class = pyglet.media.riff.WaveSource 1016 elif hasattr(pyglet.media, '_source_loader'): # pyglet v1.2 and development branches 1017 import pyglet.media.riff 1018 pyglet.media._source_loader = pyglet.media.RIFFSourceLoader() 1019 Message("""Warning: Could not load AVbin. Music disabled. 1020 1021This is usually due to Windows Data Execution Prevention (DEP). Due to a bug in 1022AVbin, a library used for decoding sound files, music is not available when \ 1023DEP is enabled. To enable music, disable DEP for Brain Workshop. To simply get \ 1024rid of this message, set USE_MUSIC = False in your config.ini file. 1025 1026To disable DEP: 1027 10281. Open Control Panel -> System 10292. Select Advanced System Settings 10303. Click on Performance -> Settings 10314. Click on the Data Execution Prevention tab 10325. Either select the "Turn on DEP for essential Windows programs and services \ 1033only" option, or add an exception for Brain Workshop. 1034 1035Press any key to continue without music support. 1036""") 1037 1038test_music() 1039if pyglet.media.have_avbin: supportedtypes['sounds'] = supportedtypes['music'] 1040elif cfg.USE_MUSIC: supportedtypes['music'] = supportedtypes['sounds'] 1041else: del supportedtypes['music'] 1042 1043supportedtypes['misc'] = supportedtypes['sounds'] + supportedtypes['sprites'] 1044 1045resourcepaths = {} 1046for restype in list(supportedtypes): 1047 res_sets = {} 1048 for folder in os.listdir(os.path.join(res_path, restype)): 1049 contents = [] 1050 if os.path.isdir(os.path.join(res_path, restype, folder)): 1051 contents = [os.path.join(res_path, restype, folder, obj) 1052 for obj in os.listdir(os.path.join(res_path, restype, folder)) 1053 if obj[-3:] in supportedtypes[restype]] 1054 contents.sort() 1055 if contents: res_sets[folder] = contents 1056 if res_sets: resourcepaths[restype] = res_sets 1057 1058sounds = {} 1059for k in list(resourcepaths['sounds']): 1060 sounds[k] = {} 1061 for f in resourcepaths['sounds'][k]: 1062 sounds[k][os.path.basename(f).split('.')[0]] = pyglet.media.load(f, streaming=False) 1063 1064sound = sounds['letters'] # is this obsolete yet? 1065 1066if cfg.USE_APPLAUSE: 1067 applausesounds = [pyglet.media.load(soundfile, streaming=False) 1068 1069 for soundfile in resourcepaths['misc']['applause']] 1070 1071applauseplayer = get_pyglet_media_Player() 1072musicplayer = get_pyglet_media_Player() 1073def play_applause(): 1074 applauseplayer.queue(random.choice(applausesounds)) 1075 applauseplayer.volume = cfg.SFX_VOLUME 1076 if DEBUG: print("Playing applause") 1077 applauseplayer.play() 1078def play_music(percent): 1079 if 'music' in resourcepaths: 1080 if preventMusicSkipping: pyglet.clock.tick(poll=True) # Prevent music skipping 1 1081 if percent >= get_threshold_advance() and 'advance' in resourcepaths['music']: 1082 musicplayer.queue(pyglet.media.load(random.choice(resourcepaths['music']['advance']), streaming = True)) 1083 elif percent >= (get_threshold_advance() + get_threshold_fallback()) // 2 and 'great' in resourcepaths['music']: 1084 musicplayer.queue(pyglet.media.load(random.choice(resourcepaths['music']['great']), streaming = True)) 1085 elif percent >= get_threshold_fallback() and 'good' in resourcepaths['music']: 1086 musicplayer.queue(pyglet.media.load(random.choice(resourcepaths['music']['good']), streaming = True)) 1087 else: 1088 return 1089 else: 1090 return 1091 musicplayer.volume = cfg.MUSIC_VOLUME 1092 if DEBUG: print("Playing music") 1093 musicplayer.play() 1094def sound_stop(): 1095 global applauseplayer 1096 global musicplayer 1097 musicplayer.volume = 0 1098 applauseplayer.volume = 0 1099def fade_out(dt): 1100 global applauseplayer 1101 global musicplayer 1102 1103 if musicplayer.volume > 0: 1104 if musicplayer.volume <= 0.1: 1105 musicplayer.volume -= 0.02 1106 else: musicplayer.volume -= 0.1 1107 if musicplayer.volume <= 0.02: 1108 musicplayer.volume = 0 1109 if applauseplayer.volume > 0: 1110 if applauseplayer.volume <= 0.1: 1111 applauseplayer.volume -= 0.02 1112 else: applauseplayer.volume -= 0.1 1113 if applauseplayer.volume <= 0.02: 1114 applauseplayer.volume = 0 1115 1116 if (applauseplayer.volume == 0 and musicplayer.volume == 0) or mode.trial_number == 3: 1117 pyglet.clock.unschedule(fade_out) 1118 1119 1120# 1121# --- END RESOURCE INITIALIZATION SECTION ---------------------------------- 1122# 1123 1124 1125# The colors of the squares in Triple N-Back mode are defined here. 1126# Color 1 is used in Dual N-Back mode. 1127def get_color(color): 1128 if color in (4, 7) and cfg.BLACK_BACKGROUND: 1129 return cfg['COLOR_%i_BLK' % color] 1130 return cfg['COLOR_%i' % color] 1131 1132def default_nback_mode(mode): 1133 if ('BACK_%i' % mode) in cfg: 1134 return cfg['BACK_%i' % mode] 1135 elif mode > 127: # try to use the base mode for crab, multi 1136 return default_nback_mode(mode % 128) 1137 else: 1138 return cfg.BACK_DEFAULT 1139 1140 1141def default_ticks(mode): 1142 if ('TICKS_%i' % mode) in cfg: 1143 return cfg['TICKS_%i' % mode] 1144 elif mode > 127: 1145 bonus = ((mode & 128)/128) * cfg.BONUS_TICKS_CRAB 1146 if mode & 768: 1147 bonus += cfg['BONUS_TICKS_MULTI_%i' % ((mode & 768)/256+1)] 1148 if DEBUG: print("Adding a bonus of %i ticks for mode %i" % (bonus, mode)) 1149 return bonus + default_ticks(mode % 128) 1150 else: 1151 return cfg.TICKS_DEFAULT 1152 1153#Create the game window 1154caption = [] 1155if CLINICAL_MODE: 1156 caption.append('BW-Clinical ') 1157else: 1158 caption.append('Brain Workshop ') 1159caption.append(VERSION) 1160if USER != 'default': 1161 caption.append(' - ') 1162 caption.append(USER) 1163if cfg.WINDOW_FULLSCREEN: 1164 style = pyglet.window.Window.WINDOW_STYLE_BORDERLESS 1165else: 1166 style = pyglet.window.Window.WINDOW_STYLE_DEFAULT 1167 1168class MyWindow(pyglet.window.Window): 1169 def on_key_press(self, symbol, modifiers): 1170 pass 1171 def on_key_release(self, symbol, modifiers): 1172 pass 1173if cfg.WINDOW_FULLSCREEN: 1174 screen = pyglet.window.get_platform().get_default_display().get_default_screen() 1175 cfg.WINDOW_WIDTH_FULLSCREEN = screen.width 1176 cfg.WINDOW_HEIGHT_FULLSCREEN = screen.height 1177 window = MyWindow(cfg.WINDOW_WIDTH_FULLSCREEN, cfg.WINDOW_HEIGHT_FULLSCREEN, caption=''.join(caption), style=style, vsync=VSYNC, fullscreen=True) 1178else: 1179 window = MyWindow(cfg.WINDOW_WIDTH, cfg.WINDOW_HEIGHT, caption=''.join(caption), style=style, vsync=VSYNC) 1180pyglet.gl.glLineWidth(calc_fontsize(2)) 1181#if DEBUG: 1182# window.push_handlers(pyglet.window.event.WindowEventLogger()) 1183if sys.platform == 'darwin' and cfg.WINDOW_FULLSCREEN: 1184 window.set_exclusive_keyboard() 1185if sys.platform == 'linux2': 1186 window.set_icon(pyglet.image.load(resourcepaths['misc']['brain'][0])) 1187 1188# set the background color of the window 1189if cfg.BLACK_BACKGROUND: 1190 glClearColor(0, 0, 0, 1) 1191else: 1192 glClearColor(1, 1, 1, 1) 1193if cfg.WINDOW_FULLSCREEN: 1194 window.maximize() 1195 window.set_fullscreen(cfg.WINDOW_FULLSCREEN) 1196 window.set_mouse_visible(False) 1197 1198 1199# All changeable game state variables are located in an instance of the Mode class 1200class Mode: 1201 def __init__(self): 1202 self.mode = cfg.GAME_MODE 1203 self.back = default_nback_mode(self.mode) 1204 self.ticks_per_trial = default_ticks(self.mode) 1205 self.num_trials = cfg.NUM_TRIALS 1206 self.num_trials_factor = cfg.NUM_TRIALS_FACTOR 1207 self.num_trials_exponent = cfg.NUM_TRIALS_EXPONENT 1208 self.num_trials_total = self.num_trials + self.num_trials_factor * \ 1209 self.back ** self.num_trials_exponent 1210 1211 self.short_mode_names = {2:'D', 1212 3:'PCA', 1213 4:'DC', 1214 5:'TC', 1215 6:'QC', 1216 7:'A', 1217 8:'DA', 1218 9:'TA', 1219 10:'Po', 1220 11:'Au', 1221 12:'TCC', 1222 20:'PC', 1223 21:'PI', 1224 22:'CA', 1225 23:'IA', 1226 24:'CI', 1227 25:'PCI', 1228 26:'PIA', 1229 27:'CIA', 1230 28:'Q', 1231 100:'AA', 1232 101:'PAA', 1233 102:'CAA', 1234 103:'IAA', 1235 104:'PCAA', 1236 105:'PIAA', 1237 106:'CIAA', 1238 107:'P' 1239 } 1240 1241 self.long_mode_names = {2:_('Dual'), 1242 3:_('Position, Color, Sound'), 1243 4:_('Dual Combination'), 1244 5:_('Tri Combination'), 1245 6:_('Quad Combination'), 1246 7:_('Arithmetic'), 1247 8:_('Dual Arithmetic'), 1248 9:_('Triple Arithmetic'), 1249 10:_('Position'), 1250 11:_('Sound'), 1251 12:_('Tri Combination (Color)'), 1252 20:_('Position, Color'), 1253 21:_('Position, Image'), 1254 22:_('Color, Sound'), 1255 23:_('Image, Sound'), 1256 24:_('Color, Image'), 1257 25:_('Position, Color, Image'), 1258 26:_('Position, Image, Sound'), 1259 27:_('Color, Image, Sound'), 1260 28:_('Quad'), 1261 100:_('Sound, Sound2'), 1262 101:_('Position, Sound, Sound2'), 1263 102:_('Color, Sound, Sound2'), 1264 103:_('Image, Sound, Sound2'), 1265 104:_('Position, Color, Sound, Sound2'), 1266 105:_('Position, Image, Sound, Sound2'), 1267 106:_('Color, Image, Sound, Sound2'), 1268 107:_('Pentuple') 1269 } 1270 1271 self.modalities = { 2:['position1', 'audio'], 1272 3:['position1', 'color', 'audio'], 1273 4:['visvis', 'visaudio', 'audiovis', 'audio'], 1274 5:['position1', 'visvis', 'visaudio', 'audiovis', 'audio'], 1275 6:['position1', 'visvis', 'visaudio', 'color', 'audiovis', 'audio'], 1276 7:['arithmetic'], 1277 8:['position1', 'arithmetic'], 1278 9:['position1', 'arithmetic', 'color'], 1279 10:['position1'], 1280 11:['audio'], 1281 12:['visvis', 'visaudio', 'color', 'audiovis', 'audio'], 1282 20:['position1', 'color'], 1283 21:['position1', 'image'], 1284 22:['color', 'audio'], 1285 23:['image', 'audio'], 1286 24:['color', 'image'], 1287 25:['position1', 'color', 'image'], 1288 26:['position1', 'image', 'audio'], 1289 27:['color', 'image', 'audio'], 1290 28:['position1', 'color', 'image', 'audio'], 1291 100:['audio', 'audio2'], 1292 101:['position1', 'audio', 'audio2'], 1293 102:['color', 'audio', 'audio2'], 1294 103:['image', 'audio', 'audio2'], 1295 104:['position1', 'color', 'audio', 'audio2'], 1296 105:['position1', 'image', 'audio', 'audio2'], 1297 106:['color', 'image', 'audio', 'audio2'], 1298 107:['position1', 'color', 'image', 'audio', 'audio2'] 1299 } 1300 1301 self.flags = {} 1302 1303 # generate crab modes 1304 for m in list(self.short_mode_names): 1305 nm = m | 128 # newmode; Crab DNB = 2 | 128 = 130 1306 self.flags[m] = {'crab':0, 'multi':1, 'selfpaced':0}# forwards 1307 self.flags[nm] = {'crab':1, 'multi':1, 'selfpaced':0}# every (self.back) stimuli are reversed for matching 1308 self.short_mode_names[nm] = 'C' + self.short_mode_names[m] 1309 self.long_mode_names[nm] = _('Crab ') + self.long_mode_names[m] 1310 self.modalities[nm] = self.modalities[m][:] # the [:] at the end is 1311 # so we take a copy of the list, in case we want to change it later 1312 1313 # generate multi-stim modes 1314 for m in list(self.short_mode_names): 1315 for n, s in [(2, _('Double-stim')), (3, _('Triple-stim')), (4, _('Quadruple-stim'))]: 1316 if set(['color', 'image']).issubset(self.modalities[m]) \ 1317 or not 'position1' in self.modalities[m] \ 1318 or set(['visvis', 'arithmetic']).intersection(self.modalities[m]): # Combination? AAAH! Scary! 1319 continue 1320 nm = m | 256 * (n-1) # newmode; 3xDNB = 2 | 512 = 514 1321 self.flags[nm] = dict(self.flags[m]) # take a copy 1322 self.flags[nm]['multi'] = n 1323 self.short_mode_names[nm] = repr(n) + 'x' + self.short_mode_names[m] 1324 self.long_mode_names[nm] = s + ' ' + self.long_mode_names[m] 1325 self.modalities[nm] = self.modalities[m][:] # take a copy ([:]) 1326 for i in range(2, n+1): 1327 self.modalities[nm].insert(i-1, 'position'+repr(i)) 1328 if 'color' in self.modalities[m] or 'image' in self.modalities[m]: 1329 for i in range(1, n+1): 1330 self.modalities[nm].insert(n+i-1, 'vis'+repr(i)) 1331 for ic in 'image', 'color': 1332 if ic in self.modalities[nm]: 1333 self.modalities[nm].remove(ic) 1334 1335 for m in list(self.short_mode_names): 1336 nm = m | 1024 1337 self.short_mode_names[nm] = 'SP-' + self.short_mode_names[m] 1338 self.long_mode_names[nm] = 'Self-paced ' + self.long_mode_names[m] 1339 self.modalities[nm] = self.modalities[m][:] 1340 self.flags[nm] = dict(self.flags[m]) 1341 self.flags[nm]['selfpaced'] = 1 1342 1343 1344 self.variable_list = [] 1345 1346 self.manual = cfg.MANUAL 1347 if not self.manual: 1348 self.enforce_standard_mode() 1349 1350 self.inputs = {'position1': False, 1351 'position2': False, 1352 'position3': False, 1353 'position4': False, 1354 'color': False, 1355 'image': False, 1356 'vis1': False, 1357 'vis2': False, 1358 'vis3': False, 1359 'vis4': False, 1360 'visvis': False, 1361 'visaudio': False, 1362 'audiovis': False, 1363 'audio': False, 1364 'audio2': False} 1365 1366 self.input_rts = {'position1': 0., 1367 'position2': 0., 1368 'position3': 0., 1369 'position4': 0., 1370 'color': 0., 1371 'image': 0., 1372 'vis1': 0., 1373 'vis2': 0., 1374 'vis3': 0., 1375 'vis4': 0., 1376 'visvis': 0., 1377 'visaudio': 0., 1378 'audiovis': 0., 1379 'audio': 0., 1380 'audio2': 0.} 1381 1382 self.hide_text = cfg.HIDE_TEXT 1383 1384 self.current_stim = {'position1': 0, 1385 'position2': 0, 1386 'position3': 0, 1387 'position4': 0, 1388 'color': 0, 1389 'vis': 0, # image or letter for non-multi mode 1390 'vis1': 0, # image or color for multi mode 1391 'vis2': 0, 1392 'vis3': 0, 1393 'vis4': 0, 1394 'audio': 0, 1395 'audio2': 0, 1396 'number': 0} 1397 1398 self.current_operation = 'none' 1399 1400 self.started = False 1401 self.paused = False 1402 self.show_missed = False 1403 self.sound_select = False 1404 self.draw_graph = False 1405 self.saccadic = False 1406 if cfg.SKIP_TITLE_SCREEN: 1407 self.title_screen = False 1408 else: 1409 self.title_screen = True 1410 self.shrink_brain = False 1411 1412 self.session_number = 0 1413 self.trial_number = 0 1414 self.tick = 0 1415 self.progress = 0 1416 1417 self.sound_mode = 'none' 1418 self.sound2_mode = 'none' 1419 self.soundlist = [] 1420 self.soundlist2 = [] 1421 1422 self.bt_sequence = [] 1423 1424 def enforce_standard_mode(self): 1425 self.back = default_nback_mode(self.mode) 1426 self.ticks_per_trial = default_ticks(self.mode) 1427 self.num_trials = cfg.NUM_TRIALS 1428 self.num_trials_factor = cfg.NUM_TRIALS_FACTOR 1429 self.num_trials_exponent = cfg.NUM_TRIALS_EXPONENT 1430 self.num_trials_total = self.num_trials + self.num_trials_factor * \ 1431 self.back ** self.num_trials_exponent 1432 self.session_number = 0 1433 1434 def short_name(self, mode=None, back=None): 1435 if mode == None: mode = self.mode 1436 if back == None: back = self.back 1437 return self.short_mode_names[mode] + str(back) + 'B' 1438 1439# What follows are the classes which control all the text and graphics. 1440# 1441# --- BEGIN GRAPHICS SECTION ---------------------------------------------- 1442# 1443 1444class Graph: 1445 def __init__(self): 1446 self.graph = 2 1447 self.reset_dictionaries() 1448 self.reset_percents() 1449 self.batch = None 1450 self.styles = ['N+10/3+4/3', 'N', '%', 'N.%', 'N+2*%-1'] 1451 self.style = 0 1452 1453 def next_style(self): 1454 self.style = (self.style + 1) % len(self.styles) 1455 print("style = %s" % self.styles[self.style]) # fixme: change the labels 1456 self.parse_stats() 1457 1458 def reset_dictionaries(self): 1459 self.dictionaries = dict([(i, {}) for i in mode.modalities]) 1460 1461 def reset_percents(self): 1462 self.percents = dict([(k, dict([(i, []) for i in v])) for k,v in mode.modalities.items()]) 1463 1464 def next_nonempty_mode(self): 1465 self.next_mode() 1466 mode1 = self.graph 1467 mode2 = None # to make sure the loop runs the first iteration 1468 while self.graph != mode2 and not self.dictionaries[self.graph]: 1469 self.next_mode() 1470 mode2 = mode1 1471 def next_mode(self): 1472 modes = list(mode.modalities) 1473 modes.sort() 1474 i = modes.index(self.graph) 1475 i = (i + 1) % len(modes) 1476 self.graph = modes[i] 1477 self.batch = None 1478 1479 def parse_stats(self): 1480 self.batch = None 1481 self.reset_dictionaries() 1482 self.reset_percents() 1483 ind = {'date':0, 'modename':1, 'percent':2, 'mode':3, 'n':4, 'ticks':5, 1484 'trials':6, 'manual':7, 'session':8, 'position1':9, 'audio':10, 1485 'color':11, 'visvis':12, 'audiovis':13, 'arithmetic':14, 1486 'image':15, 'visaudio':16, 'audio2':17, 'position2':18, 1487 'position3':19, 'position4':20, 'vis1':21, 'vis2':22, 'vis3':23, 1488 'vis4':24} 1489 1490 if os.path.isfile(os.path.join(get_data_dir(), cfg.STATSFILE)): 1491 try: 1492 statsfile_path = os.path.join(get_data_dir(), cfg.STATSFILE) 1493 statsfile = open(statsfile_path, 'r') 1494 for line in statsfile: 1495 if line == '': continue 1496 if line == '\n': continue 1497 if line[0] not in '0123456789': continue 1498 datestamp = date(int(line[:4]), int(line[5:7]), int(line[8:10])) 1499 hour = int(line[11:13]) 1500 if hour < cfg.ROLLOVER_HOUR: 1501 datestamp = date.fromordinal(datestamp.toordinal() - 1) 1502 if line.find('\t') >= 0: 1503 separator = '\t' 1504 else: separator = ',' 1505 newline = line.split(separator) 1506 try: 1507 if int(newline[7]) != 0: # only consider standard mode 1508 continue 1509 except: 1510 continue 1511 newmode = int(newline[3]) 1512 newback = int(newline[4]) 1513 1514 while len(newline) < 24: 1515 newline.append('0') # make it work for image mode, missing visaudio and audio2 1516 if len(newline) >= 16: 1517 for m in mode.modalities[newmode]: 1518 self.percents[newmode][m].append(int(newline[ind[m]])) 1519 1520 dictionary = self.dictionaries[newmode] 1521 if datestamp not in dictionary: 1522 dictionary[datestamp] = [] 1523 dictionary[datestamp].append([newback] + [int(newline[2])] + \ 1524 [self.percents[newmode][n][-1] for n in mode.modalities[newmode]]) 1525 1526 statsfile.close() 1527 except: 1528 quit_with_error(_('Error parsing stats file\n %s') % 1529 os.path.join(get_data_dir(), cfg.STATSFILE), 1530 _('Please fix, delete or rename the stats file.')) 1531 1532 def mean(x): 1533 if len(x): 1534 return sum(x)/float(len(x)) 1535 else: 1536 return 0. 1537 def cent(x): 1538 return map(lambda y: .01*y, x) 1539 1540 for dictionary in self.dictionaries.values(): 1541 for datestamp in list(dictionary): # this would be so much easier with numpy 1542 entries = dictionary[datestamp] 1543 if self.styles[self.style] == 'N': 1544 scores = [entry[0] for entry in entries] 1545 elif self.styles[self.style] == '%': 1546 scores = [.01*entry[1] for entry in entries] 1547 elif self.styles[self.style] == 'N.%': 1548 scores = [entry[0] + .01*entry[1] for entry in entries] 1549 elif self.styles[self.style] == 'N+2*%-1': 1550 scores = [entry[0] - 1 + 2*.01*entry[1] for entry in entries] 1551 elif self.styles[self.style] == 'N+10/3+4/3': 1552 adv, flb = get_threshold_advance(), get_threshold_fallback() 1553 m = 1./(adv - flb) 1554 b = -m*flb 1555 scores = [entry[0] + b + m*(entry[1]) for entry in entries] 1556 dictionary[datestamp] = (mean(scores), max(scores)) 1557 1558 for game in self.percents: 1559 for category in self.percents[game]: 1560 pcts = self.percents[game][category][-50:] 1561 if not pcts: 1562 self.percents[game][category].append(0) 1563 else: 1564 self.percents[game][category].append(sum(pcts)/len(pcts)) 1565 1566 #def export_data(self): 1567 #dictionary = {} 1568 #for x in self.dictionaries: # cycle through game modes 1569 #chartfile_name = CHARTFILE[x] 1570 #dictionary = self.dictionaries[x] 1571 #output = ['Date\t%s N-Back Average\n' % mode.long_mode_names[x]] 1572 1573 #keyslist = list(dictionary) 1574 #keyslist.sort() 1575 #if len(keyslist) == 0: continue 1576 #for datestamp in keyslist: 1577 #if dictionary[datestamp] == (-1, -1): 1578 #continue 1579 #output.append(str(datestamp)) 1580 #output.append('\t') 1581 #output.append(str(dictionary[datestamp])) 1582 #output.append('\n') 1583 1584 #try: 1585 #chartfile_path = os.path.join(get_data_dir(), chartfile_name) 1586 #chartfile = open(chartfile_path, 'w') 1587 #chartfile.write(''.join(output)) 1588 #chartfile.close() 1589 1590 #except: 1591 #quit_with_error('Error writing chart file:\n%s' % 1592 #os.path.join(get_data_dir(), chartfile_name)) 1593 1594 1595 def draw(self): 1596 if not self.batch: 1597 self.create_batch() 1598 else: 1599 self.batch.draw() 1600 1601 def create_batch(self): 1602 self.batch = pyglet.graphics.Batch() 1603 1604 linecolor = (0, 0, 255) 1605 linecolor2 = (255, 0, 0) 1606 if cfg.BLACK_BACKGROUND: 1607 axiscolor = (96, 96, 96) 1608 minorcolor = (64, 64, 64) 1609 else: 1610 axiscolor = (160, 160, 160) 1611 minorcolor = (224, 224, 224) 1612 y_marking_interval = 0.25 # This doesn't need scaling 1613 x_label_width = 20 # TODO does this need to be scaled too? 1614 1615 height = int(window.height * 0.625) 1616 width = int(window.width * 0.625) 1617 center_x = width_center() 1618 center_y = from_height_center(20) 1619 left = center_x - width // 2 1620 right = center_x + width // 2 1621 top = center_y + height // 2 1622 bottom = center_y - height // 2 1623 try: 1624 dictionary = self.dictionaries[self.graph] 1625 except: 1626 print(self.graph) 1627 graph_title = mode.long_mode_names[self.graph] + _(' N-Back') 1628 1629 self.batch.add(3, GL_LINE_STRIP, 1630 pyglet.graphics.OrderedGroup(order=1), ('v2i', ( 1631 left, top, 1632 left, bottom, 1633 right, bottom)), ('c3B', axiscolor * 3)) 1634 1635 pyglet.text.Label( 1636 _('G: Return to Main Screen\n\nN: Next Game Type'), 1637 batch=self.batch, 1638 multiline = True, width = scale_to_width(300), 1639 font_size=calc_fontsize(9), 1640 color=cfg.COLOR_TEXT, 1641 x=from_left_edge(10), y=from_top_edge(10), 1642 anchor_x='left', anchor_y='top') 1643 1644 pyglet.text.Label(graph_title, 1645 batch=self.batch, 1646 font_size=calc_fontsize(18), bold=True, color=cfg.COLOR_TEXT, 1647 x = center_x, y = top + scale_to_height(60), 1648 anchor_x = 'center', anchor_y = 'center') 1649 1650 pyglet.text.Label(_('Date'), 1651 batch=self.batch, 1652 font_size=calc_fontsize(12), bold=True, color=cfg.COLOR_TEXT, 1653 x = center_x, y = bottom - scale_to_height(80), 1654 anchor_x = 'center', anchor_y = 'center') 1655 1656 pyglet.text.Label(_('Maximum'), width=scale_to_width(1), 1657 batch=self.batch, 1658 font_size=calc_fontsize(12), bold=True, color=linecolor2+(255,), 1659 x = left - scale_to_width(60), y = center_y + scale_to_height(50), 1660 anchor_x = 'right', anchor_y = 'center') 1661 1662 pyglet.text.Label(_('Average'), width=scale_to_width(1), 1663 batch=self.batch, 1664 font_size=calc_fontsize(12), bold=True, color=linecolor+(255,), 1665 x = left - scale_to_width(60), y = center_y + scale_to_height(25), 1666 anchor_x = 'right', anchor_y = 'center') 1667 1668 pyglet.text.Label(_('Score'), width=scale_to_width(1), 1669 batch=self.batch, 1670 font_size=calc_fontsize(12), bold=True, color=cfg.COLOR_TEXT, 1671 x = left - scale_to_width(60), y = center_y, 1672 anchor_x = 'right', anchor_y = 'center') 1673 1674 dates = list(dictionary) 1675 dates.sort() 1676 if len(dates) < 2: 1677 pyglet.text.Label(_('Insufficient data: two days needed'), 1678 batch=self.batch, 1679 font_size=calc_fontsize(12), bold = True, color = axiscolor + (255,), 1680 x = center_x, y = center_y, 1681 anchor_x = 'center', anchor_y = 'center') 1682 return 1683 1684 ymin = 100000.0 1685 ymax = 0.0 1686 for entry in dates: 1687 if dictionary[entry] == (-1, -1): 1688 continue 1689 if dictionary[entry][0] < ymin: 1690 ymin = dictionary[entry][0] 1691 if dictionary[entry][1] > ymax: 1692 ymax = dictionary[entry][1] 1693 if ymin == ymax: 1694 ymin = 0 1695 1696 if preventMusicSkipping: pyglet.clock.tick(poll=True) # Prevent music skipping 1 1697 1698 ymin = int(math.floor(ymin * 4))/4. 1699 ymax = int(math.ceil(ymax * 4))/4. 1700 1701 # remove these two lines to revert to the old behaviour 1702 #ymin = 1.0 1703 #ymax += 0.25 1704 1705 # add intermediate days 1706 z = 0 1707 while z < len(dates) - 1: 1708 if dates[z+1].toordinal() > dates[z].toordinal() + 1: 1709 newdate = date.fromordinal(dates[z].toordinal() + 1) 1710 dates.insert(z+1, newdate) 1711 dictionary[newdate] = (-1, -1) 1712 z += 1 1713 1714 avgpoints = [] 1715 maxpoints = [] 1716 1717 xinterval = width / (float(len(dates) - 1)) 1718 skip_x = int(x_label_width // xinterval) 1719 1720 for index in range(len(dates)): 1721 x = int(xinterval * index + left) 1722 if dictionary[dates[index]][0] != -1: 1723 avgpoints.extend([x, int((dictionary[dates[index]][0] - ymin)/(ymax - ymin) * height + bottom)]) 1724 maxpoints.extend([x, int((dictionary[dates[index]][1] - ymin)/(ymax - ymin) * height + bottom)]) 1725 datestring = str(dates[index])[2:] 1726 # If more than 10 dates, don't separate by '-' but by newlines so 1727 # they appear vertically rather than 01-01-01 1728 if 10 < len(dates): 1729 datestring = datestring.replace('-', '\n') 1730 if not index % (skip_x + 1): 1731 pyglet.text.Label(datestring, multiline=True, width=scale_to_width(12), 1732 batch=self.batch, 1733 font_size=calc_fontsize(8), bold=True, color=cfg.COLOR_TEXT, 1734 x=x, y=bottom - scale_to_height(15), 1735 anchor_x='center', anchor_y='top') 1736 self.batch.add(2, GL_LINES, 1737 pyglet.graphics.OrderedGroup(order=0), ('v2i', ( 1738 x, bottom, 1739 x, top)), ('c3B', minorcolor * 2)) 1740 self.batch.add(2, GL_LINES, 1741 pyglet.graphics.OrderedGroup(order=1), ('v2i', ( 1742 x, bottom - scale_to_height(10), 1743 x, bottom)), ('c3B', axiscolor * 2)) 1744 1745 if preventMusicSkipping: pyglet.clock.tick(poll=True) # Prevent music skipping 2 1746 1747 y_marking = ymin 1748 while y_marking <= ymax: 1749 y = int((y_marking - ymin)/(ymax - ymin) * height + bottom) 1750 pyglet.text.Label(str(round(y_marking, 2)), 1751 batch=self.batch, 1752 font_size=calc_fontsize(10), bold=False, color=cfg.COLOR_TEXT, 1753 x = left - scale_to_width(30), y = y + scale_to_width(1), 1754 anchor_x = 'center', anchor_y = 'center') 1755 self.batch.add(2, GL_LINES, 1756 pyglet.graphics.OrderedGroup(order=0), ('v2i', ( 1757 left, y, 1758 right, y)), ('c3B', minorcolor * 2)) 1759 self.batch.add(2, GL_LINES, 1760 pyglet.graphics.OrderedGroup(order=1), ('v2i', ( 1761 left - scale_to_width(10), y, 1762 left, y)), ('c3B', axiscolor * 2)) 1763 y_marking += y_marking_interval 1764 1765 self.batch.add(len(avgpoints) // 2, GL_LINE_STRIP, 1766 pyglet.graphics.OrderedGroup(order=2), ('v2i', 1767 avgpoints), 1768 ('c3B', linecolor * (len(avgpoints) // 2))) 1769 self.batch.add(len(maxpoints) // 2, GL_LINE_STRIP, 1770 pyglet.graphics.OrderedGroup(order=3), ('v2i', 1771 maxpoints), 1772 ('c3B', linecolor2 * (len(maxpoints) // 2))) 1773 1774 if preventMusicSkipping: pyglet.clock.tick(poll=True) # Prevent music skipping 3 1775 1776 radius = scale_to_height(3) 1777 o = 4 1778 for index in range(0, len(avgpoints) // 2): 1779 x = avgpoints[index * 2] 1780 avg = avgpoints[index * 2 + 1] 1781 max = maxpoints[index * 2 + 1] 1782 # draw average 1783 self.batch.add(4, GL_POLYGON, 1784 pyglet.graphics.OrderedGroup(order=o), ('v2i', 1785 (x - radius, avg - radius, 1786 x - radius, avg + radius, 1787 x + radius, avg + radius, 1788 x + radius, avg - radius)), 1789 ('c3B', linecolor * 4)) 1790 o += 1 1791 # draw maximum 1792 self.batch.add(4, GL_POLYGON, 1793 pyglet.graphics.OrderedGroup(order=o), ('v2i', 1794 (x - radius, max - radius, 1795 x - radius, max + radius, 1796 x + radius, max + radius, 1797 x + radius, max - radius)), 1798 ('c3B', linecolor2 * 4)) 1799 o += 1 1800 1801 if preventMusicSkipping: pyglet.clock.tick(poll=True) # Prevent music skipping 4 1802 1803 labelstrings = {'position1':_('Position: ') , 'position2':_('Position 2: '), 1804 'position3':_('Position 3: '), 'position4':_('Position 4: '), 1805 'vis1':_('Color/Image 1: '), 'vis2':_('Color/Image 2: '), 1806 'vis3':_('Color/Image 3: '), 'vis4':_('Color/Image 4: '), 1807 'visvis':_('Vis & nvis: '), 'visaudio':_('Vis & n-sound: '), 1808 'audiovis':_('Sound & n-vis: '), 'audio':_('Sound: '), 1809 'color':_('Color: '), 'image':_('Image: '), 1810 'arithmetic':_('Arithmetic: '), 'audio2':_('Sound2: ')} 1811 str_list = [_('Last 50 rounds: ')] 1812 for m in mode.modalities[self.graph]: 1813 str_list.append(labelstrings[m] + '%i%% ' % self.percents[self.graph][m][-1] 1814 + ' ' * (7-len(mode.modalities[self.graph]))) 1815 1816 pyglet.text.Label(''.join(str_list), 1817 batch=self.batch, 1818 font_size=calc_fontsize(11), bold = False, color = cfg.COLOR_TEXT, 1819 x = width_center(), y = scale_to_width(20), 1820 anchor_x = 'center', anchor_y = 'center') 1821 1822class TextInputScreen: 1823 titlesize = calc_fontsize(18) 1824 textsize = calc_fontsize(16) 1825 instance = None 1826 1827 def __init__(self, title='', text='', callback=None, catch=''): 1828 self.titletext = title 1829 self.text = text 1830 self.starttext = text 1831 self.bgcolor = (255 * int(not cfg.BLACK_BACKGROUND), )*3 1832 self.textcolor = (255 * int(cfg.BLACK_BACKGROUND), )*3 + (255, ) 1833 self.batch = pyglet.graphics.Batch() 1834 self.title = pyglet.text.Label(title, font_size=self.titlesize, 1835 bold=True, color=self.textcolor, batch=self.batch, 1836 x=width_center(), y=(window.height*9)/10, 1837 anchor_x='center', anchor_y='center') 1838 self.document = pyglet.text.document.UnformattedDocument() 1839 self.document.set_style(0, len(self.document.text), {'color': self.textcolor}) 1840 self.layout = pyglet.text.layout.IncrementalTextLayout(self.document, 1841 (from_width_center(-20) - len(title) * calc_fontsize(6)), (window.height*10)/11, batch=self.batch, dpi=calc_dpi()) 1842 self.layout.x = from_width_center(15) + len(title) * calc_fontsize(6) 1843 if not callback: callback = lambda x: x 1844 self.callback = callback 1845 self.caret = pyglet.text.caret.Caret(self.layout) 1846 window.push_handlers(self.caret) 1847 window.push_handlers(self.on_key_press, self.on_draw) 1848 self.document.text = text 1849 # workaround for a bug: the keypress that spawns TextInputScreen doesn't 1850 # get handled until after the caret handler has been pushed, which seems 1851 # to result in the keypress being interpreted as a text input, so we 1852 # catch that later 1853 self.catch = catch 1854 self.instance = self 1855 1856 1857 def on_draw(self): 1858 # the bugfix hack, which currently does not work 1859 if self.catch and self.document.text == self.catch + self.starttext: 1860 self.document.text = self.starttext 1861 self.catch = '' 1862 self.caret.select_paragraph(600,0) 1863 1864 window.clear() 1865 self.batch.draw() 1866 return pyglet.event.EVENT_HANDLED 1867 1868 1869 def on_key_press(self, k, mod): 1870 if k in (key.ESCAPE, key.RETURN, key.ENTER): 1871 if k is key.ESCAPE: 1872 self.text = self.starttext 1873 else: 1874 self.text = self.document.text 1875 window.pop_handlers() 1876 window.pop_handlers() 1877 self.callback(self.text.strip()) 1878 return pyglet.event.EVENT_HANDLED 1879 1880 1881class Cycler: 1882 def __init__(self, values, default=0): 1883 self.values = values 1884 if type(default) is not int or default > len(values): 1885 default = values.index(default) 1886 self.i = default 1887 def choose(self, val): 1888 if val in self.values: 1889 self.i = self.values.index(val) 1890 def nxt(self): # not named "next" to prevent using a Cycler as an iterator, which would hang 1891 self.i = (self.i + 1) % len(self.values) 1892 return self.value() 1893 def value(self): 1894 return self.values[self.i] 1895 def __str__(self): 1896 return str(self.value()) 1897 1898class PercentCycler(Cycler): 1899 def __str__(self): 1900 v = self.value() 1901 if type(v) == float and (v < .1 or v > .9) and not v in (0., 1.): 1902 return "%2.2f%%" % (v*100.) 1903 else: 1904 return "%2.1f%%" % (v*100.) 1905 1906class Menu: 1907 """ 1908 Menu.__init__(self, options, values={}, actions={}, names={}, title='', choose_once=False, 1909 default=0): 1910 1911 A generic menu class. The argument options is edited in-place. Instancing 1912 the Menu displays the menu. Menu will install its own event handlers for 1913 on_key_press, on_text, on_text_motion and on_draw, all of which 1914 do not pass events to later handlers on the stack. When the user presses 1915 esc, Menu pops its handlers off the stack. If the argument actions is used, 1916 it should be a dict with keys being options with specific actions, and values 1917 being a python callable which returns the new value for that option. 1918 1919 """ 1920 titlesize = calc_fontsize(18) 1921 choicesize = calc_fontsize(12) 1922 footnotesize = calc_fontsize(12) 1923 fontlist = ['Courier New', # try fixed width fonts first 1924 'Monospace', 'Terminal', 'fixed', 'Fixed', 'Times New Roman', 1925 'Helvetica', 'Arial'] 1926 fontlist_serif = ['Times New Roman', 'Serif', 'Helvetica', 'Arial'] 1927 instance = None 1928 1929 1930 def __init__(self, options, values=None, actions={}, names={}, title='', 1931 footnote = _('Esc: cancel Space: modify option Enter: apply'), 1932 choose_once=False, default=0): 1933 self.bgcolor = (255 * int(not cfg.BLACK_BACKGROUND), )*3 1934 self.textcolor = (255 * int(cfg.BLACK_BACKGROUND), )*3 + (255,) 1935 self.markercolors = (0,0,255,0,255,0,255,0,0)#(255 * int(cfg.BLACK_BACKGROUND), )*3*3 1936 self.pagesize = min(len(options), (window.height*6/10) / (self.choicesize*3/2)) 1937 if type(options) == dict: 1938 vals = options 1939 self.options = list(options) 1940 else: 1941 vals = dict([[op, None] for op in options]) 1942 self.options = options 1943 self.values = values or vals # use values if there's anything in it 1944 self.actions = actions 1945 for op in self.options: 1946 if not op in names.keys(): 1947 names[op] = op 1948 self.names = names 1949 self.choose_once = choose_once 1950 self.disppos = 0 # which item in options is the first on the screen 1951 self.selpos = default # may be offscreen? 1952 self.batch = pyglet.graphics.Batch() 1953 1954 self.title = pyglet.text.Label(title, font_size=self.titlesize, 1955 bold=True, color=self.textcolor, batch=self.batch, 1956 x=width_center(), y=(window.height*9)/10, 1957 anchor_x='center', anchor_y='center') 1958 self.footnote = pyglet.text.Label(footnote, font_size=self.footnotesize, 1959 bold=True, color=self.textcolor, batch=self.batch, 1960 x=width_center(), y=from_bottom_edge(35), 1961 anchor_x='center', anchor_y='center') 1962 1963 self.labels = [pyglet.text.Label('', font_size=self.choicesize, 1964 bold=True, color=self.textcolor, batch=self.batch, 1965 x=window.width/8, y=(window.height*8)/10 - i*(self.choicesize*3/2), 1966 anchor_x='left', anchor_y='center', font_name=self.fontlist) 1967 for i in range(self.pagesize)] 1968 1969 self.marker = self.batch.add(3, GL_POLYGON, None, ('v2i', (0,)*6,), 1970 ('c3B', self.markercolors)) 1971 1972 self.update_labels() 1973 1974 window.push_handlers(self.on_key_press, self.on_text, 1975 self.on_text_motion, self.on_draw) 1976 1977 # keep a reference to the current instance as pyglet>=1.4 Window.push_handlers 1978 # only keep weak references to handlers so Menu subclasses will be deleted 1979 self.instance = self 1980 1981 def textify(self, x): 1982 if type(x) == bool: 1983 return x and _('Yes') or _('No') 1984 return str(x) 1985 1986 def update_labels(self): 1987 for l in self.labels: l.text = 'Hello, bug!' 1988 1989 markerpos = self.selpos - self.disppos 1990 i = 0 1991 di = self.disppos 1992 if not di == 0: # displacement of i 1993 self.labels[i].text = '...' 1994 i += 1 1995 ending = int(di + self.pagesize < len(self.options)) 1996 while i < self.pagesize-ending and i+self.disppos < len(self.options): 1997 k = self.options[i+di] 1998 if k == 'Blank line': 1999 self.labels[i].text = '' 2000 elif k in self.values.keys() and not self.values[k] == None: 2001 v = self.values[k] 2002 self.labels[i].text = '%s:%7s' % (self.names[k].ljust(52), self.textify(v)) 2003 else: 2004 self.labels[i].text = self.names[k] 2005 i += 1 2006 if ending: 2007 self.labels[i].text = '...' 2008 w, h, cs = window.width, window.height, self.choicesize 2009 self.marker.vertices = [w//10, int((h*8)/10 - markerpos*(cs*3/2) + cs/2), 2010 w//9, int((h*8)/10 - markerpos*(cs*3/2)), 2011 w//10, int((h*8)/10 - markerpos*(cs*3/2) - cs/2)] 2012 2013 def move_selection(self, steps, relative=True): 2014 # FIXME: pageup/pagedown can occasionally cause "Hello bug!" to be displayed 2015 if relative: 2016 self.selpos += steps 2017 else: 2018 self.selpos = steps 2019 self.selpos = min(len(self.options)-1, max(0, self.selpos)) 2020 if self.disppos >= self.selpos and not self.disppos == 0: 2021 self.disppos = max(0, self.selpos-1) 2022 if self.disppos <= self.selpos - self.pagesize +1\ 2023 and not self.disppos == len(self.options) - self.pagesize: 2024 self.disppos = max(0, min(len(self.options), self.selpos+1) - self.pagesize + 1) 2025 2026 if not self.selpos in (0, len(self.options)-1) and self.options[self.selpos] == 'Blank line': 2027 self.move_selection(int(steps > 0)*2-1) 2028 self.update_labels() 2029 2030 def on_key_press(self, sym, mod): 2031 if sym == key.ESCAPE: 2032 self.close() 2033 elif sym in (key.RETURN, key.ENTER): 2034 self.save() 2035 self.close() 2036 elif sym == key.SPACE: 2037 self.select() 2038 return pyglet.event.EVENT_HANDLED 2039 2040 def select(self): 2041 k = self.options[self.selpos] 2042 i = self.selpos 2043 if k == "Blank line": 2044 pass 2045 elif k in self.actions.keys(): 2046 self.values[k] = self.actions[k](k) 2047 elif type(self.values[k]) == bool: 2048 self.values[k] = not self.values[k] # todo: other data types 2049 elif isinstance(self.values[k], Cycler): 2050 self.values[k].nxt() 2051 elif self.values[k] == None: 2052 self.choose(k, i) 2053 self.close() 2054 if self.choose_once: 2055 self.close() 2056 self.update_labels() 2057 2058 def choose(self, k, i): # override this method in subclasses 2059 print("Thank you for beta-testing our software.") 2060 2061 def close(self): 2062 return window.remove_handlers(self.on_key_press, self.on_text, 2063 self.on_text_motion, self.on_draw) 2064 2065 def save(self): 2066 "Override me in subclasses." 2067 return 2068 2069 def on_text_motion(self, evt): 2070 if evt == key.MOTION_UP: self.move_selection(steps=-1) 2071 if evt == key.MOTION_DOWN: self.move_selection(steps=1) 2072 if evt == key.MOTION_PREVIOUS_PAGE: self.move_selection(steps=-self.pagesize) 2073 if evt == key.MOTION_NEXT_PAGE: self.move_selection(steps=self.pagesize) 2074 return pyglet.event.EVENT_HANDLED 2075 2076 def on_text(self, evt): 2077 return pyglet.event.EVENT_HANDLED # todo: entering values after select() 2078 2079 def on_draw(self): 2080 window.clear() 2081 self.batch.draw() 2082 return pyglet.event.EVENT_HANDLED 2083 2084class MainMenu(Menu): 2085 def __init__(self): 2086 def NotImplemented(): 2087 raise NotImplementedError 2088 ops = [('game', _('Choose Game Mode'), GameSelect), 2089 ('sounds', _('Choose Sounds'), SoundSelect), 2090 ('images', _('Choose Images'), ImageSelect), 2091 ('user', _('Choose User'), UserScreen), 2092 ('graph', _('Daily Progress Graph'), NotImplemented), 2093 ('help', _('Help / Tutorial'), NotImplemented), 2094 ('donate', _('Donate'), Notimplemented), 2095 ('forum', _('Go to Forum / Mailing List'), NotImplemented)] 2096 options = [ op[0] for op in ops] 2097 names = dict( [ (op[0], op[1]) for op in ops]) 2098 actions = dict( [ (op[0], op[2]) for op in ops]) 2099 2100class UserScreen(Menu): 2101 def __init__(self): 2102 2103 self.users = users = [_("New user"), 'Blank line'] + get_users() 2104 Menu.__init__(self, options=users, 2105 #actions=dict([(user, choose_user) for user in users]), 2106 title=_("Please select your user profile"), 2107 choose_once=True, 2108 default=users.index(USER)) 2109 2110 def save(self): 2111 self.select() # Enter should choose a user too 2112 Menu.save(self) 2113 2114 def choose(self, k, i): 2115 newuser = self.users[i] 2116 if newuser == _("New user"): 2117 # TODO Don't allow the user to create a username that's an empty string 2118 textInput = TextInputScreen(_("Enter new user name:"), USER, callback=set_user, catch=' ') 2119 else: 2120 set_user(newuser) 2121 2122class LanguageScreen(Menu): 2123 def __init__(self): 2124 self.languages = languages = [fn for fn in os.listdir(os.path.join('res', 'i18n')) if fn.lower().endswith('mo')] 2125 try: 2126 default = languages.index(cfg.LANGUAGE + '.mo') 2127 except: 2128 default = 0 2129 Menu.__init__(self, options=languages, 2130 title=_("Please select your preferred language"), 2131 choose_once=True, 2132 default=default) 2133 def save(self): 2134 self.select() 2135 Menu.save(self) 2136 2137 def choose(self, k, i): 2138 newlang = self.languages[i] 2139 # set the new language here 2140 2141class OptionsScreen(Menu): 2142 def __init__(self): 2143 """ 2144 Sorta works. Not yet useful, though. 2145 """ 2146 options = list(cfg) 2147 options.sort() 2148 Menu.__init__(self, options=options, values=cfg, title=_('Configuration')) 2149 2150 2151class GameSelect(Menu): 2152 def __init__(self): 2153 modalities = ['position1', 'color', 'image', 'audio', 'audio2', 'arithmetic'] 2154 options = modalities[:] 2155 names = dict([(m, _("Use %s") % m) for m in modalities]) 2156 names['position1'] = _("Use position") 2157 options.extend(["Blank line", 'combination', "Blank line", 'variable', 2158 'crab', "Blank line", 'multi', 'multimode', 'Blank line', 2159 'selfpaced', "Blank line", 'interference']) 2160 names['combination'] = _('Combination N-back mode') 2161 names['variable'] = _('Use variable N-Back levels') 2162 names['crab'] = _('Crab-back mode (reverse order of sets of N stimuli)') 2163 names['multi'] = _('Simultaneous visual stimuli') 2164 names['multimode'] = _('Simultaneous stimuli differentiated by') 2165 names['selfpaced'] = _('Self-paced mode') 2166 names['interference'] = _('Interference (tricky stimulus generation)') 2167 vals = dict([[op, None] for op in options]) 2168 curmodes = mode.modalities[mode.mode] 2169 interference_options = [i / 8. for i in range(0, 9)] 2170 if not cfg.DEFAULT_CHANCE_OF_INTERFERENCE in interference_options: 2171 interference_options.append(cfg.DEFAULT_CHANCE_OF_INTERFERENCE) 2172 interference_options.sort() 2173 if cfg.CHANCE_OF_INTERFERENCE in interference_options: 2174 interference_default = interference_options.index(cfg.CHANCE_OF_INTERFERENCE) 2175 else: 2176 interference_default = 3 2177 vals['interference'] = PercentCycler(values=interference_options, default=interference_default) 2178 vals['combination'] = 'visvis' in curmodes 2179 vals['variable'] = bool(cfg.VARIABLE_NBACK) 2180 vals['crab'] = bool(mode.flags[mode.mode]['crab']) 2181 vals['multi'] = Cycler(values=[1,2,3,4], default=mode.flags[mode.mode]['multi']-1) 2182 vals['multimode'] = Cycler(values=['color', 'image'], default=cfg.MULTI_MODE) 2183 vals['selfpaced'] = bool(mode.flags[mode.mode]['selfpaced']) 2184 for m in modalities: 2185 vals[m] = m in curmodes 2186 Menu.__init__(self, options, vals, names=names, title=_('Choose your game mode')) 2187 self.modelabel = pyglet.text.Label('', font_size=self.titlesize, 2188 bold=False, color=(0,0,0,255), batch=self.batch, 2189 x=width_center(), y=(window.height*1)/10, 2190 anchor_x='center', anchor_y='center') 2191 self.update_labels() 2192 self.newmode = mode.mode # self.newmode will be False if an invalid mode is chosen 2193 2194 def update_labels(self): 2195 self.calc_mode() 2196 try: 2197 if self.newmode: 2198 self.modelabel.text = mode.long_mode_names[self.newmode] + \ 2199 (self.values['variable'] and ' V.' or '') + ' N-Back' 2200 else: 2201 self.modelabel.text = _("An invalid mode has been selected.") 2202 except AttributeError: 2203 pass 2204 Menu.update_labels(self) 2205 2206 def calc_mode(self): 2207 modes = [k for (k, v) in self.values.items() if v and not isinstance(v, Cycler)] 2208 crab = 'crab' in modes 2209 if 'variable' in modes: modes.remove('variable') 2210 if 'combination' in modes: 2211 modes.remove('combination') 2212 modes.extend(['visvis', 'visaudio', 'audiovis']) # audio should already be there 2213 base = 0 2214 base += 256 * (self.values['multi'].value()-1) 2215 if 'crab' in modes: 2216 modes.remove('crab') 2217 base += 128 2218 if 'selfpaced' in modes: 2219 modes.remove('selfpaced') 2220 base += 1024 2221 2222 candidates = set([k for k,v in mode.modalities.items() if not 2223 [True for m in modes if not m in v] and not 2224 [True for m in v if not m in modes]]) 2225 candidates = candidates & set(range(0, 128)) 2226 if len(candidates) == 1: 2227 candidate = list(candidates)[0] + base 2228 if candidate in mode.modalities: 2229 self.newmode = candidate 2230 else: self.newmode = False 2231 else: 2232 if DEBUG: print(candidates, base) 2233 self.newmode = False 2234 2235 def close(self): 2236 Menu.close(self) 2237 if not mode.manual: 2238 mode.enforce_standard_mode() 2239 stats.retrieve_progress() 2240 update_all_labels() 2241 circles.update() 2242 2243 def save(self): 2244 self.calc_mode() 2245 cfg.VARIABLE_NBACK = self.values['variable'] 2246 cfg.MULTI_MODE = self.values['multimode'].value() 2247 cfg.CHANCE_OF_INTERFERENCE = self.values['interference'].value() 2248 if self.newmode: 2249 mode.mode = self.newmode 2250 2251 2252 def select(self): 2253 choice = self.options[self.selpos] 2254 if choice == 'combination': 2255 self.values['arithmetic'] = False 2256 self.values['image'] = False 2257 self.values['audio2'] = False 2258 self.values['audio'] = True 2259 self.values['multi'].i = 0 # no multi mode 2260 elif choice == 'arithmetic': 2261 self.values['image'] = False 2262 self.values['audio'] = False 2263 self.values['audio2'] = False 2264 self.values['combination'] = False 2265 self.values['multi'].i = 0 2266 elif choice == 'audio': 2267 self.values['arithmetic'] = False 2268 if self.values['audio']: 2269 self.values['combination'] = False 2270 self.values['audio2'] = False 2271 elif choice == 'audio2': 2272 self.values['audio'] = True 2273 self.values['combination'] = False 2274 self.values['arithmetic'] = False 2275 elif choice == 'image': 2276 self.values['combination'] = False 2277 self.values['arithmetic'] = False 2278 if self.values['multi'].value() > 1 and not self.values['image']: 2279 self.values['color'] = False 2280 self.values['multimode'].choose('color') 2281 elif choice == 'color': 2282 if self.values['multi'].value() > 1 and not self.values['color']: 2283 self.values['image'] = False 2284 self.values['multimode'].choose('image') 2285 elif choice == 'multi': 2286 self.values['arithmetic'] = False 2287 self.values['combination'] = False 2288 self.values[self.values['multimode'].value()] = False 2289 elif choice == 'multimode' and self.values['multi'].value() > 1: 2290 mm = self.values['multimode'].value() # what we're changing from 2291 notmm = (mm == 'image') and 'color' or 'image' # changing to 2292 self.values[mm] = self.values[notmm] 2293 self.values[notmm] = False 2294 2295 2296 Menu.select(self) 2297 modes = [k for k,v in self.values.items() if v] 2298 if not [v for k,v in self.values.items() 2299 if v and not k in ('crab', 'combination', 'variable')] \ 2300 or len(modes) == 1 and modes[0] in ['image', 'color']: 2301 self.values['position1'] = True 2302 self.update_labels() 2303 self.calc_mode() 2304 2305class ImageSelect(Menu): 2306 def __init__(self): 2307 imagesets = resourcepaths['sprites'] 2308 self.new_sets = {} 2309 for image in imagesets: 2310 self.new_sets[image] = image in cfg.IMAGE_SETS 2311 options = list(self.new_sets) 2312 options.sort() 2313 vals = self.new_sets 2314 Menu.__init__(self, options, vals, title=_('Choose images to use for the Image n-back tasks.')) 2315 2316 def close(self): 2317 while cfg.IMAGE_SETS: 2318 cfg.IMAGE_SETS.remove(cfg.IMAGE_SETS[0]) 2319 for k,v in self.new_sets.items(): 2320 if v: cfg.IMAGE_SETS.append(k) 2321 Menu.close(self) 2322 update_all_labels() 2323 2324 def select(self): 2325 Menu.select(self) 2326 if not [val for val in self.values.values() if (val and not isinstance(val, Cycler))]: 2327 i = 0 2328 if self.selpos == 0: 2329 i = random.randint(1, len(self.options)-1) 2330 self.values[self.options[i]] = True 2331 self.update_labels() 2332 2333class SoundSelect(Menu): 2334 def __init__(self): 2335 audiosets = resourcepaths['sounds'] # we don't want to delete 'operations' from resourcepaths['sounds'] 2336 self.new_sets = {} 2337 for audio in audiosets: 2338 if not audio == 'operations': 2339 self.new_sets['1'+audio] = audio in cfg.AUDIO1_SETS 2340 self.new_sets['2'+audio] = audio in cfg.AUDIO2_SETS 2341 for audio in audiosets: 2342 if not audio == 'operations': 2343 self.new_sets['2'+audio] = audio in cfg.AUDIO2_SETS 2344 options = list(self.new_sets) 2345 options.sort() 2346 options.insert(len(self.new_sets)//2, "Blank line") # Menu.update_labels and .select will ignore this 2347 options.append("Blank line") 2348 options.extend(['cfg.CHANNEL_AUDIO1', 'cfg.CHANNEL_AUDIO2']) 2349 lcr = ['left', 'right', 'center'] 2350 vals = self.new_sets 2351 vals['cfg.CHANNEL_AUDIO1'] = Cycler(lcr, default=lcr.index(cfg.CHANNEL_AUDIO1)) 2352 vals['cfg.CHANNEL_AUDIO2'] = Cycler(lcr, default=lcr.index(cfg.CHANNEL_AUDIO2)) 2353 names = {} 2354 for op in options: 2355 if op.startswith('1') or op.startswith('2'): 2356 names[op] = _("Use sound set '%s' for channel %s") % (op[1:], op[0]) 2357 elif 'CHANNEL_AUDIO' in op: 2358 names[op] = 'Channel %i is' % (op[-1]=='2' and 2 or 1) 2359 Menu.__init__(self, options, vals, {}, names, title=_('Choose sound sets to Sound n-back tasks.')) 2360 2361 def close(self): 2362 cfg.AUDIO1_SETS = [] 2363 cfg.AUDIO2_SETS = [] 2364 for k,v in self.new_sets.items(): 2365 if k.startswith('1') and v: cfg.AUDIO1_SETS.append(k[1:]) 2366 elif k.startswith('2') and v: cfg.AUDIO2_SETS.append(k[1:]) 2367 cfg.CHANNEL_AUDIO1 = self.values['cfg.CHANNEL_AUDIO1'].value() 2368 cfg.CHANNEL_AUDIO2 = self.values['cfg.CHANNEL_AUDIO2'].value() 2369 Menu.close(self) 2370 update_all_labels() 2371 2372 def select(self): 2373 Menu.select(self) 2374 for c in ('1', '2'): 2375 if not [v for k,v in self.values.items() if (k.startswith(c) and v and not isinstance(v, Cycler))]: 2376 options = list(resourcepaths['sounds']) 2377 options.remove('operations') 2378 i = 0 2379 if self.selpos == 0: 2380 i = random.randint(1, len(options)-1) 2381 elif self.selpos==len(options)+1: 2382 i = random.randint(len(options)+2, 2*len(options)) 2383 elif self.selpos > len(options)+1: 2384 i = len(options)+1 2385 self.values[self.options[i]] = True 2386 self.update_labels() 2387 2388# this class controls the field. 2389# the field is the grid on which the squares appear 2390class Field: 2391 def __init__(self): 2392 if cfg.FIELD_EXPAND: 2393 self.size = int(window.height * 0.85) 2394 else: self.size = int(window.height * 0.625) 2395 if cfg.BLACK_BACKGROUND: 2396 self.color = (64, 64, 64) 2397 else: 2398 self.color = (192, 192, 192) 2399 self.color4 = self.color * 4 2400 self.color8 = self.color * 8 2401 self.center_x = width_center() 2402 if cfg.FIELD_EXPAND: 2403 self.center_y = height_center() 2404 else: 2405 self.center_y = from_height_center(20) 2406 self.x1 = int(self.center_x - self.size/2) 2407 self.x2 = int(self.center_x + self.size/2) 2408 self.x3 = int(self.center_x - self.size/6) 2409 self.x4 = int(self.center_x + self.size/6) 2410 self.y1 = int(self.center_y - self.size/2) 2411 self.y2 = int(self.center_y + self.size/2) 2412 self.y3 = int(self.center_y - self.size/6) 2413 self.y4 = int(self.center_y + self.size/6) 2414 2415 # add the inside lines 2416 if cfg.GRIDLINES: 2417 self.v_lines = batch.add(8, GL_LINES, None, ('v2i', ( 2418 self.x1, self.y3, 2419 self.x2, self.y3, 2420 self.x1, self.y4, 2421 self.x2, self.y4, 2422 self.x3, self.y1, 2423 self.x3, self.y2, 2424 self.x4, self.y1, 2425 self.x4, self.y2)), 2426 ('c3B', self.color8)) 2427 2428 self.crosshair_visible = False 2429 # initialize crosshair 2430 self.crosshair_update() 2431 2432 # draw the target cross in the center 2433 def crosshair_update(self): 2434 if not cfg.CROSSHAIRS: 2435 return 2436 if (not mode.paused) and 'position1' in mode.modalities[mode.mode] and not cfg.VARIABLE_NBACK: 2437 if not self.crosshair_visible: 2438 length_of_crosshair = scale_to_height(8) 2439 self.v_crosshair = batch.add(4, GL_LINES, None, ('v2i', ( 2440 self.center_x - length_of_crosshair, self.center_y, 2441 self.center_x + length_of_crosshair, self.center_y, 2442 self.center_x, self.center_y - length_of_crosshair, 2443 self.center_x, self.center_y + length_of_crosshair)), ('c3B', self.color4)) 2444 self.crosshair_visible = True 2445 else: 2446 if self.crosshair_visible: 2447 self.v_crosshair.delete() 2448 self.crosshair_visible = False 2449 2450 2451# this class controls the visual cues (colored squares). 2452class Visual: 2453 def __init__(self): 2454 self.visible = False 2455 self.label = pyglet.text.Label( 2456 '', 2457 font_size=field.size//6, bold=True, 2458 anchor_x='center', anchor_y='center', batch=batch) 2459 self.variable_label = pyglet.text.Label( 2460 '', 2461 font_size=field.size//6, bold=True, 2462 anchor_x='center', anchor_y='center', batch=batch) 2463 2464 self.spr_square = [pyglet.sprite.Sprite(pyglet.image.load(path)) 2465 for path in resourcepaths['misc']['colored-squares']] 2466 self.spr_square_size = self.spr_square[0].width 2467 2468 if cfg.ANIMATE_SQUARES: 2469 self.size_factor = 0.9375 2470 elif cfg.OLD_STYLE_SQUARES: 2471 self.size_factor = 0.9375 2472 else: 2473 self.size_factor = 1.0 2474 self.size = int(field.size / 3 * self.size_factor) 2475 2476 # load an image set 2477 self.load_set() 2478 2479 def load_set(self, index=None): 2480 if type(index) == int: 2481 index = cfg.IMAGE_SETS[index] 2482 if index == None: 2483 index = random.choice(cfg.IMAGE_SETS) 2484 if hasattr(self, 'image_set_index') and index == self.image_set_index: 2485 return 2486 self.image_set_index = index 2487 self.image_set = [pyglet.sprite.Sprite(pyglet.image.load(path)) 2488 for path in resourcepaths['sprites'][index]] 2489 self.image_set_size = self.image_set[0].width 2490 2491 def choose_random_images(self, number): 2492 self.image_indices = random.sample(range(len(self.image_set)), number) 2493 self.images = random.sample(self.image_set, number) 2494 2495 def choose_indicated_images(self, indices): 2496 self.image_indices = indices 2497 self.images = [self.image_set[i] for i in indices] 2498 2499 def spawn(self, position=0, color=1, vis=0, number=-1, operation='none', variable = 0): 2500 self.position = position 2501 self.color = get_color(color) 2502 self.vis = vis 2503 2504 self.center_x = field.center_x + (field.size // 3)*((position+1)%3 - 1) + (field.size // 3 - self.size)//2 2505 self.center_y = field.center_y + (field.size // 3)*((position//3+1)%3 - 1) + (field.size // 3 - self.size)//2 2506 2507 if self.vis == 0: 2508 if cfg.OLD_STYLE_SQUARES: 2509 lx = self.center_x - self.size // 2 + 2 2510 rx = self.center_x + self.size // 2 - 2 2511 by = self.center_y - self.size // 2 + 2 2512 ty = self.center_y + self.size // 2 - 2 2513 cr = self.size // 5 2514 2515 if cfg.OLD_STYLE_SHARP_CORNERS: 2516 self.square = batch.add(4, GL_POLYGON, None, ('v2i', ( 2517 lx, by, 2518 rx, by, 2519 rx, ty, 2520 lx, ty,)), 2521 ('c4B', self.color * 4)) 2522 else: 2523 #rounded corners: bottom-left, bottom-right, top-right, top-left 2524 x = ([lx + int(cr*(1-math.cos(math.radians(i)))) for i in range(0, 91, 10)] + 2525 [rx - int(cr*(1-math.sin(math.radians(i)))) for i in range(0, 91, 10)] + 2526 [rx - int(cr*(1-math.sin(math.radians(i)))) for i in range(90, -1, -10)] + 2527 [lx + int(cr*(1-math.cos(math.radians(i)))) for i in range(90, -1, -10)]) 2528 2529 y = ([by + int(cr*(1-math.sin(math.radians(i)))) for i in range(0, 91, 10) + range(90, -1, -10)] + 2530 [ty - int(cr*(1-math.sin(math.radians(i)))) for i in range(0, 91, 10) + range(90, -1, -10)]) 2531 xy = [] 2532 for a,b in zip(x,y): xy.extend((a, b)) 2533 2534 self.square = batch.add(40, GL_POLYGON, None, 2535 ('v2i', xy), ('c4B', self.color * 40)) 2536 2537 else: 2538 # use sprite squares 2539 self.square = self.spr_square[color-1] 2540 self.square.opacity = 255 2541 self.square.x = self.center_x - field.size // 6 2542 self.square.y = self.center_y - field.size // 6 2543 self.square.scale = 1.0 * self.size / self.spr_square_size 2544 self.square_size_scaled = self.square.width 2545 self.square.batch = batch 2546 2547 # initiate square animation 2548 self.age = 0.0 2549 pyglet.clock.schedule_interval(self.animate_square, 1/60.) 2550 2551 elif 'arithmetic' in mode.modalities[mode.mode]: # display a number 2552 self.label.text = str(number) 2553 self.label.x = self.center_x 2554 self.label.y = self.center_y + 4 2555 self.label.color = self.color 2556 elif 'visvis' in mode.modalities[mode.mode]: # display a letter 2557 self.label.text = self.letters[vis - 1].upper() 2558 self.label.x = self.center_x 2559 self.label.y = self.center_y + 4 2560 self.label.color = self.color 2561 elif 'image' in mode.modalities[mode.mode] \ 2562 or 'vis1' in mode.modalities[mode.mode] \ 2563 or (mode.flags[mode.mode]['multi'] > 1 and cfg.MULTI_MODE == 'image'): # display a pictogram 2564 self.square = self.images[vis-1] 2565 self.square.opacity = 255 2566 self.square.color = self.color[:3] 2567 self.square.x = self.center_x - field.size // 6 2568 self.square.y = self.center_y - field.size // 6 2569 self.square.scale = 1.0 * self.size / self.image_set_size 2570 self.square_size_scaled = self.square.width 2571 self.square.batch = batch 2572 2573 # initiate square animation 2574 self.age = 0.0 2575 #self.animate_square(0) 2576 pyglet.clock.schedule_interval(self.animate_square, 1/60.) 2577 2578 if variable > 0: 2579 # display variable n-back level 2580 self.variable_label.text = str(variable) 2581 2582 if not 'position1' in mode.modalities[mode.mode]: 2583 self.variable_label.x = field.center_x 2584 self.variable_label.y = field.center_y - field.size//3 + 4 2585 else: 2586 self.variable_label.x = field.center_x 2587 self.variable_label.y = field.center_y + 4 2588 2589 self.variable_label.color = self.color 2590 2591 self.visible = True 2592 2593 def animate_square(self, dt): 2594 self.age += dt 2595 if mode.paused: return 2596 if not cfg.ANIMATE_SQUARES: return 2597 2598 # factors which affect animation 2599 scale_addition = dt / 8 2600 fade_begin_time = 0.4 2601 fade_end_time = 0.5 2602 fade_end_transparency = 1.0 # 1 = fully transparent, 0.5 = half transparent 2603 2604 self.square.scale += scale_addition 2605 dx = (self.square.width - self.square_size_scaled) // 2 2606 self.square.x = self.center_x - field.size // 6 - dx 2607 self.square.y = self.center_y - field.size // 6 - dx 2608 2609 if self.age > fade_begin_time: 2610 factor = (1.0 - fade_end_transparency * (self.age - fade_begin_time) / (fade_end_time - fade_begin_time)) 2611 if factor > 1.0: factor = 1.0 2612 if factor < 0.0: factor = 0.0 2613 self.square.opacity = int(255 * factor) 2614 2615 def hide(self): 2616 if self.visible: 2617 self.label.text = '' 2618 self.variable_label.text = '' 2619 if 'image' in mode.modalities[mode.mode] \ 2620 or 'vis1' in mode.modalities[mode.mode] \ 2621 or (mode.flags[mode.mode]['multi'] > 1 and cfg.MULTI_MODE == 'image'): # hide pictogram 2622 self.square.batch = None 2623 pyglet.clock.unschedule(self.animate_square) 2624 elif self.vis == 0: 2625 if cfg.OLD_STYLE_SQUARES: 2626 self.square.delete() 2627 else: 2628 self.square.batch = None 2629 pyglet.clock.unschedule(self.animate_square) 2630 self.visible = False 2631 2632# Circles is the 3-strikes indicator in the top left corner of the screen. 2633class Circles: 2634 def __init__(self): 2635 self.y = from_top_edge(20) 2636 self.start_x = from_left_edge(30) 2637 self.radius = scale_to_width(8) 2638 self.distance = scale_to_width(20) 2639 if cfg.BLACK_BACKGROUND: 2640 self.not_activated = [64, 64, 64, 255] 2641 else: 2642 self.not_activated = [192, 192, 192, 255] 2643 self.activated = [64, 64, 255, 255] 2644 if cfg.BLACK_BACKGROUND: 2645 self.invisible = [0, 0, 0, 0] 2646 else: 2647 self.invisible = [255, 255, 255, 0] 2648 2649 self.circle = [] 2650 for index in range(0, cfg.THRESHOLD_FALLBACK_SESSIONS - 1): 2651 self.circle.append(batch.add(4, GL_QUADS, None, ('v2i', ( 2652 self.start_x + self.distance * index - self.radius, 2653 self.y + self.radius, 2654 self.start_x + self.distance * index + self.radius, 2655 self.y + self.radius, 2656 self.start_x + self.distance * index + self.radius, 2657 self.y - self.radius, 2658 self.start_x + self.distance * index - self.radius, 2659 self.y - self.radius)), 2660 ('c4B', self.not_activated * 4))) 2661 2662 self.update() 2663 2664 def update(self): 2665 if mode.manual or mode.started or cfg.JAEGGI_MODE: 2666 for i in range(0, cfg.THRESHOLD_FALLBACK_SESSIONS - 1): 2667 self.circle[i].colors = (self.invisible * 4) 2668 else: 2669 for i in range(0, cfg.THRESHOLD_FALLBACK_SESSIONS - 1): 2670 self.circle[i].colors = (self.not_activated * 4) 2671 for i in range(0, mode.progress): 2672 self.circle[i].colors = (self.activated * 4) 2673 2674 2675# this is the update notification 2676class UpdateLabel: 2677 def __init__(self): 2678 # Some versions don't accept the align argument and some don't accept halign. 2679 # So try with one and if that fails use the other. 2680 try: 2681 self.label = pyglet.text.Label( 2682 '', 2683 multiline = True, width = field.size//3 - 4, align='middle', 2684 font_size=calc_fontsize(11), bold=True, 2685 color=(0, 128, 0, 255), 2686 x=width_center(), y=field.center_x + field.size // 6, 2687 anchor_x='center', anchor_y='center', batch=batch) 2688 except: 2689 self.label = pyglet.text.Label( 2690 '', 2691 multiline = True, width = field.size//3 - 4, halign='middle', 2692 font_size=calc_fontsize(11), bold=True, 2693 color=(0, 128, 0, 255), 2694 x=width_center(), y=field.center_x + field.size // 6, 2695 anchor_x='center', anchor_y='center', batch=batch) 2696 self.update() 2697 def update(self): 2698 if not mode.started and update_available: 2699 str_list = [] 2700 str_list.append(_('An update is available (')) 2701 str_list.append(str(update_version)) 2702 str_list.append(_('). Press W to open web site')) 2703 self.label.text = ''.join(str_list) 2704 else: self.label.text = '' 2705 2706# this is the black text above the field 2707class GameModeLabel: 2708 def __init__(self): 2709 self.label = pyglet.text.Label( 2710 '', 2711 font_size=calc_fontsize(16), 2712 color=cfg.COLOR_TEXT, 2713 x=width_center(), y=from_top_edge(20), 2714 anchor_x='center', anchor_y='center', batch=batch) 2715 self.update() 2716 def update(self): 2717 if mode.started and mode.hide_text: 2718 self.label.text = '' 2719 else: 2720 str_list = [] 2721 if cfg.JAEGGI_MODE and not CLINICAL_MODE: 2722 str_list.append(_('Jaeggi mode: ')) 2723 if mode.manual: 2724 str_list.append(_('Manual mode: ')) 2725 str_list.append(mode.long_mode_names[mode.mode] + ' ') 2726 if cfg.VARIABLE_NBACK: 2727 str_list.append(_('V. ')) 2728 str_list.append(str(mode.back)) 2729 str_list.append(_('-Back')) 2730 self.label.text = ''.join(str_list) 2731 2732 def flash(self): 2733 pyglet.clock.unschedule(gameModeLabel.unflash) 2734 self.label.color = (255,0 , 255, 255) 2735 self.update() 2736 pyglet.clock.schedule_once(gameModeLabel.unflash, 0.5) 2737 def unflash(self, dt): 2738 self.label.color = cfg.COLOR_TEXT 2739 self.update() 2740 2741class JaeggiWarningLabel: 2742 def __init__(self): 2743 self.label = pyglet.text.Label( 2744 '', 2745 font_size=calc_fontsize(12), bold = True, 2746 color=(255, 0, 255, 255), 2747 x=width_center(), y=field.center_x + field.size // 3 + 8, 2748 anchor_x='center', anchor_y='center', batch=batch) 2749 2750 def show(self): 2751 pyglet.clock.unschedule(jaeggiWarningLabel.hide) 2752 self.label.text = _('Please disable Jaeggi Mode to access additional modes.') 2753 pyglet.clock.schedule_once(jaeggiWarningLabel.hide, 3.0) 2754 def hide(self, dt): 2755 self.label.text = '' 2756 2757# this is the keyboard reference list along the left side 2758class KeysListLabel: 2759 def __init__(self): 2760 self.label = pyglet.text.Label( 2761 '', 2762 multiline = True, width = scale_to_width(300), bold = False, 2763 font_size=calc_fontsize(9), 2764 color=cfg.COLOR_TEXT, 2765 x = scale_to_width(10), y = from_top_edge(30), 2766 anchor_x='left', anchor_y='top', batch=batch) 2767 self.update() 2768 def update(self): 2769 str_list = [] 2770 if mode.started: 2771 self.label.y = from_top_edge(30) 2772 if not mode.hide_text: 2773 str_list.append(_('P: Pause / Unpause\n')) 2774 str_list.append('\n') 2775 str_list.append(_('F8: Hide / Reveal Text\n')) 2776 str_list.append('\n') 2777 str_list.append(_('ESC: Cancel Session\n')) 2778 elif CLINICAL_MODE: 2779 self.label.y = from_top_edge(30) 2780 str_list.append(_('ESC: Exit')) 2781 else: 2782 if mode.manual or cfg.JAEGGI_MODE: 2783 self.label.y = from_top_edge(30) 2784 else: 2785 self.label.y = from_top_edge(40) 2786 if 'morse' in cfg.AUDIO1_SETS or 'morse' in cfg.AUDIO2_SETS: 2787 str_list.append(_('J: Morse Code Reference\n')) 2788 str_list.append('\n') 2789 str_list.append(_('H: Help / Tutorial\n')) 2790 str_list.append('\n') 2791 if mode.manual: 2792 str_list.extend([ 2793 _('F1: Decrease N-Back\n'), 2794 _('F2: Increase N-Back\n'), '\n', 2795 _('F3: Decrease Trials\n'), 2796 _('F4: Increase Trials\n'), '\n']) 2797 if mode.manual: 2798 str_list.extend([ 2799 _('F5: Decrease Speed\n'), 2800 _('F6: Increase Speed\n'), '\n', 2801 _('C: Choose Game Type\n'), 2802 _('S: Select Sounds\n')]) 2803 str_list.append(_('I: Select Images\n')) 2804 if mode.manual: 2805 str_list.append(_('M: Standard Mode\n')) 2806 else: 2807 str_list.extend([ 2808 _('M: Manual Mode\n'), 2809 _('D: Donate\n'), '\n', 2810 _('G: Daily Progress Graph\n'), '\n', 2811 _('W: Brain Workshop Web Site\n')]) 2812 if cfg.WINDOW_FULLSCREEN: 2813 str_list.append(_('E: Saccadic Eye Exercise\n')) 2814 str_list.extend(['\n', _('ESC: Exit\n')]) 2815 2816 self.label.text = ''.join(str_list) 2817 2818class TitleMessageLabel: 2819 def __init__(self): 2820 self.label = pyglet.text.Label( 2821 _('Brain Workshop'), 2822 #multiline = True, width = window.width // 2, 2823 font_size=calc_fontsize(32), bold = True, color = cfg.COLOR_TEXT, 2824 x = width_center(), y = from_top_edge(25), 2825 anchor_x = 'center', anchor_y = 'center') 2826 self.label2 = pyglet.text.Label( 2827 _('Version ') + str(VERSION), 2828 font_size=calc_fontsize(14), bold = False, color = cfg.COLOR_TEXT, 2829 x = width_center(), y = from_top_edge(55), 2830 anchor_x = 'center', anchor_y = 'center') 2831 2832 def draw(self): 2833 self.label.draw() 2834 self.label2.draw() 2835 2836class TitleKeysLabel: 2837 def __init__(self): 2838 str_list = [] 2839 if not (cfg.JAEGGI_MODE or CLINICAL_MODE): 2840 str_list.append(_('C: Choose Game Mode\n')) 2841 str_list.append(_('S: Choose Sounds\n')) 2842 str_list.append(_('I: Choose Images\n')) 2843 if not CLINICAL_MODE: 2844 str_list.append(_('U: Choose User\n')) 2845 str_list.append(_('G: Daily Progress Graph\n')) 2846 str_list.append(_('H: Help / Tutorial\n')) 2847 if not CLINICAL_MODE: 2848 str_list.append(_('D: Donate\n')) 2849 str_list.append(_('F: Go to Forum / Mailing List\n')) 2850 str_list.append(_('O: Edit configuration file')) 2851 2852 self.keys = pyglet.text.Label( 2853 ''.join(str_list), 2854 multiline = True, width = scale_to_width(260), 2855 font_size=calc_fontsize(12), bold = True, color = cfg.COLOR_TEXT, 2856 x = from_width_center(65), y = from_bottom_edge(230), 2857 anchor_x = 'center', anchor_y = 'top') 2858 2859 self.space = pyglet.text.Label( 2860 _('Press SPACE to enter the Workshop'), 2861 font_size=calc_fontsize(20), bold = True, color = (32, 32, 255, 255), 2862 x = width_center(), y = from_bottom_edge(35), 2863 anchor_x = 'center', anchor_y = 'center') 2864 def draw(self): 2865 self.space.draw() 2866 self.keys.draw() 2867 2868 2869# this is the word "brain" above the brain logo. 2870class LogoUpperLabel: 2871 def __init__(self): 2872 self.label = pyglet.text.Label( 2873 'Brain', # I think we shouldn't translate the program name. Yes? 2874 font_size=calc_fontsize(11), bold = True, 2875 color=cfg.COLOR_TEXT, 2876 x=field.center_x, y=field.center_y + scale_to_height(30), 2877 anchor_x='center', anchor_y='center') 2878 def draw(self): 2879 self.label.draw() 2880 2881# this is the word "workshop" below the brain logo. 2882class LogoLowerLabel: 2883 def __init__(self): 2884 self.label = pyglet.text.Label( 2885 'Workshop', 2886 font_size=calc_fontsize(11), bold = True, 2887 color=cfg.COLOR_TEXT, 2888 x=field.center_x, y=field.center_y - scale_to_height(27), 2889 anchor_x='center', anchor_y='center') 2890 def draw(self): 2891 self.label.draw() 2892 2893# this is the word "Paused" which appears when the game is paused. 2894class PausedLabel: 2895 def __init__(self): 2896 self.label = pyglet.text.Label( 2897 '', 2898 font_size=calc_fontsize(14), 2899 color=(64, 64, 255, 255), 2900 x=field.center_x, y=field.center_y, 2901 anchor_x='center', anchor_y='center', batch=batch) 2902 self.update() 2903 def update(self): 2904 if mode.paused: 2905 self.label.text = 'Paused' 2906 else: 2907 self.label.text = '' 2908 2909# this is the congratulations message which appears when advancing N-back levels. 2910class CongratsLabel: 2911 def __init__(self): 2912 self.label = pyglet.text.Label( 2913 '', 2914 font_size=calc_fontsize(14), 2915 color=(255, 32, 32, 255), 2916 x=field.center_x, y=from_top_edge(47), 2917 anchor_x='center', anchor_y='center', batch=batch) 2918 self.update() 2919 def update(self, show=False, advance=False, fallback=False, awesome=False, great=False, good=False, perfect = False): 2920 str_list = [] 2921 if show and not CLINICAL_MODE and cfg.USE_SESSION_FEEDBACK: 2922 if perfect: str_list.append(_('Perfect score! ')) 2923 elif awesome: str_list.append(_('Awesome score! ')) 2924 elif great: str_list.append(_('Great score! ')) 2925 elif good: str_list.append(_('Not bad! ')) 2926 else: str_list.append(_('Keep trying. You\'re getting there! ')) 2927 if advance: 2928 str_list.append(_('N-Back increased')) 2929 elif fallback: 2930 str_list.append(_('N-Back decreased')) 2931 self.label.text = ''.join(str_list) 2932 2933class FeedbackLabel: 2934 def __init__(self, modality, pos=0, total=1): 2935 """ 2936 Generic text label for giving user feedback during N-back sessions. All 2937 of the feedback labels should be instances of this class. 2938 2939 pos should be which label number this one is displayed as (order: left-to-right). 2940 total should be the total number of feedback labels for this mode. 2941 """ 2942 self.modality = modality 2943 self.letter = key.symbol_string(cfg['KEY_%s' % modality.upper()]) 2944 if self.letter == 'SEMICOLON': 2945 self.letter = ';' 2946 modalityname = modality 2947 if modalityname.endswith('vis'): 2948 modalityname = modalityname[:-3] + ' & n-vis' 2949 elif modalityname.endswith('audio') and not modalityname == 'audio': 2950 modalityname = modalityname[:-5] + ' & n-audio' 2951 if mode.flags[mode.mode]['multi'] == 1 and modalityname == 'position1': 2952 modalityname = 'position' 2953 2954 if total == 2 and not cfg.JAEGGI_MODE and cfg.ENABLE_MOUSE: 2955 if pos == 0: 2956 self.mousetext = "Left-click or" 2957 if pos == 1: 2958 self.mousetext = "Right-click or" 2959 else: 2960 self.mousetext = "" 2961 2962 self.text = "%s %s: %s" % (_(self.mousetext), self.letter, _(modalityname)) # FIXME: will this break pyglettext? 2963 2964 if total < 4: 2965 self.text += _(' match') 2966 font_size=calc_fontsize(16) 2967 elif total < 5: font_size=calc_fontsize(14) 2968 elif total < 6: font_size=calc_fontsize(13) 2969 else: font_size=calc_fontsize(11) 2970 2971 self.label = pyglet.text.Label( 2972 text=self.text, 2973 x=-200, y=from_bottom_edge(30), # we'll fix the x position later, after we see how big the label is 2974 anchor_x='left', anchor_y='center', batch=batch, font_size=font_size) 2975 #w = self.label.width # this doesn't work; how are you supposed to find the width of a label texture? 2976 w = (len(self.text) * font_size*4)/5 2977 dis = (window.width-100) / float(total-.99) 2978 x = 30 + int( pos*dis - w*pos/(total-.5) ) 2979 2980 # draw an icon next to the label for multi-stim mode 2981 if mode.flags[mode.mode]['multi'] > 1 and self.modality[-1].isdigit(): 2982 self.id = int(modality[-1]) 2983 if cfg.MULTI_MODE == 'color': 2984 self.icon = pyglet.sprite.Sprite(visuals[self.id-1].spr_square[cfg.VISUAL_COLORS[self.id-1]-1].image) 2985 self.icon.scale = .125 * visuals[self.id-1].size / visuals[self.id-1].image_set_size 2986 self.icon.y = from_bottom_edge(22) 2987 self.icon.x = x - 15 2988 x += 15 2989 2990 else: # 'image' 2991 self.icon = pyglet.sprite.Sprite(visuals[self.id-1].images[self.id-1].image) 2992 self.icon.color = get_color(1)[:3] 2993 self.icon.scale = .25 * visuals[self.id-1].size / visuals[self.id-1].image_set_size 2994 self.icon.y = from_bottom_edge(15) 2995 self.icon.x = x - 25 2996 x += 25 2997 2998 self.icon.opacity = 255 2999 self.icon.batch = batch 3000 3001 self.label.x = x 3002 3003 self.update() 3004 3005 def draw(self): 3006 pass # don't draw twice; this was just for debugging 3007 #self.label.draw() 3008 3009 def update(self): 3010 if mode.started and not mode.hide_text and self.modality in mode.modalities[mode.mode]: # still necessary? 3011 self.label.text = self.text 3012 else: 3013 self.label.text = '' 3014 if cfg.SHOW_FEEDBACK and mode.inputs[self.modality]: 3015 result = check_match(self.modality) 3016 #self.label.bold = True 3017 if result == 'correct': 3018 self.label.color = cfg.COLOR_LABEL_CORRECT 3019 elif result == 'unknown': 3020 self.label.color = cfg.COLOR_LABEL_OOPS 3021 elif result == 'incorrect': 3022 self.label.color = cfg.COLOR_LABEL_INCORRECT 3023 elif cfg.SHOW_FEEDBACK and (not mode.inputs['audiovis']) and mode.show_missed: 3024 result = check_match(self.modality, check_missed=True) 3025 if result == 'missed': 3026 self.label.color = cfg.COLOR_LABEL_OOPS 3027 #self.label.bold = True 3028 else: 3029 self.label.color = cfg.COLOR_TEXT 3030 self.label.bold = False 3031 3032 def delete(self): 3033 self.label.delete() 3034 if mode.flags[mode.mode]['multi'] > 1 and self.modality[-1].isdigit(): 3035 self.icon.batch = None 3036 3037 3038def generate_input_labels(): 3039 labels = [] 3040 modalities = mode.modalities[mode.mode] 3041 pos = 0 3042 total = len(modalities) 3043 for m in modalities: 3044 if m != 'arithmetic': 3045 3046 labels.append(FeedbackLabel(m, pos, total)) 3047 pos += 1 3048 return labels 3049 3050class ArithmeticAnswerLabel: 3051 def __init__(self): 3052 self.answer = [] 3053 self.negative = False 3054 self.decimal = False 3055 self.label = pyglet.text.Label( 3056 '', 3057 x=window.width/2 - 40, y=from_bottom_edge(30), 3058 anchor_x='left', anchor_y='center', batch=batch) 3059 self.update() 3060 def update(self): 3061 if not 'arithmetic' in mode.modalities[mode.mode] or not mode.started: 3062 self.label.text = '' 3063 return 3064 if mode.started and mode.hide_text: 3065 self.label.text = '' 3066 return 3067 3068 self.label.font_size=calc_fontsize(16) 3069 str_list = [] 3070 str_list.append(_('Answer: ')) 3071 str_list.append(str(self.parse_answer())) 3072 self.label.text = ''.join(str_list) 3073 3074 if cfg.SHOW_FEEDBACK and mode.show_missed: 3075 result = check_match('arithmetic') 3076 if result == _('correct'): 3077 self.label.color = cfg.COLOR_LABEL_CORRECT 3078 self.label.bold = True 3079 if result == _('incorrect'): 3080 self.label.color = cfg.COLOR_LABEL_INCORRECT 3081 self.label.bold = True 3082 else: 3083 self.label.color = cfg.COLOR_TEXT 3084 self.label.bold = False 3085 3086 def parse_answer(self): 3087 chars = ''.join(self.answer) 3088 if chars == '' or chars == '.': 3089 result = Decimal('0') 3090 else: 3091 result = Decimal(chars) 3092 if self.negative: 3093 result = Decimal('0') - result 3094 return result 3095 3096 def input(self, input): 3097 if input == '-': 3098 if self.negative: 3099 self.negative = False 3100 else: self.negative = True 3101 elif input == '.': 3102 if not self.decimal: 3103 self.decimal = True 3104 self.answer.append(input) 3105 else: 3106 self.answer.append(input) 3107 self.update() 3108 3109 def reset_input(self): 3110 self.answer = [] 3111 self.negative = False 3112 self.decimal = False 3113 self.update() 3114 3115 3116# this is the text that shows the seconds per trial and the number of trials. 3117class SessionInfoLabel: 3118 def __init__(self): 3119 self.label = pyglet.text.Label( 3120 '', 3121 multiline = True, width = scale_to_width(128), 3122 font_size=calc_fontsize(11), 3123 color=cfg.COLOR_TEXT, 3124 x=from_left_edge(20), y=from_bottom_edge(145), 3125 anchor_x='left', anchor_y='top', batch=batch) 3126 self.update() 3127 def update(self): 3128 if mode.started or CLINICAL_MODE: 3129 self.label.text = '' 3130 else: 3131 self.label.text = _('Session:\n%1.2f sec/trial\n%i+%i trials\n%i seconds') % \ 3132 (mode.ticks_per_trial / 10.0, mode.num_trials, \ 3133 mode.num_trials_total - mode.num_trials, 3134 int((mode.ticks_per_trial / 10.0) * \ 3135 (mode.num_trials_total))) 3136 def flash(self): 3137 pyglet.clock.unschedule(sessionInfoLabel.unflash) 3138 self.label.bold = True 3139 self.update() 3140 pyglet.clock.schedule_once(sessionInfoLabel.unflash, 1.0) 3141 def unflash(self, dt): 3142 self.label.bold = False 3143 self.update() 3144# this is the text that shows the seconds per trial and the number of trials. 3145 3146class ThresholdLabel: 3147 def __init__(self): 3148 self.label = pyglet.text.Label( 3149 '', 3150 multiline = True, width = scale_to_width(128), 3151 font_size=calc_fontsize(11), 3152 color=cfg.COLOR_TEXT, 3153 x=from_right_edge(20), y=from_bottom_edge(145), 3154 anchor_x='right', anchor_y='top', batch=batch) 3155 self.update() 3156 def update(self): 3157 if mode.started or mode.manual or CLINICAL_MODE: 3158 self.label.text = '' 3159 else: 3160 self.label.text = _(u'Thresholds:\nRaise level: \u2265 %i%%\nLower level: < %i%%') % \ 3161 (get_threshold_advance(), get_threshold_fallback()) # '\u2265' = '>=' 3162 3163# this controls the "press space to begin session #" text. 3164class SpaceLabel: 3165 def __init__(self): 3166 self.label = pyglet.text.Label( 3167 '', 3168 font_size=calc_fontsize(16), 3169 bold=True, 3170 color=(32, 32, 255, 255), 3171 x=width_center(), y=from_bottom_edge(62), 3172 anchor_x='center', anchor_y='center', batch=batch) 3173 self.update() 3174 def update(self): 3175 if mode.started: 3176 self.label.text = '' 3177 else: 3178 str_list = [] 3179 str_list.append(_('Press SPACE to begin session #')) 3180 str_list.append(str(mode.session_number + 1)) 3181 str_list.append(': ') 3182 str_list.append(mode.long_mode_names[mode.mode] + ' ') 3183 3184 if cfg.VARIABLE_NBACK: 3185 str_list.append(_('V. ')) 3186 str_list.append(str(mode.back)) 3187 str_list.append(_('-Back')) 3188 self.label.text = ''.join(str_list) 3189 3190def check_match(input_type, check_missed = False): 3191 current = 0 3192 back_data = '' 3193 operation = 0 3194 # FIXME: I'm not going to think about whether crab_back will work with 3195 # cfg.VARIABLE_NBACK yet, since I don't actually understand how the latter works 3196 3197 if mode.flags[mode.mode]['crab'] == 1: 3198 back = 1 + 2*((mode.trial_number-1) % mode.back) 3199 else: 3200 back = mode.back 3201 3202 if cfg.VARIABLE_NBACK: 3203 nback_trial = mode.trial_number - mode.variable_list[mode.trial_number - back - 1] - 1 3204 else: 3205 nback_trial = mode.trial_number - back - 1 3206 3207 if len(stats.session['position1']) < mode.back: 3208 return 'unknown' 3209 3210 if input_type in ('visvis', 'visaudio', 'image'): 3211 current = mode.current_stim['vis'] 3212 elif input_type in ('audiovis', ): 3213 current = mode.current_stim['audio'] 3214 if input_type in ('visvis', 'audiovis', 'image'): 3215 back_data = 'vis' 3216 elif input_type in ('visaudio', ): 3217 back_data = 'audio' 3218 elif input_type == 'arithmetic': 3219 current = mode.current_stim['number'] 3220 back_data = stats.session['numbers'][nback_trial] 3221 operation = mode.current_operation 3222 else: 3223 current = mode.current_stim[input_type] 3224 back_data = input_type 3225 3226 if input_type == 'arithmetic': 3227 if operation == 'add': 3228 correct_answer = back_data + current 3229 elif operation == 'subtract': 3230 correct_answer = back_data - current 3231 elif operation == 'multiply': 3232 correct_answer = back_data * current 3233 elif operation == 'divide': 3234 correct_answer = Decimal(back_data) / Decimal(current) 3235 if correct_answer == arithmeticAnswerLabel.parse_answer(): 3236 return 'correct' 3237 else: 3238 # Catch accesses past list end 3239 try: 3240 if current == stats.session[back_data][nback_trial]: 3241 if check_missed: 3242 return 'missed' 3243 else: 3244 return 'correct' 3245 except Exception as e: 3246 print(e) 3247 return 'incorrect' 3248 return 'incorrect' 3249 3250 3251# this controls the statistics which display upon completion of a session. 3252class AnalysisLabel: 3253 def __init__(self): 3254 self.label = pyglet.text.Label( 3255 '', 3256 font_size=calc_fontsize(14), 3257 color=cfg.COLOR_TEXT, 3258 x=width_center(), y=from_bottom_edge(92), 3259 anchor_x='center', anchor_y='center', batch=batch) 3260 self.update() 3261 3262 def update(self, skip=False): 3263 if mode.started or mode.session_number == 0 or skip: 3264 self.label.text = '' 3265 return 3266 3267 poss_mods = ['position1', 'position2', 'position3', 'position4', 3268 'vis1', 'vis2', 'vis3', 'vis4', 'color', 'visvis', 3269 'visaudio', 'audiovis', 'image', 'audio', 3270 'audio2', 'arithmetic'] # arithmetic must be last so it's easy to exclude 3271 3272 rights = dict([(mod, 0) for mod in poss_mods]) 3273 wrongs = dict([(mod, 0) for mod in poss_mods]) 3274 category_percents = dict([(mod, 0) for mod in poss_mods]) 3275 3276 mods = mode.modalities[mode.mode] 3277 data = stats.session 3278 3279 for mod in mods: 3280 for x in range(mode.back, len(data['position1'])): 3281 3282 if mode.flags[mode.mode]['crab'] == 1: 3283 back = 1 + 2*(x % mode.back) 3284 else: 3285 back = mode.back 3286 if cfg.VARIABLE_NBACK: 3287 back = mode.variable_list[x - back] 3288 3289 # data is a dictionary of lists. 3290 if mod in ['position1', 'position2', 'position3', 'position4', 3291 'vis1', 'vis2', 'vis3', 'vis4', 'audio', 'audio2', 'color', 'image']: 3292 rights[mod] += int((data[mod][x] == data[mod][x-back]) and data[mod+'_input'][x]) 3293 wrongs[mod] += int((data[mod][x] == data[mod][x-back]) ^ data[mod+'_input'][x]) # ^ is XOR 3294 if cfg.JAEGGI_SCORING: 3295 rights[mod] += int(data[mod][x] != data[mod][x-back] and not data[mod+'_input'][x]) 3296 3297 if mod in ['visvis', 'visaudio', 'audiovis']: 3298 modnow = mod.startswith('vis') and 'vis' or 'audio' # these are the python<2.5 compatible versions 3299 modthn = mod.endswith('vis') and 'vis' or 'audio' # of 'vis' if mod.startswith('vis') else 'audio' 3300 rights[mod] += int((data[modnow][x] == data[modthn][x-back]) and data[mod+'_input'][x]) 3301 wrongs[mod] += int((data[modnow][x] == data[modthn][x-back]) ^ data[mod+'_input'][x]) 3302 if cfg.JAEGGI_SCORING: 3303 rights[mod] += int(data[modnow][x] != data[modthn][x-back] and not data[mod+'_input'][x]) 3304 3305 if mod in ['arithmetic']: 3306 ops = {'add':'+', 'subtract':'-', 'multiply':'*', 'divide':'/'} 3307 answer = eval("Decimal(data['numbers'][x-back]) %s Decimal(data['numbers'][x])" % ops[data['operation'][x]]) 3308 rights[mod] += int(answer == Decimal(data[mod+'_input'][x])) # data[...][x] is only Decimal if op == / 3309 wrongs[mod] += int(answer != Decimal(data[mod+'_input'][x])) 3310 3311 str_list = [] 3312 if not CLINICAL_MODE: 3313 str_list += [_('Correct-Errors: ')] 3314 sep = ' ' 3315 keys = dict([(mod, cfg['KEY_%s' % mod.upper()]) for mod in poss_mods[:-1]]) # exclude 'arithmetic' 3316 3317 for mod in poss_mods[:-1]: # exclude 'arithmetic' 3318 if mod in mods: 3319 keytext = key.symbol_string(keys[mod]) 3320 if keytext == 'SEMICOLON': keytext = ';' 3321 str_list += ["%s:%i-%i%s" % (keytext, rights[mod], wrongs[mod], sep)] 3322 3323 if 'arithmetic' in mods: 3324 str_list += ["%s:%i-%i%s" % (_("Arithmetic"), rights['arithmetic'], wrongs['arithmetic'], sep)] 3325 3326 def calc_percent(r, w): 3327 if r+w: return int(r*100 / float(r+w)) 3328 else: return 0 3329 3330 right = sum([rights[mod] for mod in mods]) 3331 wrong = sum([wrongs[mod] for mod in mods]) 3332 3333 for mod in mods: 3334 category_percents[mod] = calc_percent(rights[mod], wrongs[mod]) 3335 3336 if cfg.JAEGGI_SCORING: 3337 percent = min([category_percents[m] for m in mode.modalities[mode.mode]]) 3338 #percent = min(category_percents['position1'], category_percents['audio']) # cfg.JAEGGI_MODE forces mode.mode==2 3339 if not CLINICAL_MODE: 3340 str_list += [_('Lowest score: %i%%') % percent] 3341 else: 3342 percent = calc_percent(right, wrong) 3343 str_list += [_('Score: %i%%') % percent] 3344 3345 self.label.text = ''.join(str_list) 3346 3347 stats.submit_session(percent, category_percents) 3348 3349# this controls the title of the session history chart. 3350class ChartTitleLabel: 3351 def __init__(self): 3352 self.label = pyglet.text.Label( 3353 '', 3354 font_size=calc_fontsize(10), 3355 bold = True, 3356 color = cfg.COLOR_TEXT, 3357 x = from_right_edge(30), 3358 y = from_top_edge(85), 3359 anchor_x = 'right', 3360 anchor_y = 'top', 3361 batch = batch) 3362 self.update() 3363 def update(self): 3364 if mode.started: 3365 self.label.text = '' 3366 else: 3367 self.label.text = _('Today\'s Last 20:') 3368 3369# this controls the session history chart. 3370class ChartLabel: 3371 def __init__(self): 3372 self.start_x = from_right_edge(140) 3373 self.start_y = from_top_edge(105) 3374 self.line_spacing = calc_fontsize(15) 3375 self.column_spacing_12 = calc_fontsize(30) 3376 self.column_spacing_23 = calc_fontsize(70) 3377 self.font_size = calc_fontsize(10) 3378 self.color_normal = (128, 128, 128, 255) 3379 self.color_advance = (0, 160, 0, 255) 3380 self.color_fallback = (160, 0, 0, 255) 3381 self.column1 = [] 3382 self.column2 = [] 3383 self.column3 = [] 3384 for zap in range(0, 20): 3385 self.column1.append(pyglet.text.Label( 3386 '', font_size = self.font_size, 3387 x = self.start_x, y = self.start_y - zap * self.line_spacing, 3388 anchor_x = 'left', anchor_y = 'top', batch=batch)) 3389 self.column2.append(pyglet.text.Label( 3390 '', font_size = self.font_size, 3391 x = self.start_x + self.column_spacing_12, y = self.start_y - zap * self.line_spacing, 3392 anchor_x = 'left', anchor_y = 'top', batch=batch)) 3393 self.column3.append(pyglet.text.Label( 3394 '', font_size = self.font_size, 3395 x = self.start_x + self.column_spacing_12 + self.column_spacing_23, y = self.start_y - zap * self.line_spacing, 3396 anchor_x = 'left', anchor_y = 'top', batch=batch)) 3397 stats.parse_statsfile() 3398 self.update() 3399 3400 def update(self): 3401 for x in range(0, 20): 3402 self.column1[x].text = '' 3403 self.column2[x].text = '' 3404 self.column3[x].text = '' 3405 if mode.started: return 3406 index = 0 3407 for x in range(len(stats.history) - 20, len(stats.history)): 3408 if x < 0: continue 3409 manual = stats.history[x][4] 3410 color = self.color_normal 3411 if not manual and stats.history[x][3] >= get_threshold_advance(): 3412 color = self.color_advance 3413 elif not manual and stats.history[x][3] < get_threshold_fallback(): 3414 color = self.color_fallback 3415 self.column1[index].color = color 3416 self.column2[index].color = color 3417 self.column3[index].color = color 3418 if manual: 3419 self.column1[index].text = 'M' 3420 elif stats.history[x][0] > -1: 3421 self.column1[index].text = '#%i' % stats.history[x][0] 3422 self.column2[index].text = mode.short_name(mode=stats.history[x][1], back=stats.history[x][2]) 3423 self.column3[index].text = '%i%%' % stats.history[x][3] 3424 index += 1 3425 3426# this controls the title of the session history chart. 3427class AverageLabel: 3428 def __init__(self): 3429 self.label = pyglet.text.Label( 3430 '', 3431 font_size=calc_fontsize(10), bold=False, 3432 color=cfg.COLOR_TEXT, 3433 x=from_right_edge(30), y=from_top_edge(70), 3434 anchor_x='right', anchor_y='top', batch=batch) 3435 self.update() 3436 def update(self): 3437 if mode.started or CLINICAL_MODE: 3438 self.label.text = '' 3439 else: 3440 sessions = [sess for sess in stats.history if sess[1] == mode.mode][-20:] 3441 if sessions: 3442 average = sum([sess[2] for sess in sessions]) / float(len(sessions)) 3443 else: 3444 average = 0. 3445 self.label.text = _("%sNB average: %1.2f") % (mode.short_mode_names[mode.mode], average) 3446 3447 3448class TodayLabel: 3449 def __init__(self): 3450 self.labelTitle = pyglet.text.Label( 3451 '', 3452 font_size=calc_fontsize(9), 3453 color = cfg.COLOR_TEXT, 3454 x=window.width, y=from_top_edge(5), 3455 anchor_x='right', anchor_y='top',width=scale_to_width(280), multiline=True, batch=batch) 3456 self.update() 3457 def update(self): 3458 if mode.started: 3459 self.labelTitle.text = '' 3460 else: 3461 total_trials = sum([mode.num_trials + mode.num_trials_factor * \ 3462 his[2] ** mode.num_trials_exponent for his in stats.history]) 3463 total_time = mode.ticks_per_trial * TICK_DURATION * total_trials 3464 3465 self.labelTitle.text = _( 3466 ("%i min %i sec done today in %i sessions\n" \ 3467 + "%i min %i sec done in last 24 hours in %i sessions") \ 3468 % (stats.time_today//60, stats.time_today%60, stats.sessions_today, \ 3469 stats.time_thours//60, stats.time_thours%60, stats.sessions_thours)) 3470 3471class TrialsRemainingLabel: 3472 def __init__(self): 3473 self.label = pyglet.text.Label( 3474 '', 3475 font_size=calc_fontsize(12), bold = True, 3476 color=cfg.COLOR_TEXT, 3477 x=from_right_edge(10), y=from_top_edge(5), 3478 anchor_x='right', anchor_y='top', batch=batch) 3479 self.update() 3480 def update(self): 3481 if (not mode.started) or mode.hide_text: 3482 self.label.text = '' 3483 else: 3484 self.label.text = _('%i remaining') % (mode.num_trials_total - mode.trial_number) 3485 3486class Saccadic: 3487 def __init__(self): 3488 self.position = 'left' 3489 self.counter = 0 3490 self.radius = scale_to_height(10) 3491 self.color = (0, 0, 255, 255) 3492 3493 def tick(self, dt): 3494 self.counter += 1 3495 if self.counter == cfg.SACCADIC_REPETITIONS: 3496 self.stop() 3497 elif self.position == 'left': 3498 self.position = 'right' 3499 else: self.position = 'left' 3500 3501 def start(self): 3502 self.position = 'left' 3503 mode.saccadic = True 3504 self.counter = 0 3505 pyglet.clock.schedule_interval(saccadic.tick, cfg.SACCADIC_DELAY) 3506 3507 def stop(self): 3508 pyglet.clock.unschedule(saccadic.tick) 3509 mode.saccadic = False 3510 3511 def draw(self): 3512 y = height_center() 3513 if saccadic.position == 'left': 3514 x = self.radius 3515 elif saccadic.position == 'right': 3516 x = window.width - self.radius 3517 pyglet.graphics.draw(4, GL_POLYGON, ('v2i', ( 3518 x - self.radius, y - self.radius, # lower-left 3519 x + self.radius, y - self.radius, # lower-right 3520 x + self.radius, y + self.radius, # upper-right 3521 x - self.radius, y + self.radius, # upper-left 3522 3523 )), ('c4B', self.color * 4)) 3524 3525# self.square = batch.add(40, GL_POLYGON, None, 3526# ('v2i', xy), ('c4B', self.color * 40)) 3527 3528 3529class Panhandle: 3530 def __init__(self, n=-1): 3531 paragraphs = [ 3532_(""" 3533You have completed %i sessions with Brain Workshop. Your perseverance suggests \ 3534that you are finding some benefit from using the program. If you have been \ 3535benefiting from Brain Workshop, don't you think Brain Workshop should \ 3536benefit from you? 3537""") % n, 3538_(""" 3539Brain Workshop is and always will be 100% free. Up until now, Brain Workshop \ 3540as a project has succeeded because a very small number of people have each \ 3541donated a huge amount of time to it. It would be much better if the project \ 3542were supported by small donations from a large number of people. Do your \ 3543part. Donate. 3544"""), 3545_(""" 3546As of March 2010, Brain Workshop has been downloaded over 75,000 times in 20 \ 3547months. If each downloader donated an average of $1, we could afford to pay \ 3548decent full- or part-time salaries (as appropriate) to all of our developers, \ 3549and we would be able to buy advertising to help people learn about Brain \ 3550Workshop. With $2 per downloader, or with more downloaders, we could afford \ 3551to fund controlled experiments and clinical trials on Brain Workshop and \ 3552cognitive training. Help us make that vision a reality. Donate. 3553"""), 3554_(""" 3555The authors think it important that access to cognitive training \ 3556technologies be available to everyone as freely as possible. Like other \ 3557forms of education, cognitive training should not be a luxury of the rich, \ 3558since that would tend to exacerbate class disparity and conflict. Charging \ 3559money for cognitive training does exactly that. The commercial competitors \ 3560of Brain Workshop have two orders of magnitude more users than does Brain \ 3561Workshop because they have far more resources for research, development, and \ 3562marketing. Help us bridge that gap and improve social equality of \ 3563opportunity. Donate. 3564"""), 3565_(""" 3566Brain Workshop has many known bugs and missing features. The developers \ 3567would like to fix these issues, but they also have to work in order to be \ 3568able to pay for rent and food. If you think the developers' time is better \ 3569spent programming than serving coffee, then do something about it. Donate. 3570"""), 3571_(""" 3572Press SPACE to continue, or press D to donate now. 3573""")] # feel free to add more paragraphs or to change the chances for the 3574 # paragraphs you like and dislike, etc. 3575 chances = [-1, 10, 10, 10, 10, 0] # if < 0, 100% chance of being included. Otherwise, relative weight. 3576 # if == 0, appended to end and not counted 3577 # for target_len. 3578 assert len(chances) == len(paragraphs) 3579 target_len = 3 3580 text = [] 3581 options = [] 3582 for i in range(len(chances)): 3583 if chances[i] < 0: 3584 text.append(i) 3585 else: 3586 options.extend([i]*chances[i]) 3587 while len(text) < target_len and len(options) > 0: 3588 choice = random.choice(options) 3589 while choice in options: 3590 options.remove(choice) 3591 text.append(choice) 3592 for i in range(len(chances)): 3593 if chances[i] == 0: 3594 text.append(i) 3595 self.text = ''.join([paragraphs[i] for i in text]) 3596 3597 self.batch = pyglet.graphics.Batch() 3598 self.label = pyglet.text.Label(self.text, 3599 color=cfg.COLOR_TEXT, 3600 batch=self.batch, 3601 multiline=True, 3602 width=(4*window.width)/5, 3603 font_size=calc_fontsize(14), 3604 x=width_center(), y=height_center(), 3605 anchor_x='center', anchor_y='center') 3606 window.push_handlers(self.on_key_press, self.on_draw) 3607 self.on_draw() 3608 3609 def on_key_press(self, sym, mod): 3610 if sym in (key.ESCAPE, key.SPACE): 3611 self.close() 3612 elif sym in (key.RETURN, key.ENTER, key.D): 3613 self.select() 3614 return pyglet.event.EVENT_HANDLED 3615 3616 def select(self): 3617 webbrowser.open_new_tab(WEB_DONATE) 3618 self.close() 3619 3620 def close(self): 3621 return window.remove_handlers(self.on_key_press, self.on_draw) 3622 3623 def on_draw(self): 3624 window.clear() 3625 self.batch.draw() 3626 return pyglet.event.EVENT_HANDLED 3627 3628# 3629# --- END GRAPHICS SECTION ---------------------------------------------- 3630# 3631 3632# this class stores the raw statistics and history information. 3633# the information is analyzed by the AnalysisLabel class. 3634class Stats: 3635 def __init__(self): 3636 # set up data variables 3637 self.initialize_session() 3638 self.history = [] 3639 self.full_history = [] # not just today 3640 self.sessions_today = 0 3641 self.time_today = 0 3642 self.time_thours = 0 3643 self.sessions_thours = 0 3644 3645 def parse_statsfile(self): 3646 self.clear() 3647 if os.path.isfile(os.path.join(get_data_dir(), cfg.STATSFILE)): 3648 try: 3649 #last_session = [] 3650 #last_session_number = 0 3651 last_mode = 0 3652 last_back = 0 3653 statsfile_path = os.path.join(get_data_dir(), cfg.STATSFILE) 3654 statsfile = open(statsfile_path, 'r') 3655 is_today = False 3656 is_thours = False 3657 today = date.today() 3658 yesterday = date.fromordinal(today.toordinal() - 1) 3659 tomorrow = date.fromordinal(today.toordinal() + 1) 3660 for line in statsfile: 3661 if line == '': continue 3662 if line == '\n': continue 3663 if line[0] not in '0123456789': continue 3664 datestamp = date(int(line[:4]), int(line[5:7]), int(line[8:10])) 3665 hour = int(line[11:13]) 3666 mins = int(line[14:16]) 3667 sec = int(line[17:19]) 3668 thour = datetime.datetime.today().hour 3669 tmin = datetime.datetime.today().minute 3670 tsec = datetime.datetime.today().second 3671 if int(strftime('%H')) < cfg.ROLLOVER_HOUR: 3672 if datestamp == today or (datestamp == yesterday and hour >= cfg.ROLLOVER_HOUR): 3673 is_today = True 3674 elif datestamp == today and hour >= cfg.ROLLOVER_HOUR: 3675 is_today = True 3676 if datestamp == today or (datestamp == yesterday and (hour > thour or (hour == thour and (mins > tmin or (mins == tmin and sec > tsec))))): 3677 is_thours = True 3678 if '\t' in line: 3679 separator = '\t' 3680 else: separator = ',' 3681 newline = line.split(separator) 3682 newmode = int(newline[3]) 3683 newback = int(newline[4]) 3684 newpercent = int(newline[2]) 3685 newmanual = bool(int(newline[7])) 3686 newsession_number = int(newline[8]) 3687 try: 3688 sesstime = int(round(float(newline[25]))) 3689 except Exception as e: 3690 debug_msg(e) 3691 # this session wasn't performed with this version of BW, and is therefore 3692 # old, and therefore the session time doesn't matter 3693 sesstime = 0 3694 if newmanual: 3695 newsession_number = 0 3696 self.full_history.append([newsession_number, newmode, newback, newpercent, newmanual]) 3697 if is_thours: 3698 stats.sessions_thours += 1 3699 stats.time_thours += sesstime 3700 if is_today: 3701 stats.sessions_today += 1 3702 self.time_today += sesstime 3703 self.history.append([newsession_number, newmode, newback, newpercent, newmanual]) 3704 #if not newmanual and (is_today or cfg.RESET_LEVEL): 3705 # last_session = self.full_history[-1] 3706 statsfile.close() 3707 self.retrieve_progress() 3708 3709 except Exception as e: 3710 debug_msg(e) 3711 quit_with_error(_('Error parsing stats file\n%s') % 3712 os.path.join(get_data_dir(), cfg.STATSFILE), 3713 _('\nPlease fix, delete or rename the stats file.'), 3714 quit=False) 3715 3716 def retrieve_progress(self): 3717 if cfg.RESET_LEVEL: 3718 sessions = [s for s in self.history if s[1] == mode.mode] 3719 else: 3720 sessions = [s for s in self.full_history if s[1] == mode.mode] 3721 mode.enforce_standard_mode() 3722 if sessions: 3723 ls = sessions[-1] 3724 mode.back = ls[2] 3725 if ls[3] >= get_threshold_advance(): 3726 mode.back += 1 3727 mode.session_number = ls[0] 3728 mode.progress = 0 3729 for s in sessions: 3730 if s[2] == mode.back and s[3] < get_threshold_fallback(): 3731 mode.progress += 1 3732 elif s[2] != mode.back: 3733 mode.progress = 0 3734 if mode.progress >= cfg.THRESHOLD_FALLBACK_SESSIONS: 3735 mode.progress = 0 3736 mode.back -= 1 3737 if mode.back < 1: 3738 mode.back = 1 3739 else: # no sessions today for this user and this mode 3740 mode.back = default_nback_mode(mode.mode) 3741 mode.num_trials_total = mode.num_trials + mode.num_trials_factor * mode.back ** mode.num_trials_exponent 3742 3743 def initialize_session(self): 3744 self.session = {} 3745 for name in ('position1', 'position2', 'position3', 'position4', 3746 'vis1', 'vis2', 'vis3', 'vis4', 3747 'color', 'image', 'audio', 'audio2' 3748 ): 3749 self.session[name] = [] 3750 self.session["%s_input" % name] = [] 3751 self.session["%s_rt" % name] = [] # reaction times 3752 for name in ('vis', 'numbers', 'operation', 'visvis_input', 3753 'visaudio_input', 'audiovis_input', 'arithmetic_input', 'visvis_rt', 3754 'visaudio_rt', 'audiovis_rt' # , 'arithmetic_rt' 3755 ): 3756 self.session[name] = [] 3757 3758 def save_input(self): 3759 for k, v in mode.current_stim.items(): 3760 if k == 'number': 3761 self.session['numbers'].append(v) 3762 else: 3763 self.session[k].append(v) 3764 if k == 'vis': # goes to both self.session['vis'] and ['image'] 3765 self.session['image'].append(v) 3766 for k, v in mode.inputs.items(): 3767 self.session[k + '_input'].append(v) 3768 for k, v in mode.input_rts.items(): 3769 self.session[k + '_rt'].append(v) 3770 3771 self.session['operation'].append(mode.current_operation) 3772 self.session['arithmetic_input'].append(arithmeticAnswerLabel.parse_answer()) 3773 3774 3775 def submit_session(self, percent, category_percents): 3776 global musicplayer 3777 global applauseplayer 3778 self.history.append([mode.session_number, mode.mode, mode.back, percent, mode.manual]) 3779 3780 if ATTEMPT_TO_SAVE_STATS: 3781 try: 3782 sep = STATS_SEPARATOR 3783 statsfile_path = os.path.join(get_data_dir(), cfg.STATSFILE) 3784 statsfile = open(statsfile_path, 'a') 3785 outlist = [strftime("%Y-%m-%d %H:%M:%S"), 3786 mode.short_name(), 3787 str(percent), 3788 str(mode.mode), 3789 str(mode.back), 3790 str(mode.ticks_per_trial), 3791 str(mode.num_trials_total), 3792 str(int(mode.manual)), 3793 str(mode.session_number), 3794 str(category_percents['position1']), 3795 str(category_percents['audio']), 3796 str(category_percents['color']), 3797 str(category_percents['visvis']), 3798 str(category_percents['audiovis']), 3799 str(category_percents['arithmetic']), 3800 str(category_percents['image']), 3801 str(category_percents['visaudio']), 3802 str(category_percents['audio2']), 3803 str(category_percents['position2']), 3804 str(category_percents['position3']), 3805 str(category_percents['position4']), 3806 str(category_percents['vis1']), 3807 str(category_percents['vis2']), 3808 str(category_percents['vis3']), 3809 str(category_percents['vis4']), 3810 str(mode.ticks_per_trial * TICK_DURATION * mode.num_trials_total), 3811 str(0), 3812 ] 3813 statsfile.write(sep.join(outlist)) # adds sep between each element 3814 statsfile.write('\n') # but we don't want a sep before '\n' 3815 statsfile.close() 3816 if CLINICAL_MODE: 3817 picklefile = open(os.path.join(get_data_dir(), STATS_BINARY), 'ab') 3818 pickle.dump([strftime("%Y-%m-%d %H:%M:%S"), mode.short_name(), 3819 percent, mode.mode, mode.back, mode.ticks_per_trial, 3820 mode.num_trials_total, int(mode.manual), 3821 mode.session_number, category_percents['position1'], 3822 category_percents['audio'], category_percents['color'], 3823 category_percents['visvis'], category_percents['audiovis'], 3824 category_percents['arithmetic'], category_percents['image'], 3825 category_percents['visaudio'], category_percents['audio2'], 3826 category_percents['position2'], category_percents['position3'], 3827 category_percents['position4'], 3828 category_percents['vis1'], category_percents['vis2'], 3829 category_percents['vis3'], category_percents['vis4']], 3830 picklefile, protocol=2) 3831 picklefile.close() 3832 cfg.SAVE_SESSIONS = True # FIXME: put this where it belongs 3833 cfg.SESSION_STATS = USER + '-sessions.dat' # FIXME: default user; configurability 3834 if cfg.SAVE_SESSIONS: 3835 picklefile = open(os.path.join(get_data_dir(), cfg.SESSION_STATS), 'ab') 3836 session = {} # it's not a dotdict because we want to pickle it 3837 session['summary'] = outlist # that's what goes into stats.txt 3838 session['cfg'] = cfg.__dict__ 3839 session['timestamp'] = strftime("%Y-%m-%d %H:%M:%S") 3840 session['mode'] = mode.mode 3841 session['n'] = mode.back 3842 session['manual'] = mode.manual 3843 session['trial_duration'] = mode.ticks_per_trial * TICK_DURATION 3844 session['trials'] = mode.num_trials_total 3845 session['session'] = self.session 3846 pickle.dump(session, picklefile) 3847 picklefile.close() 3848 except Exception as e: 3849 debug_msg(e) 3850 quit_with_error(_('Error writing to stats file\n%s') % 3851 os.path.join(get_data_dir(), cfg.STATSFILE), 3852 _('\nPlease check file and directory permissions.')) 3853 3854 perfect = awesome = great = good = advance = fallback = False 3855 3856 if not mode.manual: 3857 if percent >= get_threshold_advance(): 3858 mode.back += 1 3859 mode.num_trials_total = (mode.num_trials + 3860 mode.num_trials_factor * mode.back ** mode.num_trials_exponent) 3861 mode.progress = 0 3862 circles.update() 3863 if cfg.USE_APPLAUSE: 3864 play_applause() 3865 advance = True 3866 elif mode.back > 1 and percent < get_threshold_fallback(): 3867 if cfg.JAEGGI_MODE: 3868 mode.back -= 1 3869 fallback = True 3870 else: 3871 if mode.progress == cfg.THRESHOLD_FALLBACK_SESSIONS - 1: 3872 mode.back -= 1 3873 mode.num_trials_total = mode.num_trials + mode.num_trials_factor * mode.back ** mode.num_trials_exponent 3874 fallback = True 3875 mode.progress = 0 3876 circles.update() 3877 else: 3878 mode.progress += 1 3879 circles.update() 3880 3881 if percent == 100: perfect = True 3882 elif percent >= get_threshold_advance(): awesome = True 3883 elif percent >= (get_threshold_advance() + get_threshold_fallback()) // 2: great = True 3884 elif percent >= get_threshold_fallback(): good = True 3885 congratsLabel.update(True, advance, fallback, awesome, great, good, perfect) 3886 3887 if mode.manual and not cfg.USE_MUSIC_MANUAL: 3888 return 3889 3890 if cfg.USE_MUSIC: 3891 play_music(percent) 3892 3893 def clear(self): 3894 self.history = [] 3895 self.sessions_today = 0 3896 self.time_today = 0 3897 self.sessions_thours = 0 3898 self.time_thours = 0 3899 3900def update_all_labels(do_analysis=False): 3901 updateLabel.update() 3902 congratsLabel.update() 3903 if do_analysis: 3904 analysisLabel.update() 3905 else: 3906 analysisLabel.update(skip=True) 3907 3908 if preventMusicSkipping: pyglet.clock.tick(poll=True) # Prevent music/applause skipping 1 3909 3910 gameModeLabel.update() 3911 keysListLabel.update() 3912 pausedLabel.update() 3913 sessionInfoLabel.update() 3914 thresholdLabel.update() 3915 spaceLabel.update() 3916 chartTitleLabel.update() 3917 chartLabel.update() 3918 3919 if preventMusicSkipping: pyglet.clock.tick(poll=True) # Prevent music/applause skipping 2 3920 3921 averageLabel.update() 3922 todayLabel.update() 3923 trialsRemainingLabel.update() 3924 3925 update_input_labels() 3926 3927def update_input_labels(): 3928 arithmeticAnswerLabel.update() 3929 for label in input_labels: 3930 label.update() 3931 3932# this function handles initiation of a new session. 3933def new_session(): 3934 mode.tick = -9 # give a 1-second delay before displaying first trial 3935 mode.tick -= 5 * (mode.flags[mode.mode]['multi'] - 1 ) 3936 if cfg.MULTI_MODE == 'image': 3937 mode.tick -= 5 * (mode.flags[mode.mode]['multi'] - 1 ) 3938 3939 mode.session_number += 1 3940 mode.trial_number = 0 3941 mode.started = True 3942 mode.paused = False 3943 circles.update() 3944 3945 mode.sound_mode = random.choice(cfg.AUDIO1_SETS) 3946 mode.sound2_mode = random.choice(cfg.AUDIO2_SETS) 3947 3948 visuals[0].load_set() 3949 visuals[0].choose_random_images(8) 3950 visuals[0].letters = random.sample(sounds[mode.sound_mode ].keys(), 8) 3951 visuals[0].letters2 = random.sample(sounds[mode.sound2_mode].keys(), 8) 3952 3953 3954 for i in range(1, mode.flags[mode.mode]['multi']): 3955 visuals[i].load_set(visuals[0].image_set_index) 3956 visuals[i].choose_indicated_images(visuals[0].image_indices) 3957 visuals[i].letters = visuals[0].letters # I don't think these are used for anything, but I'm not sure 3958 visuals[i].letters2 = visuals[0].letters2 3959 3960 global input_labels 3961 input_labels.extend(generate_input_labels()) # have to do this after images are loaded 3962 3963 3964 mode.soundlist = [sounds[mode.sound_mode][l] for l in visuals[0].letters] 3965 mode.soundlist2 = [sounds[mode.sound2_mode][l] for l in visuals[0].letters2] 3966 3967 if cfg.JAEGGI_MODE: 3968 compute_bt_sequence() 3969 3970 if preventMusicSkipping: pyglet.clock.tick(poll=True) # Prevent music/applause skipping 3971 3972 if cfg.VARIABLE_NBACK: 3973 # compute variable n-back sequence using beta distribution 3974 mode.variable_list = [] 3975 for index in range(0, mode.num_trials_total - mode.back): 3976 mode.variable_list.append(int(random.betavariate(mode.back / 2.0, 1) * mode.back + 1)) 3977 field.crosshair_update() 3978 reset_input() 3979 stats.initialize_session() 3980 update_all_labels() 3981 pyglet.clock.schedule_interval(fade_out, 0.05) 3982 3983# this function handles the finish or cancellation of a session. 3984def end_session(cancelled=False): 3985 for label in input_labels: 3986 label.delete() 3987 while input_labels: 3988 input_labels.remove(input_labels[0]) 3989 if cancelled: 3990 mode.session_number -= 1 3991 if not cancelled: 3992 stats.sessions_today += 1 3993 for visual in visuals: visual.hide() 3994 mode.started = False 3995 mode.paused = False 3996 circles.update() 3997 field.crosshair_update() 3998 reset_input() 3999 if cancelled: 4000 update_all_labels() 4001 else: 4002 update_all_labels(do_analysis = True) 4003 if cfg.PANHANDLE_FREQUENCY: 4004 statsfile_path = os.path.join(get_data_dir(), cfg.STATSFILE) 4005 statsfile = open(statsfile_path, 'r') 4006 sessions = len(statsfile.readlines()) # let's just hope people 4007 statsfile.close() # don't manually edit their statsfiles 4008 if (sessions % cfg.PANHANDLE_FREQUENCY) == 0 and not CLINICAL_MODE: 4009 Panhandle(n=sessions) 4010 4011 4012 4013# this function causes the key labels along the bottom to revert to their 4014# "non-pressed" state for a new trial or when returning to the main screen. 4015def reset_input(): 4016 for k in list(mode.inputs): 4017 mode.inputs[k] = False 4018 mode.input_rts[k] = 0. 4019 arithmeticAnswerLabel.reset_input() 4020 update_input_labels() 4021 4022# this handles the computation of a round with exactly 6 position and 6 audio matches 4023# this function is not currently used -- compute_bt_sequence() is used instead 4024##def new_compute_bt_sequence(matches=6, modalities=['audio', 'vis']): 4025## # not ready for visaudio or audiovis, doesn't get 4026## seq = {} 4027## for m in modalities: 4028## seq[m] = [False]*mode.back + \ 4029## random.shuffle([True]*matches + 4030## [False]*(mode.num_trials_total - mode.back - matches)) 4031## for i in range(mode.back): 4032## seq[m][i] = random.randint(1,8) 4033## 4034## for i in range(mode.back, len(seq[m])): 4035## if seq[m][i] == True: 4036## seq[m][i] = seq[m][i-mode.back] 4037## elif seq[m][i] == False: # should be all other cases 4038## seq[m][i] = random.randint(1,7) 4039## if seq[m][i] >= seq[m][i-mode.back]: 4040## seq[m][i] += 1 4041## mode.bt_sequence = seq.values() 4042 4043def compute_bt_sequence(): 4044 bt_sequence = [[], []] 4045 for x in range(0, mode.num_trials_total): 4046 bt_sequence[0].append(0) 4047 bt_sequence[1].append(0) 4048 4049 for x in range(0, mode.back): 4050 bt_sequence[0][x] = random.randint(1, 8) 4051 bt_sequence[1][x] = random.randint(1, 8) 4052 4053 position = 0 4054 audio = 0 4055 both = 0 4056 4057 # brute force it 4058 while True: 4059 position = 0 4060 for x in range(mode.back, mode.num_trials_total): 4061 bt_sequence[0][x] = random.randint(1, 8) 4062 if bt_sequence[0][x] == bt_sequence[0][x - mode.back]: 4063 position += 1 4064 if position != 6: 4065 continue 4066 while True: 4067 audio = 0 4068 for x in range(mode.back, mode.num_trials_total): 4069 bt_sequence[1][x] = random.randint(1, 8) 4070 if bt_sequence[1][x] == bt_sequence[1][x - mode.back]: 4071 audio += 1 4072 if audio == 6: 4073 break 4074 both = 0 4075 for x in range(mode.back, mode.num_trials_total): 4076 if bt_sequence[0][x] == bt_sequence[0][x - mode.back] and bt_sequence[1][x] == bt_sequence[1][x - mode.back]: 4077 both += 1 4078 if both == 2: 4079 break 4080 4081 mode.bt_sequence = bt_sequence 4082 4083player = get_pyglet_media_Player() 4084player2 = get_pyglet_media_Player() 4085# responsible for the random generation of each new stimulus (audio, color, position) 4086def generate_stimulus(): 4087 # first, randomly generate all stimuli 4088 positions = random.sample(range(1,9), 4) # sample without replacement 4089 for s, p in zip(range(1, 5), positions): 4090 mode.current_stim['position' + repr(s)] = p 4091 mode.current_stim['vis' + repr(s)] = random.randint(1, 8) 4092 4093 #mode.current_stim['position1'] = random.randint(1, 8) 4094 mode.current_stim['color'] = random.randint(1, 8) 4095 mode.current_stim['vis'] = random.randint(1, 8) 4096 mode.current_stim['audio'] = random.randint(1, 8) 4097 mode.current_stim['audio2'] = random.randint(1, 8) 4098 4099 4100 # treat arithmetic specially 4101 operations = [] 4102 if cfg.ARITHMETIC_USE_ADDITION: operations.append('add') 4103 if cfg.ARITHMETIC_USE_SUBTRACTION: operations.append('subtract') 4104 if cfg.ARITHMETIC_USE_MULTIPLICATION: operations.append('multiply') 4105 if cfg.ARITHMETIC_USE_DIVISION: operations.append('divide') 4106 mode.current_operation = random.choice(operations) 4107 4108 if cfg.ARITHMETIC_USE_NEGATIVES: 4109 min_number = 0 - cfg.ARITHMETIC_MAX_NUMBER 4110 else: 4111 min_number = 0 4112 max_number = cfg.ARITHMETIC_MAX_NUMBER 4113 4114 if mode.current_operation == 'divide' and 'arithmetic' in mode.modalities[mode.mode]: 4115 if len(stats.session['position1']) >= mode.back: 4116 number_nback = stats.session['numbers'][mode.trial_number - mode.back - 1] 4117 possibilities = [] 4118 for x in range(min_number, max_number + 1): 4119 if x == 0: 4120 continue 4121 if number_nback % x == 0: 4122 possibilities.append(x) 4123 continue 4124 frac = Decimal(abs(number_nback)) / Decimal(abs(x)) 4125 if (frac % 1) in map(Decimal, cfg.ARITHMETIC_ACCEPTABLE_DECIMALS): 4126 possibilities.append(x) 4127 mode.current_stim['number'] = random.choice(possibilities) 4128 else: 4129 mode.current_stim['number'] = random.randint(min_number, max_number) 4130 while mode.current_stim['number'] == 0: 4131 mode.current_stim['number'] = random.randint(min_number, max_number) 4132 else: 4133 mode.current_stim['number'] = random.randint(min_number, max_number) 4134 4135 multi = mode.flags[mode.mode]['multi'] 4136 4137 real_back = mode.back 4138 if mode.flags[mode.mode]['crab'] == 1: 4139 real_back = 1 + 2*((mode.trial_number-1) % mode.back) 4140 else: 4141 real_back = mode.back 4142 if cfg.VARIABLE_NBACK: 4143 real_back = mode.variable_list[mode.trial_number - real_back - 1] 4144 4145 if mode.modalities[mode.mode] != ['arithmetic'] and mode.trial_number > mode.back: 4146 for mod in mode.modalities[mode.mode]: 4147 if mod in ('visvis', 'visaudio', 'image'): 4148 current = 'vis' 4149 elif mod in ('audiovis', ): 4150 current = 'audio' 4151 elif mod == 'arithmetic': 4152 continue 4153 else: 4154 current = mod 4155 if mod in ('visvis', 'audiovis', 'image'): 4156 back_data = 'vis' 4157 elif mod in ('visaudio', ): 4158 back_data = 'audio' 4159 else: 4160 back_data = mod 4161 4162 back = None 4163 r1, r2 = random.random(), random.random() 4164 if multi > 1: 4165 r2 = 3./2. * r2 # 33% chance of multi-stim reversal 4166 4167 if (r1 < cfg.CHANCE_OF_GUARANTEED_MATCH): 4168 back = real_back 4169 4170 elif r2 < cfg.CHANCE_OF_INTERFERENCE and mode.back > 1: 4171 back = real_back 4172 interference = [-1, 1, mode.back] 4173 if back < 3: interference = interference[1:] # for crab mode and 2-back 4174 random.shuffle(interference) 4175 for i in interference: # we'll just take the last one that works. 4176 if mode.trial_number - (real_back+i) - 1 >= 0 and \ 4177 stats.session[back_data][mode.trial_number - (real_back+i) - 1] != \ 4178 stats.session[back_data][mode.trial_number - real_back - 1]: 4179 back = real_back + i 4180 if back == real_back: back = None # if none of the above worked 4181 elif DEBUG: 4182 print('Forcing interference for %s' % current) 4183 4184 if back: 4185 nback_trial = mode.trial_number - back - 1 4186 matching_stim = stats.session[back_data][nback_trial] 4187 # check for collisions in multi-stim mode 4188 if multi > 1 and mod.startswith('position'): 4189 potential_conflicts = set(range(1, multi+1)) - set([int(mod[-1])]) 4190 conflict_positions = [positions[i-1] for i in potential_conflicts] 4191 if matching_stim in conflict_positions: # swap 'em 4192 i = positions.index(matching_stim) 4193 if DEBUG: 4194 print("moving position%i from %i to %i for %s" % (i+1, positions[i], mode.current_stim[current], current)) 4195 mode.current_stim['position' + repr(i+1)] = mode.current_stim[current] 4196 positions[i] = mode.current_stim[current] 4197 positions[int(current[-1])-1] = matching_stim 4198 if DEBUG: 4199 print("setting %s to %i" % (current, matching_stim)) 4200 mode.current_stim[current] = matching_stim 4201 4202 if multi > 1: 4203 if random.random() < cfg.CHANCE_OF_INTERFERENCE / 3.: 4204 mod = 'position' 4205 if 'vis1' in mode.modalities[mode.mode] and random.random() < .5: 4206 mod = 'vis' 4207 offset = random.choice(range(1, multi)) 4208 for i in range(multi): 4209 mode.current_stim[mod + repr(i+1)] = stats.session[mod + repr(((i+offset)%multi) + 1)][mode.trial_number - real_back - 1] 4210 if mod == 'position': 4211 positions[i] = mode.current_stim[mod + repr(i+1)] 4212 4213 4214 # set static stimuli according to mode. 4215 # default position is 0 (center) 4216 # default color is 1 (red) or 2 (black) 4217 # default vis is 0 (square) 4218 # audio is never static so it doesn't have a default. 4219 if not 'color' in mode.modalities[mode.mode]: mode.current_stim['color'] = cfg.VISUAL_COLORS[0] 4220 if not 'position1' in mode.modalities[mode.mode]: mode.current_stim['position1'] = 0 4221 if not set(['visvis', 'arithmetic', 'image']).intersection( mode.modalities[mode.mode] ): 4222 mode.current_stim['vis'] = 0 4223 if multi > 1 and not 'vis1' in mode.modalities[mode.mode]: 4224 for i in range(1, 5): 4225 if cfg.MULTI_MODE == 'color': 4226 mode.current_stim['vis'+repr(i)] = 0 # use squares 4227 elif cfg.MULTI_MODE == 'image': 4228 mode.current_stim['vis'+repr(i)] = cfg.VISUAL_COLORS[0] 4229 4230 # in jaeggi mode, set using the predetermined sequence. 4231 if cfg.JAEGGI_MODE: 4232 mode.current_stim['position1'] = mode.bt_sequence[0][mode.trial_number - 1] 4233 mode.current_stim['audio'] = mode.bt_sequence[1][mode.trial_number - 1] 4234 4235 # initiate the chosen stimuli. 4236 # mode.current_stim['audio'] is a number from 1 to 8. 4237 if 'arithmetic' in mode.modalities[mode.mode] and mode.trial_number > mode.back: 4238 player.queue(sounds['operations'][mode.current_operation]) # maybe we should try... catch... here 4239 player.play() # and maybe we should recycle sound players... 4240 elif 'audio' in mode.modalities[mode.mode] and not 'audio2' in mode.modalities[mode.mode]: 4241 player.queue(mode.soundlist[mode.current_stim['audio']-1]) 4242 player.play() 4243 elif 'audio2' in mode.modalities[mode.mode]: 4244 # dual audio modes - two sound players 4245 player.queue(mode.soundlist[mode.current_stim['audio']-1]) 4246 player.min_distance = 100.0 4247 if cfg.CHANNEL_AUDIO1 == 'left': 4248 player.position = (-99.0, 0.0, 0.0) 4249 elif cfg.CHANNEL_AUDIO1 == 'right': 4250 player.position = (99.0, 0.0, 0.0) 4251 elif cfg.CHANNEL_AUDIO1 == 'center': 4252 #player.position = (0.0, 0.0, 0.0) 4253 pass 4254 player.play() 4255 player2.queue(mode.soundlist2[mode.current_stim['audio2']-1]) 4256 player2.min_distance = 100.0 4257 if cfg.CHANNEL_AUDIO2 == 'left': 4258 player2.position = (-99.0, 0.0, 0.0) 4259 elif cfg.CHANNEL_AUDIO2 == 'right': 4260 player2.position = (99.0, 0.0, 0.0) 4261 elif cfg.CHANNEL_AUDIO2 == 'center': 4262 #player2.position = (0.0, 0.0, 0.0) 4263 pass 4264 player2.play() 4265 4266 4267 if cfg.VARIABLE_NBACK and mode.trial_number > mode.back: 4268 variable = mode.variable_list[mode.trial_number - 1 - mode.back] 4269 else: 4270 variable = 0 4271 if DEBUG and multi < 2: 4272 print("trial=%i, \tpos=%i, \taud=%i, \tcol=%i, \tvis=%i, \tnum=%i,\top=%s, \tvar=%i" % \ 4273 (mode.trial_number, mode.current_stim['position1'], mode.current_stim['audio'], 4274 mode.current_stim['color'], mode.current_stim['vis'], \ 4275 mode.current_stim['number'], mode.current_operation, variable)) 4276 if multi == 1: 4277 visuals[0].spawn(mode.current_stim['position1'], mode.current_stim['color'], 4278 mode.current_stim['vis'], mode.current_stim['number'], 4279 mode.current_operation, variable) 4280 else: # multi > 1 4281 for i in range(1, multi+1): 4282 if cfg.MULTI_MODE == 'color': 4283 if DEBUG: 4284 print("trial=%i, \tpos=%i, \taud=%i, \tcol=%i, \tvis=%i, \tnum=%i,\top=%s, \tvar=%i" % \ 4285 (mode.trial_number, mode.current_stim['position' + repr(i)], mode.current_stim['audio'], 4286 cfg.VISUAL_COLORS[i-1], mode.current_stim['vis'+repr(i)], \ 4287 mode.current_stim['number'], mode.current_operation, variable)) 4288 visuals[i-1].spawn(mode.current_stim['position'+repr(i)], cfg.VISUAL_COLORS[i-1], 4289 mode.current_stim['vis'+repr(i)], mode.current_stim['number'], 4290 mode.current_operation, variable) 4291 else: 4292 if DEBUG: 4293 print("trial=%i, \tpos=%i, \taud=%i, \tcol=%i, \tvis=%i, \tnum=%i,\top=%s, \tvar=%i" % \ 4294 (mode.trial_number, mode.current_stim['position' + repr(i)], mode.current_stim['audio'], 4295 mode.current_stim['vis'+repr(i)], i, \ 4296 mode.current_stim['number'], mode.current_operation, variable)) 4297 visuals[i-1].spawn(mode.current_stim['position'+repr(i)], mode.current_stim['vis'+repr(i)], 4298 i, mode.current_stim['number'], 4299 mode.current_operation, variable) 4300 4301def toggle_manual_mode(): 4302 if mode.manual: 4303 mode.manual = False 4304 else: 4305 mode.manual = True 4306 4307 #if not mode.manual: 4308 #mode.enforce_standard_mode() 4309 4310 update_all_labels() 4311 4312def set_user(newuser): 4313 global cfg 4314 global USER 4315 global CONFIGFILE 4316 USER = newuser 4317 if USER.lower() == 'default': 4318 CONFIGFILE = 'config.ini' 4319 else: 4320 CONFIGFILE = USER + '-config.ini' 4321 rewrite_configfile(CONFIGFILE, overwrite=False) 4322 cfg = parse_config(CONFIGFILE) 4323 stats.initialize_session() 4324 stats.parse_statsfile() 4325 if len(stats.full_history) > 0 and not cfg.JAEGGI_MODE: 4326 mode.mode = stats.full_history[-1][1] 4327 stats.retrieve_progress() 4328 # text labels also need to be remade; until that's done, this remains commented out 4329 #if cfg.BLACK_BACKGROUND: 4330 # glClearColor(0, 0, 0, 1) 4331 #else: 4332 # glClearColor(1, 1, 1, 1) 4333 window.set_fullscreen(cfg.WINDOW_FULLSCREEN) # window size needs to be changed 4334 update_all_labels() 4335 save_last_user('defaults.ini') 4336 4337 4338def get_users(): 4339 users = ['default'] + [fn.split('-')[0] for fn in os.listdir(get_data_dir()) if '-stats.txt' in fn] 4340 if 'Readme' in users: users.remove('Readme') 4341 return users 4342 4343# there are 4 event loops: 4344# on_mouse_press: allows the user to use the mouse (LMB and RMB) instead of keys 4345# on_key_press: listens to the keyboard and acts when certain keys are pressed 4346# on_draw: draws everything to the screen something like 60 times per second 4347# update(dt): the session timer loop which controls the game during the sessions. 4348# Runs once every quarter-second. 4349# 4350# --- BEGIN EVENT LOOP SECTION ---------------------------------------------- 4351# 4352 4353# this is where the keyboard keys are defined. 4354@window.event 4355def on_mouse_press(x, y, button, modifiers): 4356 Flag = True 4357 if mode.started: 4358 if len(mode.modalities[mode.mode])==2: 4359 for k in mode.modalities[mode.mode]: 4360 if k == 'arithmetic': 4361 Flag = False 4362 if Flag: 4363 if (button == pyglet.window.mouse.LEFT): 4364 mode.inputs[mode.modalities[mode.mode][0]] = True 4365 elif (button == pyglet.window.mouse.RIGHT): 4366 mode.inputs[mode.modalities[mode.mode][1]] = True 4367 update_input_labels() 4368 4369@window.event 4370def on_key_press(symbol, modifiers): 4371 if symbol == key.D and (modifiers & key.MOD_CTRL): 4372 dump_pyglet_info() 4373 4374 elif mode.title_screen and not mode.draw_graph: 4375 if symbol == key.ESCAPE or symbol == key.X: 4376 window.on_close() 4377 4378 elif symbol == key.SPACE: 4379 mode.title_screen = False 4380 #mode.shrink_brain = True 4381 #pyglet.clock.schedule_interval(shrink_brain, 1/60.) 4382 4383 elif symbol == key.C and not cfg.JAEGGI_MODE: 4384 GameSelect() 4385 4386 elif symbol == key.I and not cfg.JAEGGI_MODE: 4387 ImageSelect() 4388 4389 elif symbol == key.H: 4390 webbrowser.open_new_tab(WEB_TUTORIAL) 4391 4392 elif symbol == key.D and not CLINICAL_MODE: 4393 webbrowser.open_new_tab(WEB_DONATE) 4394 4395 elif symbol == key.V and DEBUG: 4396 OptionsScreen() 4397 4398 elif symbol == key.G: 4399# sound_stop() 4400 graph.parse_stats() 4401 graph.graph = mode.mode 4402 mode.draw_graph = True 4403 4404 elif symbol == key.U: 4405 UserScreen() 4406 4407 elif symbol == key.L: 4408 LanguageScreen() 4409 4410 elif symbol == key.S and not cfg.JAEGGI_MODE: 4411 SoundSelect() 4412 4413 elif symbol == key.F: 4414 webbrowser.open_new_tab(WEB_FORUM) 4415 4416 elif symbol == key.O: 4417 edit_config_ini() 4418 4419 elif mode.draw_graph: 4420 if symbol == key.ESCAPE or symbol == key.G or symbol == key.X: 4421 mode.draw_graph = False 4422 4423 #elif symbol == key.E and (modifiers & key.MOD_CTRL): 4424 #graph.export_data() 4425 4426 elif symbol == key.N: 4427 graph.next_nonempty_mode() 4428 4429 elif symbol == key.M: 4430 graph.next_style() 4431 4432 elif mode.saccadic: 4433 if symbol in (key.ESCAPE, key.E, key.X, key.SPACE): 4434 saccadic.stop() 4435 4436 elif not mode.started: 4437 4438 if symbol == key.ESCAPE or symbol == key.X: 4439 if cfg.SKIP_TITLE_SCREEN: 4440 window.on_close() 4441 else: 4442 mode.title_screen = True 4443 4444 elif symbol == key.SPACE: 4445 new_session() 4446 4447 elif CLINICAL_MODE: 4448 pass 4449 #if symbol == key.H: 4450 #webbrowser.open_new_tab(CLINICAL_TUTORIAL) 4451 # No elifs below this line at this indentation will be 4452 # executed in CLINICAL_MODE 4453 4454 elif symbol == key.E and cfg.WINDOW_FULLSCREEN: 4455 saccadic.start() 4456 4457 elif symbol == key.G: 4458# sound_stop() 4459 graph.parse_stats() 4460 graph.graph = mode.mode 4461 mode.draw_graph = True 4462 4463 elif symbol == key.F1 and mode.manual: 4464 if mode.back > 1: 4465 mode.back -= 1 4466 gameModeLabel.flash() 4467 spaceLabel.update() 4468 sessionInfoLabel.update() 4469 4470 elif symbol == key.F2 and mode.manual: 4471 mode.back += 1 4472 gameModeLabel.flash() 4473 spaceLabel.update() 4474 sessionInfoLabel.update() 4475 4476 elif symbol == key.F3 and mode.num_trials > 5 and mode.manual: 4477 mode.num_trials -= 5 4478 mode.num_trials_total = mode.num_trials + mode.num_trials_factor * \ 4479 mode.back ** mode.num_trials_exponent 4480 sessionInfoLabel.flash() 4481 4482 elif symbol == key.F4 and mode.manual: 4483 mode.num_trials += 5 4484 mode.num_trials_total = mode.num_trials + mode.num_trials_factor * \ 4485 mode.back ** mode.num_trials_exponent 4486 sessionInfoLabel.flash() 4487 4488 elif symbol == key.F5 and mode.manual: 4489 if mode.ticks_per_trial < TICKS_MAX: 4490 mode.ticks_per_trial += 1 4491 sessionInfoLabel.flash() 4492 4493 elif symbol == key.F6 and mode.manual: 4494 if mode.ticks_per_trial > TICKS_MIN: 4495 mode.ticks_per_trial -= 1 4496 sessionInfoLabel.flash() 4497 4498 elif symbol == key.C and (modifiers & key.MOD_CTRL): 4499 stats.clear() 4500 chartLabel.update() 4501 averageLabel.update() 4502 todayLabel.update() 4503 mode.progress = 0 4504 circles.update() 4505 4506 elif symbol == key.C: 4507 if cfg.JAEGGI_MODE: 4508 jaeggiWarningLabel.show() 4509 return 4510 GameSelect() 4511 4512 elif symbol == key.U: 4513 UserScreen() 4514 4515 elif symbol == key.I: 4516 if cfg.JAEGGI_MODE: 4517 jaeggiWarningLabel.show() 4518 return 4519 ImageSelect() 4520 4521 elif symbol == key.S: 4522 if cfg.JAEGGI_MODE: 4523 jaeggiWarningLabel.show() 4524 return 4525 SoundSelect() 4526 4527 elif symbol == key.W: 4528 webbrowser.open_new_tab(WEB_SITE) 4529 if update_available: 4530 window.on_close() 4531 4532 elif symbol == key.M: 4533 toggle_manual_mode() 4534 update_all_labels() 4535 mode.progress = 0 4536 circles.update() 4537 4538 elif symbol == key.H: 4539 webbrowser.open_new_tab(WEB_TUTORIAL) 4540 4541 elif symbol == key.D and not CLINICAL_MODE: 4542 webbrowser.open_new_tab(WEB_DONATE) 4543 4544 elif symbol == key.J and 'morse' in cfg.AUDIO1_SETS or 'morse' in cfg.AUDIO2_SETS: 4545 webbrowser.open_new_tab(WEB_MORSE) 4546 4547 4548 # these are the keys during a running session. 4549 elif mode.started: 4550 if (symbol == key.ESCAPE or symbol == key.X) and not CLINICAL_MODE: 4551 end_session(cancelled = True) 4552 4553 elif symbol == key.P and not CLINICAL_MODE: 4554 mode.paused = not mode.paused 4555 pausedLabel.update() 4556 field.crosshair_update() 4557 4558 elif symbol == key.F8 and not CLINICAL_MODE: 4559 mode.hide_text = not mode.hide_text 4560 update_all_labels() 4561 4562 elif mode.tick != 0 and mode.trial_number > 0: 4563 if 'arithmetic' in mode.modalities[mode.mode]: 4564 if symbol == key.BACKSPACE or symbol == key.DELETE: 4565 arithmeticAnswerLabel.reset_input() 4566 elif symbol == key.MINUS or symbol == key.NUM_SUBTRACT: 4567 arithmeticAnswerLabel.input('-') 4568 elif symbol == key.PERIOD or symbol == key.NUM_DECIMAL: 4569 arithmeticAnswerLabel.input('.') 4570 elif symbol == key._0 or symbol == key.NUM_0: 4571 arithmeticAnswerLabel.input('0') 4572 elif symbol == key._1 or symbol == key.NUM_1: 4573 arithmeticAnswerLabel.input('1') 4574 elif symbol == key._2 or symbol == key.NUM_2: 4575 arithmeticAnswerLabel.input('2') 4576 elif symbol == key._3 or symbol == key.NUM_3: 4577 arithmeticAnswerLabel.input('3') 4578 elif symbol == key._4 or symbol == key.NUM_4: 4579 arithmeticAnswerLabel.input('4') 4580 elif symbol == key._5 or symbol == key.NUM_5: 4581 arithmeticAnswerLabel.input('5') 4582 elif symbol == key._6 or symbol == key.NUM_6: 4583 arithmeticAnswerLabel.input('6') 4584 elif symbol == key._7 or symbol == key.NUM_7: 4585 arithmeticAnswerLabel.input('7') 4586 elif symbol == key._8 or symbol == key.NUM_8: 4587 arithmeticAnswerLabel.input('8') 4588 elif symbol == key._9 or symbol == key.NUM_9: 4589 arithmeticAnswerLabel.input('9') 4590 4591 4592 for k in mode.modalities[mode.mode]: 4593 if not k == 'arithmetic': 4594 keycode = cfg['KEY_%s' % k.upper()] 4595 if symbol == keycode: 4596 mode.inputs[k] = True 4597 mode.input_rts[k] = time.time() - mode.trial_starttime 4598 update_input_labels() 4599 4600 if symbol == cfg.KEY_ADVANCE and mode.flags[mode.mode]['selfpaced']: 4601 mode.tick = mode.ticks_per_trial-2 4602 4603 return pyglet.event.EVENT_HANDLED 4604# the loop where everything is drawn on the screen. 4605@window.event 4606def on_draw(): 4607 if mode.shrink_brain: 4608 return 4609 window.clear() 4610 if mode.draw_graph: 4611 graph.draw() 4612 elif mode.saccadic: 4613 saccadic.draw() 4614 elif mode.title_screen: 4615 brain_graphic.draw() 4616 titleMessageLabel.draw() 4617 titleKeysLabel.draw() 4618 else: 4619 batch.draw() 4620 if not mode.started and not CLINICAL_MODE: 4621 brain_icon.draw() 4622 logoUpperLabel.draw() 4623 logoLowerLabel.draw() 4624 for label in input_labels: 4625 label.draw() 4626 4627# the event timer loop. Runs every 1/10 second. This loop controls the session 4628# game logic. 4629# During each trial the tick goes from 1 to ticks_per_trial-1 then back to 0. 4630# tick = 1: Input from the last trial is saved. Input is reset. 4631# A new square appears and the sound cue plays. 4632# tick = 6: the square disappears. 4633# tick = ticks_per_trial - 1: tick is reset to 0. 4634# tick = 1: etc. 4635def update(dt): 4636 if mode.started and not mode.paused: # only run the timer during a game 4637 if (not mode.flags[mode.mode]['selfpaced'] or 4638 mode.tick > mode.ticks_per_trial-6 or 4639 mode.tick < 5): 4640 mode.tick += 1 4641 if mode.tick == 1: 4642 mode.show_missed = False 4643 if mode.trial_number > 0: 4644 stats.save_input() 4645 mode.trial_number += 1 4646 mode.trial_starttime = time.time() 4647 trialsRemainingLabel.update() 4648 if mode.trial_number > mode.num_trials_total: 4649 end_session() 4650 else: generate_stimulus() 4651 reset_input() 4652 # Hide square at either the 0.5 second mark or sooner 4653 positions = len([mod for mod in mode.modalities[mode.mode] if mod.startswith('position')]) 4654 positions = max(0, positions-1) 4655 if mode.tick == (6+positions) or mode.tick == mode.ticks_per_trial - 1: 4656 for visual in visuals: visual.hide() 4657 if mode.tick == mode.ticks_per_trial - 2: # display feedback for 200 ms 4658 mode.tick = 0 4659 mode.show_missed = True 4660 update_input_labels() 4661 if mode.tick == mode.ticks_per_trial: 4662 mode.tick = 0 4663pyglet.clock.schedule_interval(update, TICK_DURATION) 4664 4665angle = 0 4666def pulsate(dt): 4667 global angle 4668 if mode.started: return 4669 if not window.visible: return 4670 angle += 15 4671 if angle == 360: 4672 angle = 0 4673 r = 0 4674 g = 0 4675 b = 191 + min(64, int(80 * math.cos(math.radians(angle)))) 4676 spaceLabel.label.color = (r, g, b, 255) 4677#pyglet.clock.schedule_interval(pulsate, 1/20.) 4678 4679# 4680# --- END EVENT LOOP SECTION ---------------------------------------------- 4681# 4682 4683 4684batch = pyglet.graphics.Batch() 4685 4686try: 4687 test_polygon = batch.add(4, GL_QUADS, None, ('v2i', ( 4688 100, 100, 4689 100, 200, 4690 200, 200, 4691 200, 100)), 4692 ('c3B', (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))) 4693 test_polygon.delete() 4694except Exception as e: 4695 debug_msg(e) 4696 quit_with_error('Error creating test polygon. Full text of error:\n') 4697 4698# Instantiate the classes 4699mode = Mode() 4700field = Field() 4701visuals = [Visual() for i in range(4)] 4702stats = Stats() 4703graph = Graph() 4704circles = Circles() 4705saccadic = Saccadic() 4706 4707updateLabel = UpdateLabel() 4708gameModeLabel = GameModeLabel() 4709jaeggiWarningLabel = JaeggiWarningLabel() 4710keysListLabel = KeysListLabel() 4711logoUpperLabel = LogoUpperLabel() 4712logoLowerLabel = LogoLowerLabel() 4713titleMessageLabel = TitleMessageLabel() 4714titleKeysLabel = TitleKeysLabel() 4715pausedLabel = PausedLabel() 4716congratsLabel = CongratsLabel() 4717sessionInfoLabel = SessionInfoLabel() 4718thresholdLabel = ThresholdLabel() 4719spaceLabel = SpaceLabel() 4720analysisLabel = AnalysisLabel() 4721chartTitleLabel = ChartTitleLabel() 4722chartLabel = ChartLabel() 4723averageLabel = AverageLabel() 4724todayLabel = TodayLabel() 4725trialsRemainingLabel = TrialsRemainingLabel() 4726 4727arithmeticAnswerLabel = ArithmeticAnswerLabel() 4728input_labels = [] 4729 4730 4731# load last game mode 4732stats.initialize_session() 4733stats.parse_statsfile() 4734if len(stats.full_history) > 0 and not cfg.JAEGGI_MODE: 4735 mode.mode = stats.full_history[-1][1] 4736stats.retrieve_progress() 4737 4738update_all_labels() 4739 4740# Initialize brain sprite 4741brain_icon = pyglet.sprite.Sprite(pyglet.image.load(random.choice(resourcepaths['misc']['brain']))) 4742brain_icon.position = (field.center_x - brain_icon.width//2, 4743 field.center_y - brain_icon.height//2) 4744if cfg.BLACK_BACKGROUND: 4745 brain_graphic = pyglet.sprite.Sprite(pyglet.image.load(random.choice(resourcepaths['misc']['splash-black']))) 4746else: 4747 brain_graphic = pyglet.sprite.Sprite(pyglet.image.load(random.choice(resourcepaths['misc']['splash']))) 4748brain_graphic.position = (field.center_x - brain_graphic.width//2, 4749 field.center_y - brain_graphic.height//2 + 40) 4750def scale_brain(dt): 4751 brain_graphic.scale = dt 4752 brain_graphic.x = field.center_x - brain_graphic.image.width//2 + scale_to_width(2) + (brain_graphic.image.width - brain_graphic.width) // 2 4753 brain_graphic.y = field.center_y - brain_graphic.image.height//2 + scale_to_height(60) + (brain_graphic.image.height - brain_graphic.height) // 2 4754 window.clear() 4755 brain_graphic.draw() 4756 if brain_graphic.width < 56: 4757 mode.shrink_brain = False 4758 pyglet.clock.unschedule(shrink_brain) 4759 brain_graphic.scale = 1 4760 brain_graphic.position = (field.center_x - brain_graphic.width//2, 4761 field.center_y - brain_graphic.height//2 + 40) 4762 4763scale_brain(scale_to_width(1)) 4764# If we had messages queued during loading (like from moving our data files), display them now 4765messagequeue.reverse() 4766for msg in messagequeue: 4767 Message(msg) 4768 4769# start the event loops! 4770if __name__ == '__main__': 4771 4772 pyglet.app.run() 4773 4774# nothing below the line "pyglet.app.run()" will be executed until the 4775# window is closed or ESC is pressed. 4776