DMDX Help.
Corsi Block Tapping Task Example.
So someone asked about doing a Corsi
Block Tapping task with DMDX and my mind being a terrier with a new bone to chew
on wouldn't stop thinking about it so here we are.
This task presents a
couple of new challenges beyond the obvious interesting stuff like placing nine
block targets on the screen randomly without having them overlap and doing so in
a fashion that has a linear time function (as opposed to just trying and trying
and trying till you get all nine not overlapping), both
of which are relatively straight forward DMDX arcanery, however once you get to
actually displaying procedurally generated targets independent of the screen's
dimensions one runs slap dab into the fact that you're trying to use integer
counters to represent fractional entities as when you're positioning things on
the screen in a fashion that's going to work on any display you're using values between zero and one. Great, we've crossed
that bridge before, you just multiply all your values by 100 and use macros with
a period in front of them and then (ugh) of course we've got to put leading
zeroes in there for values less than 0.10 for 18 different values, yuck.
Which I was happy enough doing until I realized that when I want to go ahead and
feed the coordinates of those targets to <2Did>
(more on that later as that's another modification) I've got to put leading zeroes
on another 36 of the things... Soo we came up with another macro command
FXPSET
that takes the hard work out of putting leading zeroes on fractional
macro components. And then of course we've got all these targets moving
around the place from one presentation sequence to the next and we need some way
of talking to <2Did> to update it's button
locations on the screen so we came up with
<setbuttonrect> that
allows us to update the rects that were provided in the original <2Did>
definition. BTW, if for some reason the listing here gets mutilated those
targets it's displaying are the Unicode black square, U+25A0.
There's another version of this task in the
<mip> documentation that
hides the mouse cursor during the sequence demonstration and additionally
centers the mouse at the start of every trial.
So before anything else happens we set up a couple of operational
parameters that determine how large and how close the blocks are on the screen,
macros .hwb., .fm.
and .mpx.. Basically if you decide you
want to change the sizes first off you'll choose a font multiplier in
.fm. first and then determine how large that is
on the screen in terms of a percentage of the screen width in
.hwb. (actually half the width) and then
determine how close you'd like them to get by setting the minimum center to
center proximity with .mpx.
(again in a percentage of the screen width). After that the general
structure of our item file is similar to a few other fancy procedural item files
such as the Anagram task
and the N-back task
(although the N-back task has more frippery around that central procedure)
in that there are a number of items that set up parameters for calls to a common
subroutine (at item 999 here), this task's two parameters being one, the item number
that should be doing the actual gathering of data that will appear in the output
file and two, the number of blocks in the series to remember. In this task
because up to nine RTs can be gathered per call of the subroutine our subroutine
will increment that item number as many times as it needs to gather the RTs (so
the first call with .nb. set to 2 and
.itm. set to 200 will generate data for items 200 and 201).
After that we have our general purpose routine that first off sets up
the positioning of the targets in a non-overlapping fashion. The trouble
with starting with a blank screen and trying random coordinates willy nilly is
that the time taken to complete positioning nine different blocks isn't finite,
it's perfectly conceivable to have the random number generator keep on producing
coordinates that overlap with another target and while it may not take forever
to complete indeterminate numbers of seconds are not beyond of the realms of
possibility. Here I
chose to put the nine targets on the screen in a 3x3 evenly spaced grid on the
screen and then to distort their positions by relatively small amounts a fixed
number of times. While this involves thousands of operations they execute very
rapidly on today's machines (unless of course
Branch Diagnostics has been
turned on in which case it's really slow and you want to dial back the number of
iterations -- in fact it was so slow I did something I rarely do and that is
after I got this section of the item file working and needed to debug the next
one I only turned branch diagnostics on after this section and turned them off
again when the function returned so as to only have them on in sections of code
that were yet to be debugged instead of the whole item file). So we have to represent the coordinates of of our nine blocks
and we use an array of counters (100 to 117) to do this which runs us into the
conundrum of having to represent values of screen coordinates between zero and
one with counters that are integers. Easiest way to do that is use a fixed
point representation and here we're going to define the the two least
significant digits as being the fractional component and when we pass them to
DMDX's routines we'll use the aforementioned
FXPSET
function to render the counters as fixed point real values. Basically from
item 1000 to the item following 1100 we're adding smallish random offsets to the
X and Y coordinates of a block, checking if it went off the screen (items 1100 and
shortly thereafter) and then after that we're checking whether it got too close
to any other blocks and if so we discard those new coordinates. Next up we
set up the 18 macros that will be passed to
<xy> to display the blocks, two for each
block, an X and a Y, .b1x. through
.b9y..
After the block positions have been determined we need to determine the
order the blocks will flash in around item 1200. Here we're using counters
120 through 128 each storing a block number and then we swap every counter with
another thus randomizing it's order. Having done that we're ready to
display the sequence the subject will have to repeat in items 1300 through 1400
and we do that by having a writing color switch for each block that either gets
set to blue or yellow if it's the currently selected block. In the items
around 1500 we're updating the positions the blocks have been moved to with
<macro fxpset> and
<sbr> so that the
subject can either tap on them (if the
<!2Did windowstouch>
keyword in the parameters has been uncommented) or click on them with the mouse.
Lastly in items 1600 through 1999 we present the blocks starting off
all blue and mapping the positive response for the correct block (all others
being negative). There are a few special considerations here, one, unlike a
normal DMDX RT gathering paradigm we don't want to miss input as we're
assembling the next display in the sequence so we're using a macro
.co. that's either set to the usual clock on
<co> to start with and then to the
continue clock on keyword
<cco> that will catch the input in between
items. Secondly the timing is rather special so I'm using another macro
.dly. that's either going to use the usual DMDX
delay between items when it starts off (a
half second or so) and then switch to a delay of two ticks thereafter to make
the display immediately follow the user's input. Which brings us to the third
"interesting" thing here and that is providing feedback for the user in that
when they click on the right button we want to display that in yellow
(1710-1713) the next time through our RT gathering item. Rather
complex but totally needed to give the user a nice feel as they're doing the
task. Didn't think the rest of it was going to be any easier than it was
but the amount of work required to get the timing right and feedback feeling good was a bit of a surprise.
NOTE: As I was developing this script I'd accidentally included two
definitions of block1 in the
<2Did> definition
which led to some particularly "interesting" behavior and it wasn't until I'd
turned on <testmode 10>
(in conjunction with some passpoint diagnostics because I was concerned I'd
botched the newly added
<sbr> code and
there was also some concern about using
<2Did> with
<cco>) that I finally noticed the
duplication error. What was happening was that every now and then I could
swear DMDX was ignoring a correct click on a block, but because the task has a
relatively low timeout (the default 4 seconds any job has) and in it's later
stages becomes particularly challenging I couldn't been 100% sure.
Eventually there was enough of a question that I figured I should probe the
script in some detail so instead of displaying a block for each target I changed
a test version of the script to display the block number and I noticed that
sometimes block 4 was missed (once it was block 5), sometimes it was only half
of block 4 that was missed and eventually I realized the region that was suspect
was over on the left of the screen and near the top and that was looking an
awful lot like the
.1,.2,.3,.4
original region definition -- but block 1 which was often in the same area was
always fine. Which was really curious as as far as I could tell all
instances of
.1,.2,.3,.4 must
have been overwritten once the task was running -- but it tipped me
off to include
checkoverlapping in
the
<2Did> definition
and sure enough everything started working 100% of the time. What was
happening was that because there were two block 1 definitions the
<sbr> code was
updating the first and leaving the second alone and as the
<2Did> code was
processing regions looking for hits if block 4 or 5 happened to be all the way
over on the left and above the middle of the screen the second remaining
spurious
block1,.1,.2,.3,.4
definition could soak up that hit and the other block wouldn't be clicked on and
of course
checkoverlapping
fixed that as the code no longer stopped once it found one hit and went on to
see the block 4 region and deliver that hit as well (because
<mpr> and <mnr>
code similarly stops once it finds a matching button definition the second block
1 definition was never a valid response as the code differentiates between
identically named regions and DMDX never considered the second block 1 region a
valid response). Moral of the story is that
checkoverlapping is
your friend, these days with CPUs being so stupid fast there's little cost
including it just to be safe...
<ep>
<id #keyboard> <vm
desktop> <!branchdiagnostics> <cr>
<fd 20> <!default frame duration specifies
how long each block in a pattern is displayed for>
<bgc 0,0,0> <dwc
255,255,255> <nfb>
<2Did mouse checkoverlapping block1,.1,.2,.3,.4
block2,.1,.2,.3,.4 block3,.1,.2,.3,.4 block4,.1,.2,.3,.4 block5,.1,.2,.3,.4
block6,.1,.2,.3,.4 block7,.1,.2,.3,.4 block8,.1,.2,.3,.4 block9,.1,.2,.3,.4> <!coords
of buttons are dummys as we update them later>
<!2Did windowstouch
checkoverlapping block1,.1,.2,.3,.4 block2,.1,.2,.3,.4 block3,.1,.2,.3,.4
block4,.1,.2,.3,.4 block5,.1,.2,.3,.4 block6,.1,.2,.3,.4 block7,.1,.2,.3,.4
block8,.1,.2,.3,.4 block9,.1,.2,.3,.4>
<inst hardmargin>
<eop>
0 <inst
.1, .1, .95>
"This ", "is ", "a ", "corsi block tapping task. ", "It ",
"should ", "be ", "run ", "with ", "the ", "Unicode ", "option ", "on ",
"otherwise ", "the ", "target ", "boxes ", "won't ", "be ", "squares ", "but ",
"will ", "instead ", "be ", "vertical ", "bars ", "(|) ", "and ", "it ", "also
", "requires ", "DMDX ", "version ", "6.0.1.0
", "(or
", "later).";
~1 <xyjustification center>
<macro .hwb. 6> <! half the width of a block on the screen for hit detection
* 100>
<macro .fm. 4> <! fontmultipliers to achieve said size when using
■ to
draw a block>
<macro .mpx. 15> <! minimum proximity center to center of
blocks * 100>;
!these generate the items that perform the task, the item
number in .itm. will be the item that gathers RTs in the output, there will be
more than one of them for the tasks where the number of blocks (.nb.) is more
than one and they will ascend by one for each RT gathered;
!~1 <macro .itm.
100> <macro .nb. 1> <call 999>;
~1 <macro .itm. 200> <macro .nb. 2> <call
999>;
~1 <macro .itm. 210> <macro .nb. 2> <call 999>;
~1 <macro .itm. 220>
<macro .nb. 2> <call 999>;
~1 <macro .itm. 300> <macro .nb. 3> <call 999>;
~1 <macro .itm. 310> <macro .nb. 3> <call 999>;
~1 <macro .itm. 320> <macro .nb.
3> <call 999>;
~1 <macro .itm. 400> <macro .nb. 4> <call 999>;
~1 <macro .itm.
410> <macro .nb. 4> <call 999>;
~1 <macro .itm. 420> <macro .nb. 4> <call
999>;
~1 <macro .itm. 500> <macro .nb. 5> <call 999>;
~1 <macro .itm. 510>
<macro .nb. 5> <call 999>;
~1 <macro .itm. 520> <macro .nb. 5> <call 999>;
~1 <macro .itm. 600> <macro .nb. 6> <call 999>;
~1 <macro .itm. 610> <macro .nb.
6> <call 999>;
~1 <macro .itm. 620> <macro .nb. 6> <call 999>;
~1 <macro .itm.
700> <macro .nb. 7> <call 999>;
~1 <macro .itm. 710> <macro .nb. 7> <call
999>;
~1 <macro .itm. 720> <macro .nb. 7> <call 999>;
~1 <macro .itm. 800>
<macro .nb. 8> <call 999>;
~1 <macro .itm. 810> <macro .nb. 8> <call 999>;
~1 <macro .itm. 820> <macro .nb. 8> <call 999>;
~1 <macro .itm. 900> <macro .nb.
9> <call 999>;
~1 <macro .itm. 910> <macro .nb. 9> <call 999>;
~1 <macro .itm.
920> <macro .nb. 9> <call 999>;
0 "Done." <lastframe>;
~999 <!so first
up we'll put our blocks in a grid (screen pos * 100)>
<set c100=25> <set
c101=25>
<set c102=50> <set c103=25>
<set c104=75> <set c105=25>
<set
c106=25> <set c107=50>
<set c108=50> <set c109=50>
<set c110=75> <set
c111=50>
<set c112=25> <set c113=75>
<set c114=50> <set c115=75>
<set
c116=75> <set c117=75>
<set c1=12> <!number of deformation iterations, make
it 2 when branchdiagnostics is on because this can take a while, otherwise 10 or
so looks good>;
~1000 <set c2=0> <!counter for deforming each block>;
~1001 <macro set .nxi. = 100 + 2 * c2>
<macro set .nyi. = 101 + 2 * c2>;
~1 <macro set .nx. = c~.nxi. + (random 2 * ~.mpx.) - ~.mpx.>
<macro set .ny.
= c~.nyi. + (random 2 * ~.mpx.) - ~.mpx.>;
~1 <bi 1100, ~.nx. .lt. ~.mpx. /
2> <!see if it's off the screen>;
~1 <bi 1100, ~.ny. .lt. ~.mpx. / 2>;
~1
<bi 1100, ~.nx. .gt. 100 - ~.mpx. / 2>;
~1 <bi 1100, ~.ny. .gt. 100 - ~.mpx.
/ 2>;
~1 <set c3=0> <!see if it's too close to any other block>;
~1010 <bi
1020, c2 .eq. c3>;
~1 <macro set .xi. = 100 + 2 * c3>
<macro set .yi. =
101 + 2 * c3>;
~1 <bi 1020, (abs ~.nx. - c~.xi.) .gt. ~.mpx.>;
~1 <bi
1020, (abs ~.ny. - c~.yi.) .gt. ~.mpx.>;
~1 <bu 1100> <!too close to another
block so discard this iteration>;
~1020 <inc c3> <bi -1010, c3 .lt. 9> <!keep
looping till compared all blocks>;
~1 <set c~.nxi. = ~.nx.> <!doesn't overlap
so store new cords>
<set c~.nyi. = ~.ny.>;
~1100 <inc c2> <bi -1001, c2
.lt. 9> <!keep looping till deformed all blocks>;
~1 <dec c1> <bi -1000, c1 .gt.
0> <!keep deforming stuff but good>;
~1 <!because xy can't take counters
gonna load all those in macros (not to mention being unable to represent numbers
less than 1 with counters and having to put leading zeroes on integers less than
10, ho hum)>
<macro fxpset 2 .b1x. = c100> <macro fxpset 2 .b1y. = c101>
<macro fxpset 2 .b2x. = c102> <macro fxpset 2 .b2y. = c103>
<macro fxpset 2
.b3x. = c104> <macro fxpset 2 .b3y. = c105>
<macro fxpset 2 .b4x. = c106>
<macro fxpset 2 .b4y. = c107>
<macro fxpset 2 .b5x. = c108> <macro fxpset 2
.b5y. = c109>
<macro fxpset 2 .b6x. = c110> <macro fxpset 2 .b6y. = c111>
<macro fxpset 2 .b7x. = c112> <macro fxpset 2 .b7y. = c113>
<macro
fxpset 2 .b8x. = c114> <macro fxpset 2 .b8y. = c115>
<macro fxpset 2 .b9x. =
c116> <macro fxpset 2 .b9y. = c117>
<set c120=0> <set c121=1> <set c122=2>
<set c123=3> <set c124=4> <set c125=5> <set c126=6> <set c127=7> <set c128=8>
<set c1=0>
<!this is our list of blocks to flash, next we shuffle it>;
~1200 <macro set i=120 + (random 9)> <macro set j=120 + c1>;
~1 <set c2=c~j>
<set c~j = c~i> <set c~i = c2> <inc c1> <bi -1200, c1 .lt. 9>;
1 <!blank
screen before> / <set c1=0> <!next flash each block up to .nb.>;
~1300 <macro
.wc1. 0,0,255> <macro .wc2. 0,0,255> <macro .wc3. 0,0,255> <macro .wc4. 0,0,255>
<macro .wc5. 0,0,255> <macro .wc6. 0,0,255> <macro .wc7. 0,0,255> <macro .wc8.
0,0,255> <macro .wc9. 0,0,255> <macro set i = 120 + c1>;
~1 <set c2 = 1310 +
c~i>;
~1 <ib c2> <!make our selected block yellow, all others already set
blue>;
~1310 <macro .wc1. 255,255,0> <bu 1400>;
~1311 <macro .wc2.
255,255,0> <bu 1400>;
~1312 <macro .wc3. 255,255,0> <bu 1400>;
~1313
<macro .wc4. 255,255,0> <bu 1400>;
~1314 <macro .wc5. 255,255,0> <bu 1400>;
~1315 <macro .wc6. 255,255,0> <bu 1400>;
~1316 <macro .wc7. 255,255,0> <bu
1400>;
~1317 <macro .wc8. 255,255,0> <bu 1400>;
~1318 <macro .wc9.
255,255,0>;
1400 <dfm ~.fm. STAT> <wc ~.wc1.> <xy ~.b1x., ~.b1y.> "■", <wc
~.wc2.> <xy ~.b2x., ~.b2y.> "■", <wc ~.wc3.> <xy ~.b3x., ~.b3y.> "■", <wc
~.wc4.> <xy ~.b4x., ~.b4y.> "■", <wc ~.wc5.> <xy ~.b5x., ~.b5y.> "■", <wc
~.wc6.> <xy ~.b6x., ~.b6y.> "■", <wc ~.wc7.> <xy ~.b7x., ~.b7y.> "■", <wc
~.wc8.> <xy ~.b8x., ~.b8y.> "■", <wc ~.wc9.> <xy ~.b9x., ~.b9y.> "■" / !;
~1
<inc c1> <bi -1300, c1 .lt. ~.nb.> <!keep flashing the requested number of
blocks>;
~1 <!branchdiagnostics> <set c1= 0> <!set the rects of blocks>;
~1500 <macro set .nxi. = 100 + 2 * c1>
<macro set .nyi. = 101 + 2 * c1>;
~1 <!build macros for left, top, right, bottom rect>
<macro fxpset 2 l = c~.nxi.
- ~.hwb.> <macro fxpset 2 t = c~.nyi. - ~.hwb.>
<macro fxpset 2 r = c~.nxi.
+ ~.hwb.> <macro fxpset 2 b = c~.nyi. + ~.hwb.>
<macro set i=c1 + 1>;
~1 <sbr
+block~i, ~l, ~t, ~r, ~b> <inc c1> <bi -1500, c1 .lt. 9> <!keep looping till
updated all block rects>;
~1 <set c1 = 0> m.dly.++ m.co.+<co>+ <!use co/cco
so we don't miss a response between items as it's a bit nebulous when an RT is
appropriate> <macro .wc1. 0,0,255> <macro .wc2. 0,0,255> <macro .wc3. 0,0,255>
<macro .wc4. 0,0,255> <macro .wc5. 0,0,255> <macro .wc6. 0,0,255> <macro .wc7.
0,0,255> <macro .wc8. 0,0,255> <macro .wc9. 0,0,255> <!loop around
mapping correct response from c120..8>;
~1600 <umpr> <umnr> <macro set i=120
+ c1> <set c2=0>;
~1610 <macro set b=c2 + 1>;
~1 <bi 1612, c~i .eq. c2>;
~1 <mnr +block~b> <bu 1613>;
~1612 <mpr +block~b>;
~1613 <inc c2> <bi
-1610, c2 .lt. 9>;
+~.itm. ~.dly. <dfm ~.fm. STAT> <wc ~.wc1.> <xy ~.b1x.,
~.b1y.> "■", <wc ~.wc2.> <xy ~.b2x., ~.b2y.> "■", <wc ~.wc3.> <xy ~.b3x.,
~.b3y.> "■", <wc ~.wc4.> <xy ~.b4x., ~.b4y.> "■", <wc ~.wc5.> <xy ~.b5x.,
~.b5y.> "■", <wc ~.wc6.> <xy ~.b6x., ~.b6y.> "■", <wc ~.wc7.> <xy ~.b7x.,
~.b7y.> "■", <wc ~.wc8.> <xy ~.b8x., ~.b8y.> "■", <wc ~.wc9.> <xy ~.b9x.,
~.b9y.> "■" <dfm 1.0> ~.co. <biw 1999>;
~1 <!provide some positive feedback
by drawing the box yellow next time around> <macro set i=120 + c1> <set c2=0>;
~1710 <macro set b=c2 + 1>;
~1 <bi 1712, c~i .eq. c2>;
~1 <macro .wc~b.
0,0,255> <bu 1713>;
~1712 <macro .wc~b. 255,255,0>;
~1713 <inc c2> <bi
-1710, c2 .lt. 9>;
~1 m.dly.+<delay 2>+ m.co.+<cco>+ <macro yikwid> <macro
set .itm. = ~.itm. + 1> <inc c1> <bi -1600, c1 .lt. ~.nb.> <!keep testing the
requested number of blocks>;
1 <!if they got it right provide some closure by
drawing the last box yellow> <delay 2> <dfm ~.fm. STAT> <wc ~.wc1.> <xy ~.b1x.,
~.b1y.> "■", <wc ~.wc2.> <xy ~.b2x., ~.b2y.> "■", <wc ~.wc3.> <xy ~.b3x.,
~.b3y.> "■", <wc ~.wc4.> <xy ~.b4x., ~.b4y.> "■", <wc ~.wc5.> <xy ~.b5x.,
~.b5y.> "■", <wc ~.wc6.> <xy ~.b6x., ~.b6y.> "■", <wc ~.wc7.> <xy ~.b7x.,
~.b7y.> "■", <wc ~.wc8.> <xy ~.b8x., ~.b8y.> "■", <wc ~.wc9.> <xy ~.b9x.,
~.b9y.> "■" <dfm 1.0> / ;
~1999 <return> <branchdiagnostics 0>;
0
"oops...";
DMDX Index.