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...

<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>
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 ", " ", "(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 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.++<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 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> <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>+<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 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.