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