diff --git a/habit.m b/habit.m index 72bbc89..bbc0091 100644 --- a/habit.m +++ b/habit.m @@ -8,15 +8,20 @@ system = load_system(varargin{:}); timing = load_events(varargin{:}); system.w = setup_screen(varargin{:}); + system.pos = setup_pos(system.w, varargin{:}); system.tex = load_textures(system.w, varargin{:}); - %% + %% instructions + [onset, output] = instructions(system, 1); + + %% start timing and data collection record(length(timing)) = struct(); system.starttime = GetSecs(); + %% run through events for i=1:length(timing) t = timing(i); - [onset, output] = t.func(system, t); + [onset, output] = t.func(system, t, record); record(i).event_name = t.event_name; record(i).output = output; record(i).onset = onset; @@ -25,6 +30,6 @@ info.record = record; info.system = system; - Screen('CloseAll'); + closedown(); end diff --git a/private/choice.m b/private/choice.m new file mode 100644 index 0000000..378bdae --- /dev/null +++ b/private/choice.m @@ -0,0 +1,33 @@ +function [onset, output] = choice(system, t, varargin) + + ideal = GetSecs()+t.onset; + Screen('DrawTexture', system.w, system.tex.ocean_bottom); + Screen('DrawTexture', system.w, system.tex.astronaut{1,1}); + + %% positon choice options + chest_w = 27;chest_h = 27; %TODO: use sprite + % TODO: use DrawTextures (many at once) + Screen('DrawTexture', system.w, system.tex.chest,... + [], [ system.pos.left.x system.pos.left.y system.pos.left.x+chest_w system.pos.left.y+chest_h] ); + Screen('DrawTexture', system.w, system.tex.chest,... + [], [ system.pos.up.x system.pos.up.y system.pos.up.x+chest_w system.pos.up.y+chest_h] ); + Screen('DrawTexture', system.w, system.tex.chest,... + [], [ system.pos.right.x system.pos.right.y system.pos.right.x+chest_w system.pos.right.y+chest_h] ); + + onset = Screen('Flip', system.w, ideal); + + keys = KbName({'Left','Up', 'Right'}); + [k rt] = waitForKeys(keys, onset + t.max_rt); + if rt > 0 + idx = find(keys == k,1); + well_prob = t.chance(idx); + output.score = (rand(1) <= well_prob); + else + output.score = 0 + end + oupput.onset_ideal = ideal; + output.key = k; + output.rt = rt; + + % TODO: animate avatar walk in while loop? +end diff --git a/private/closedown.m b/private/closedown.m new file mode 100644 index 0000000..ea32f1c --- /dev/null +++ b/private/closedown.m @@ -0,0 +1,13 @@ +function closedown() +% CLOSEDOWN - clean up PTB connections +% originally from https://github.com/LabNeuroCogDevel/froggerRPG/blob/master/private/closedown.m + ListenChar(0); + ShowCursor; + Screen('CloseAll'); + Priority(0); + sca; + diary off; + close all; + %openPTBSnd('close'); + % TODO: close DAQ? +end diff --git a/private/escclose.m b/private/escclose.m new file mode 100644 index 0000000..cc698b8 --- /dev/null +++ b/private/escclose.m @@ -0,0 +1,6 @@ +function escclose(keyCode) + if keyCode(KbName('escape')) + closedown() + error('early exit') + end +end diff --git a/private/feedback.m b/private/feedback.m new file mode 100644 index 0000000..9380b83 --- /dev/null +++ b/private/feedback.m @@ -0,0 +1,21 @@ +function [onset, output] = feedback(system, t, record) + ideal = t.onset + GetSecs(); % relative time + + Screen('DrawTexture', system.w, system.tex.ocean_bottom); + % reach back in time and find the previous choice event + choice = record(t.i-2).output, + if choice.score + msg = 'REWARD!' + else + msg = 'NO REWARD!' + end + + % TODO: find chosen direction using choice.key + % TODO: show open/closed chest over choice location + % TODO: animate chest sprite + DrawFormattedText(system.w, msg ,... + 'center','center', [255,255,255]); + + onset = Screen('Flip', system.w, ideal); + output.ideal = ideal; +end diff --git a/private/fixation.m b/private/fixation.m index 6191322..d573bfb 100644 --- a/private/fixation.m +++ b/private/fixation.m @@ -1,11 +1,12 @@ -function [onset, output] = fixation(system, t) +function [onset, output] = fixation(system, t, varargin) % show the background Screen('DrawTexture', system.w, system.tex.ocean_bottom); DrawFormattedText(system.w, '+' ,'center','center', t.cross_color); - % run - onset = t.onset + system.starttime; - onset = Screen('Flip', system.w, onset); - output = []; + %onset = t.onset + system.starttime; % abs time + ideal = t.onset + GetSecs(); % relative time + onset = Screen('Flip', system.w, ideal); + output.onset_ideal = ideal; + output.onset = onset; end diff --git a/private/instructions.m b/private/instructions.m index c13d751..26e28ca 100644 --- a/private/instructions.m +++ b/private/instructions.m @@ -1,11 +1,14 @@ -function [onset, output] = instructions(system, number, t) +function [onset, output] = instructions(system, number, varargin) output.instruction = number; - output.msg = 'example output'; % show the background Screen('DrawTexture', system.w, system.tex.ocean_bottom); - Screen('DrawTexture', system.w, system.tex.chest); + %Screen('DrawTexture', system.w, system.tex.chest); + DrawFormattedText(system.w, 'Push any key to start' ,... + 'center','center', [255,255,255]); % run - onset = Screen('Flip', system.w, t.onset); + onset = Screen('Flip', system.w, 0); + acceptkeys = KbName({'Space','Left','Up','Right','Return','1'}); + [output.k output.rt] = waitForKeys(acceptkeys,Inf); end diff --git a/private/load_events.m b/private/load_events.m index 2aad41f..0e2ca65 100644 --- a/private/load_events.m +++ b/private/load_events.m @@ -3,17 +3,34 @@ % TODO: programaticly set % heavy lifting done by .func - timing(1).event_name = 'instruct1'; - timing(1).func = @(system, varargin) instructions(system, 1, varargin{:}); - timing(1).onset = 0; + i = 1; + timing(i).event_name = 'choice'; + timing(i).func = @choice; + timing(i).chance = [1,1,1]; % left, up, right + timing(i).max_rt = 2; + timing(i).i = i; - timing(2).event_name = 'iti'; - timing(2).onset = 5; - timing(2).cross_color = [255,255,255]; % white - timing(2).func = @fixation; + i=i+1; + timing(i).event_name = 'isi'; + timing(i).dur = 2; + timing(i).cross_color = [0,0,255];%blue + timing(i).func = @fixation; + timing(i).onset=0; % as soon as choice ends + timing(i).i = i; + + i=i+1; + timing(i).event_name = 'feedback'; + timing(i).dur = 2; + timing(i).func = @feedback; + timing(i).onset=timing(i-1).dur + timing(i).i = i; + + i=i+1; + timing(i).event_name = 'iti'; + timing(i).dur = 3; + timing(i).cross_color = [255,255,255]; % white + timing(i).func = @fixation; + timing(i).onset=timing(i-1).dur + timing(i).i = i; - timing(3).event_name = 'isi'; - timing(3).onset = 10; - timing(3).cross_color = [0,0,255];%blue - timing(3).func = @fixation; end diff --git a/private/load_textures.m b/private/load_textures.m index 1a4cd29..db3a62d 100644 --- a/private/load_textures.m +++ b/private/load_textures.m @@ -3,12 +3,31 @@ fprintf('# loading textures\n'); imgs = [dir('out/imgs/*.png')]; + avatar_sprites = {'astronaut', 'shark','lizard_blue', 'alient_green'}; + well_sprites = {'chest_sprites', 'wellcoin_sprites'}; for f = imgs' - [~, fname, ~] = fileparts(f.name); + [~, fname, ~] = fileparts(f.name); fname(fname=='-')='_'; [img, ~, alpha] = imread(fullfile(f.folder,f.name)); img(:,:,4) = alpha; - % TODO: adjust images to screen size - tex_struct.(fname) = Screen('MakeTexture',w, img); + % TODO: scale images to screen size + + % sprites within one file. have animations frames in a grid + if ismember(fname, avatar_sprites) + % 4 rows 3 cols + sprites = cell(4,4); + [x,y,_] =size(img); + h= floor(y/4); + wd = floor(x/4); + for i=1:4; for j=1:4; + ii=(i-1)*h+1; jj=(j-1)*h+1; + img_sub = img(jj:(jj+wd-1),ii:(ii+h-1),:); + tmp_tex = Screen('MakeTexture',w, img_sub); + sprites(i,j) = tmp_tex; + end;end + tex_struct.(fname) = sprites; + else + tex_struct.(fname) = Screen('MakeTexture',w, img); + end end end diff --git a/private/setup_pos.m b/private/setup_pos.m new file mode 100644 index 0000000..d62f85d --- /dev/null +++ b/private/setup_pos.m @@ -0,0 +1,10 @@ +function pos = setup_pos(w, varargin); + % TODO: use screen size? + pos.left.x=10; + pos.left.y=400; + + pos.up.x=200; + pos.up.y=200; + + pos.right.x=400; + pos.right.y=400; diff --git a/private/waitForKeys.m b/private/waitForKeys.m new file mode 100644 index 0000000..8a129b0 --- /dev/null +++ b/private/waitForKeys.m @@ -0,0 +1,43 @@ +function [key,RT] = waitForKey(acceptkeysidx,timeout) +% WAITFORKEY -- wait for given key index (e.g from KbName()) until timeout +% return key=0 and RT=-Inf if no acceptable key push given before timeout +% originally from https://github.com/LabNeuroCogDevel/audodd/blob/master/waitForKey.m +% +% Usage: +% % 2 seconds to respond a (return k=1) or b (return k=2) +% % no response, k=0, rt=-Inf +% [k rt] = waitForKey( KbName({'a','b'}), GetSecs() + 2); +% +% % wait forever for the scanner to send a trigger (trg key is = ) +% [k rt] = waitForKey( KbName({'=+'}), Inf); +% +% Note: +% accepts multiple keys down as response endorsing first acceptable key + + + % no response can be meaningful if too fast + % wait 60ms before trying to record a response + % TODO: determine more approprate min response time limit + % also reduce hold over from previous trial errors + fastestResponseTime=.06; + WaitSecs(fastestResponseTime); + + key=0; + RT=-Inf; + % continue until we get a keypress we like + % or we run out of time + while(RT<=0 && GetSecs() < timeout ) + [keyPressed, thisRT, keyCode] = KbCheck; + if keyPressed && any( keyCode(acceptkeysidx) ) + RT=thisRT; + + %NB! we only pick the first (in order of acceptkeys) + % if subject is holding down more than one key, will not record both! + % will warn to screen about mutliple pushes though + key=find(keyCode(acceptkeysidx) ,1); + if(nnz(keyCode(acceptkeysidx) ) > 1) + fprintf('WARNING: multple good button pushes! reporting first button is pushed\n'); + end + end + end +end