@m2c2kit/assessment-color-shapes 0.8.30 → 0.8.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,1092 @@
1
- import { Game, Sprite, Scene, M2Error, WebColors, Transition, RandomDraws, Shape, Label, Action, Timer, Easings, TransitionDirection } from '@m2c2kit/core';
1
+ import { Game, RandomDraws, Sprite, Scene, M2Error as M2Error$1, WebColors, Transition, Shape, Label, Action, Timer, Easings, TransitionDirection } from '@m2c2kit/core';
2
2
  import { LocalePicker, Instructions, CountdownScene, Grid, Button } from '@m2c2kit/addons';
3
3
 
4
+ class M2Error extends Error {
5
+ constructor(...params) {
6
+ super(...params);
7
+ this.name = "M2Error";
8
+ Object.setPrototypeOf(this, M2Error.prototype);
9
+ if (Error.captureStackTrace) {
10
+ Error.captureStackTrace(this, M2Error);
11
+ }
12
+ }
13
+ }
14
+ class DataCalc {
15
+ /**
16
+ * A class for transformation and calculation of m2c2kit data.
17
+ *
18
+ * @remarks The purpose is to provide a simple and intuitive interface for
19
+ * assessments to score and summarize their own data. It is not meant for
20
+ * data analysis or statistical modeling. The idiomatic approach is based on the
21
+ * dplyr R package.
22
+ *
23
+ * @param data - An array of observations, where each observation is a set of
24
+ * key-value pairs of variable names and values.
25
+ * @param options - Options, such as groups to group the data by
26
+ * @example
27
+ * ```js
28
+ * const dc = new DataCalc(gameData.trials);
29
+ * const mean_response_time_correct_trials = dc
30
+ * .filter((obs) => obs.correct_response_index === obs.user_response_index)
31
+ * .summarize({ mean_rt: mean("response_time_duration_ms") })
32
+ * .pull("mean_rt");
33
+ * ```
34
+ */
35
+ constructor(data, options) {
36
+ this._groups = new Array();
37
+ if (!Array.isArray(data)) {
38
+ throw new M2Error(
39
+ "DataCalc constructor expects an array of observations as first argument"
40
+ );
41
+ }
42
+ for (let i = 0; i < data.length; i++) {
43
+ if (data[i] === null || typeof data[i] !== "object" || Array.isArray(data[i])) {
44
+ throw new M2Error(
45
+ `DataCalc constructor expects all elements to be objects (observations). Element at index ${i} is ${typeof data[i]}. Element: ${JSON.stringify(data[i])}`
46
+ );
47
+ }
48
+ }
49
+ this._observations = this.deepCopy(data);
50
+ const allVariables = /* @__PURE__ */ new Set();
51
+ for (const observation of data) {
52
+ for (const key of Object.keys(observation)) {
53
+ allVariables.add(key);
54
+ }
55
+ }
56
+ for (const observation of this._observations) {
57
+ for (const variable of allVariables) {
58
+ if (!(variable in observation)) {
59
+ observation[variable] = null;
60
+ }
61
+ }
62
+ }
63
+ if (options?.groups) {
64
+ this._groups = Array.from(options.groups);
65
+ }
66
+ }
67
+ /**
68
+ * Returns the groups in the data.
69
+ */
70
+ get groups() {
71
+ return this._groups;
72
+ }
73
+ /**
74
+ * Returns the observations in the data.
75
+ *
76
+ * @remarks An observation is conceptually similar to a row in a dataset,
77
+ * where the keys are the variable names and the values are the variable values.
78
+ */
79
+ get observations() {
80
+ return this._observations;
81
+ }
82
+ /**
83
+ * Alias for the observations property.
84
+ */
85
+ get rows() {
86
+ return this._observations;
87
+ }
88
+ /**
89
+ * Returns a single variable from the data.
90
+ *
91
+ * @remarks If the variable length is 1, the value is returned. If the
92
+ * variable has length > 1, an array of values is returned.
93
+ *
94
+ * @param variable - Name of variable to pull from the data
95
+ * @returns the value of the variable
96
+ *
97
+ * @example
98
+ * ```js
99
+ * const d = [{ a: 1, b: 2, c: 3 }];
100
+ * const dc = new DataCalc(d);
101
+ * console.log(
102
+ * dc.pull("c")
103
+ * ); // 3
104
+ * ```
105
+ */
106
+ pull(variable) {
107
+ if (this._observations.length === 0) {
108
+ console.warn(
109
+ `DataCalc.pull(): No observations available to pull variable "${variable}" from. Returning null.`
110
+ );
111
+ return null;
112
+ }
113
+ this.verifyObservationsContainVariable(variable);
114
+ const values = this._observations.map((o) => o[variable]);
115
+ if (values.length === 1) {
116
+ return values[0];
117
+ }
118
+ return values;
119
+ }
120
+ /**
121
+ * Returns the number of observations in the data.
122
+ *
123
+ * @example
124
+ * ```js
125
+ * const d = [
126
+ * { a: 1, b: 2, c: 3 },
127
+ * { a: 0, b: 8, c: 3 }
128
+ * ];
129
+ * const dc = new DataCalc(d);
130
+ * console.log(
131
+ * dc.length
132
+ * ); // 2
133
+ * ```
134
+ */
135
+ get length() {
136
+ return this._observations.length;
137
+ }
138
+ /**
139
+ * Filters observations based on a predicate function.
140
+ *
141
+ * @param predicate - A function that returns true for observations to keep and
142
+ * false for observations to discard
143
+ * @returns A new `DataCalc` object with only the observations that pass the
144
+ * predicate function
145
+ *
146
+ * @example
147
+ * ```js
148
+ * const d = [
149
+ * { a: 1, b: 2, c: 3 },
150
+ * { a: 0, b: 8, c: 3 },
151
+ * { a: 9, b: 4, c: 7 },
152
+ * ];
153
+ * const dc = new DataCalc(d);
154
+ * console.log(dc.filter((obs) => obs.b >= 3).observations);
155
+ * // [ { a: 0, b: 8, c: 3 }, { a: 9, b: 4, c: 7 } ]
156
+ * ```
157
+ */
158
+ filter(predicate) {
159
+ if (this._groups.length > 0) {
160
+ throw new M2Error(
161
+ `filter() cannot be used on grouped data. The data are currently grouped by ${this._groups.join(
162
+ ", "
163
+ )}. Ungroup the data first using ungroup().`
164
+ );
165
+ }
166
+ return new DataCalc(
167
+ this._observations.filter(
168
+ predicate
169
+ ),
170
+ { groups: this._groups }
171
+ );
172
+ }
173
+ /**
174
+ * Groups observations by one or more variables.
175
+ *
176
+ * @remarks This is used with the `summarize()` method to calculate summaries
177
+ * by group.
178
+ *
179
+ * @param groups - variable names to group by
180
+ * @returns A new `DataCalc` object with the observations grouped by one or
181
+ * more variables
182
+ *
183
+ * @example
184
+ * ```js
185
+ * const d = [
186
+ * { a: 1, b: 2, c: 3 },
187
+ * { a: 0, b: 8, c: 3 },
188
+ * { a: 9, b: 4, c: 7 },
189
+ * { a: 5, b: 0, c: 7 },
190
+ * ];
191
+ * const dc = new DataCalc(d);
192
+ * const grouped = dc.groupBy("c");
193
+ * // subsequent summarize operations will be performed separately by
194
+ * // each unique level of c, in this case, 3 and 7
195
+ * ```
196
+ */
197
+ groupBy(...groups) {
198
+ groups.forEach((group) => {
199
+ this.verifyObservationsContainVariable(group);
200
+ });
201
+ return new DataCalc(this._observations, { groups });
202
+ }
203
+ /**
204
+ * Ungroups observations.
205
+ *
206
+ * @returns A new DataCalc object with the observations ungrouped
207
+ */
208
+ ungroup() {
209
+ return new DataCalc(this._observations);
210
+ }
211
+ /**
212
+ * Adds new variables to the observations based on the provided mutation options.
213
+ *
214
+ * @param mutations - An object where the keys are the names of the new variables
215
+ * and the values are functions that take an observation and return the value
216
+ * for the new variable.
217
+ * @returns A new DataCalc object with the new variables added to the observations.
218
+ *
219
+ * @example
220
+ * const d = [
221
+ * { a: 1, b: 2, c: 3 },
222
+ * { a: 0, b: 8, c: 3 },
223
+ * { a: 9, b: 4, c: 7 },
224
+ * ];
225
+ * const dc = new DataCalc(d);
226
+ * console.log(
227
+ * dc.mutate({ doubledA: (obs) => obs.a * 2 }).observations
228
+ * );
229
+ * // [ { a: 1, b: 2, c: 3, doubledA: 2 },
230
+ * // { a: 0, b: 8, c: 3, doubledA: 0 },
231
+ * // { a: 9, b: 4, c: 7, doubledA: 18 } ]
232
+ */
233
+ mutate(mutations) {
234
+ if (this._groups.length > 0) {
235
+ throw new M2Error(
236
+ `mutate() cannot be used on grouped data. The data are currently grouped by ${this._groups.join(
237
+ ", "
238
+ )}. Ungroup the data first using ungroup().`
239
+ );
240
+ }
241
+ const newObservations = this._observations.map((observation) => {
242
+ let newObservation = { ...observation };
243
+ for (const [newVariable, transformFunction] of Object.entries(
244
+ mutations
245
+ )) {
246
+ newObservation = {
247
+ ...newObservation,
248
+ [newVariable]: transformFunction(observation)
249
+ };
250
+ }
251
+ return newObservation;
252
+ });
253
+ return new DataCalc(newObservations, { groups: this._groups });
254
+ }
255
+ /**
256
+ * Calculates summaries of the data.
257
+ *
258
+ * @param summarizations - An object where the keys are the names of the new
259
+ * variables and the values are `DataCalc` summary functions: `sum()`,
260
+ * `mean()`, `median()`, `variance()`, `sd()`, `min()`, `max()`, or `n()`.
261
+ * The summary functions take a variable name as a string, or alternatively,
262
+ * a value or array of values to summarize.
263
+ * @returns A new `DataCalc` object with the new summary variables.
264
+ *
265
+ * @example
266
+ * ```js
267
+ * const d = [
268
+ * { a: 1, b: 2, c: 3 },
269
+ * { a: 0, b: 8, c: 3 },
270
+ * { a: 9, b: 4, c: 7 },
271
+ * { a: 5, b: 0, c: 7 },
272
+ * ];
273
+ * const dc = new DataCalc(d);
274
+ * console.log(
275
+ * dc.summarize({
276
+ * meanA: mean("a"),
277
+ * varA: variance("a"),
278
+ * totalB: sum("b")
279
+ * }).observations
280
+ * );
281
+ * // [ { meanA: 3.75, varA: 16.916666666666668, totalB: 14 } ]
282
+ *
283
+ * console.log(
284
+ * dc.summarize({
285
+ * filteredTotalC: sum(dc.filter(obs => obs.b > 2).pull("c"))
286
+ * }).observations
287
+ * );
288
+ * // [ { filteredTotalC: 10 } ]
289
+ * ```
290
+ */
291
+ summarize(summarizations) {
292
+ if (this._groups.length === 0) {
293
+ const obs = {};
294
+ for (const [newVariable, value] of Object.entries(summarizations)) {
295
+ if (typeof value === "object" && value !== null && "summarizeFunction" in value) {
296
+ const summarizeOperation = value;
297
+ obs[newVariable] = summarizeOperation.summarizeFunction(
298
+ this,
299
+ summarizeOperation.parameters,
300
+ summarizeOperation.options
301
+ );
302
+ } else {
303
+ obs[newVariable] = value;
304
+ }
305
+ }
306
+ return new DataCalc([obs], { groups: this._groups });
307
+ }
308
+ return this.summarizeByGroups(summarizations);
309
+ }
310
+ summarizeByGroups(summarizations) {
311
+ const groupMap = /* @__PURE__ */ new Map();
312
+ this._observations.forEach((obs) => {
313
+ const groupKey = this._groups.map(
314
+ (g) => typeof obs[g] === "object" ? JSON.stringify(obs[g]) : obs[g]
315
+ ).join("|");
316
+ if (!groupMap.has(groupKey)) {
317
+ groupMap.set(groupKey, []);
318
+ }
319
+ const groupArray = groupMap.get(groupKey);
320
+ if (groupArray) {
321
+ groupArray.push(obs);
322
+ } else {
323
+ groupMap.set(groupKey, [obs]);
324
+ }
325
+ });
326
+ const summarizedObservations = [];
327
+ groupMap.forEach((groupObs, groupKey) => {
328
+ const groupValues = groupKey.split("|");
329
+ const firstObs = groupObs[0];
330
+ const summaryObj = {};
331
+ this._groups.forEach((group, i) => {
332
+ const valueStr = groupValues[i];
333
+ const originalType = typeof firstObs[group];
334
+ if (originalType === "number") {
335
+ summaryObj[group] = Number(valueStr);
336
+ } else if (originalType === "boolean") {
337
+ summaryObj[group] = valueStr === "true";
338
+ } else if (valueStr.startsWith("{") || valueStr.startsWith("[")) {
339
+ try {
340
+ summaryObj[group] = JSON.parse(valueStr);
341
+ } catch {
342
+ throw new M2Error(
343
+ `Failed to parse group value ${valueStr} as JSON for group ${group}`
344
+ );
345
+ }
346
+ } else {
347
+ summaryObj[group] = valueStr;
348
+ }
349
+ });
350
+ const groupDataCalc = new DataCalc(groupObs);
351
+ for (const [newVariable, value] of Object.entries(summarizations)) {
352
+ if (typeof value === "object" && value !== null && "summarizeFunction" in value) {
353
+ const summarizeOperation = value;
354
+ summaryObj[newVariable] = summarizeOperation.summarizeFunction(
355
+ groupDataCalc,
356
+ summarizeOperation.parameters,
357
+ summarizeOperation.options
358
+ );
359
+ } else {
360
+ summaryObj[newVariable] = value;
361
+ }
362
+ }
363
+ summarizedObservations.push(summaryObj);
364
+ });
365
+ return new DataCalc(summarizedObservations, { groups: this._groups });
366
+ }
367
+ /**
368
+ * Selects specific variables to keep in the dataset.
369
+ * Variables prefixed with "-" will be excluded from the result.
370
+ *
371
+ * @param variables - Names of variables to select; prefix with '-' to exclude instead
372
+ * @returns A new DataCalc object with only the selected variables (minus excluded ones)
373
+ *
374
+ * @example
375
+ * ```js
376
+ * const d = [
377
+ * { a: 1, b: 2, c: 3, d: 4 },
378
+ * { a: 5, b: 6, c: 7, d: 8 }
379
+ * ];
380
+ * const dc = new DataCalc(d);
381
+ * // Keep a and c
382
+ * console.log(dc.select("a", "c").observations);
383
+ * // [ { a: 1, c: 3 }, { a: 5, c: 7 } ]
384
+ * ```
385
+ */
386
+ select(...variables) {
387
+ const includeVars = [];
388
+ const excludeVars = [];
389
+ variables.forEach((variable) => {
390
+ if (variable.startsWith("-")) {
391
+ excludeVars.push(variable.substring(1));
392
+ } else {
393
+ includeVars.push(variable);
394
+ }
395
+ });
396
+ const allVars = includeVars.length > 0 ? includeVars : Object.keys(this._observations[0] || {});
397
+ [...allVars, ...excludeVars].forEach((variable) => {
398
+ this.verifyObservationsContainVariable(variable);
399
+ });
400
+ const excludeSet = new Set(excludeVars);
401
+ const newObservations = this._observations.map((observation) => {
402
+ const newObservation = {};
403
+ if (includeVars.length > 0) {
404
+ includeVars.forEach((variable) => {
405
+ if (!excludeSet.has(variable)) {
406
+ newObservation[variable] = observation[variable];
407
+ }
408
+ });
409
+ } else {
410
+ Object.keys(observation).forEach((key) => {
411
+ if (!excludeSet.has(key)) {
412
+ newObservation[key] = observation[key];
413
+ }
414
+ });
415
+ }
416
+ return newObservation;
417
+ });
418
+ return new DataCalc(newObservations, { groups: this._groups });
419
+ }
420
+ /**
421
+ * Arranges (sorts) the observations based on one or more variables.
422
+ *
423
+ * @param variables - Names of variables to sort by, prefixed with '-' for descending order
424
+ * @returns A new DataCalc object with the observations sorted
425
+ *
426
+ * @example
427
+ * ```js
428
+ * const d = [
429
+ * { a: 5, b: 2 },
430
+ * { a: 3, b: 7 },
431
+ * { a: 5, b: 1 }
432
+ * ];
433
+ * const dc = new DataCalc(d);
434
+ * // Sort by a (ascending), then by b (descending)
435
+ * console.log(dc.arrange("a", "-b").observations);
436
+ * // [ { a: 3, b: 7 }, { a: 5, b: 2 }, { a: 5, b: 1 } ]
437
+ * ```
438
+ */
439
+ arrange(...variables) {
440
+ if (this._groups.length > 0) {
441
+ throw new M2Error(
442
+ `arrange() cannot be used on grouped data. The data are currently grouped by ${this._groups.join(
443
+ ", "
444
+ )}. Ungroup the data first using ungroup().`
445
+ );
446
+ }
447
+ const sortedObservations = [...this._observations].sort((a, b) => {
448
+ for (const variable of variables) {
449
+ let varName = variable;
450
+ let direction = 1;
451
+ if (variable.startsWith("-")) {
452
+ varName = variable.substring(1);
453
+ direction = -1;
454
+ }
455
+ if (!(varName in a) || !(varName in b)) {
456
+ throw new M2Error(
457
+ `arrange(): variable ${varName} does not exist in all observations`
458
+ );
459
+ }
460
+ const aVal = a[varName];
461
+ const bVal = b[varName];
462
+ if (typeof aVal !== typeof bVal) {
463
+ return direction * (String(aVal) < String(bVal) ? -1 : 1);
464
+ }
465
+ if (aVal < bVal) return -1 * direction;
466
+ if (aVal > bVal) return 1 * direction;
467
+ }
468
+ return 0;
469
+ });
470
+ return new DataCalc(sortedObservations, { groups: this._groups });
471
+ }
472
+ /**
473
+ * Keeps only unique/distinct observations.
474
+ *
475
+ * @returns A new `DataCalc` object with only unique observations
476
+ *
477
+ * @example
478
+ * ```js
479
+ * const d = [
480
+ * { a: 1, b: 2, c: 3 },
481
+ * { a: 1, b: 2, c: 3 }, // Duplicate
482
+ * { a: 2, b: 3, c: 5 },
483
+ * { a: 1, b: 2, c: { name: "dog" } },
484
+ * { a: 1, b: 2, c: { name: "dog" } } // Duplicate with nested object
485
+ * ];
486
+ * const dc = new DataCalc(d);
487
+ * console.log(dc.distinct().observations);
488
+ * // [ { a: 1, b: 2, c: 3 }, { a: 2, b: 3, c: 5 }, { a: 1, b: 2, c: { name: "dog" } } ]
489
+ * ```
490
+ */
491
+ distinct() {
492
+ const seen = /* @__PURE__ */ new Set();
493
+ const uniqueObs = this._observations.filter((obs) => {
494
+ const key = JSON.stringify(this.normalizeForComparison(obs));
495
+ if (seen.has(key)) return false;
496
+ seen.add(key);
497
+ return true;
498
+ });
499
+ return new DataCalc(uniqueObs, { groups: this._groups });
500
+ }
501
+ /**
502
+ * Renames variables in the observations.
503
+ *
504
+ * @param renames - Object mapping new variable names to old variable names
505
+ * @returns A new DataCalc object with renamed variables
506
+ *
507
+ * @example
508
+ * ```js
509
+ * const d = [
510
+ * { a: 1, b: 2, c: 3 },
511
+ * { a: 4, b: 5, c: 6 }
512
+ * ];
513
+ * const dc = new DataCalc(d);
514
+ * console.log(dc.rename({ x: 'a', z: 'c' }).observations);
515
+ * // [ { x: 1, b: 2, z: 3 }, { x: 4, b: 5, z: 6 } ]
516
+ * ```
517
+ */
518
+ rename(renames) {
519
+ if (this._observations.length === 0) {
520
+ throw new M2Error("Cannot rename variables on an empty dataset");
521
+ }
522
+ Object.values(renames).forEach((oldName) => {
523
+ this.verifyObservationsContainVariable(oldName);
524
+ });
525
+ const newObservations = this._observations.map((observation) => {
526
+ const newObservation = {};
527
+ for (const [key, value] of Object.entries(observation)) {
528
+ const newKey = Object.entries(renames).find(
529
+ ([, old]) => old === key
530
+ )?.[0];
531
+ if (newKey) {
532
+ newObservation[newKey] = value;
533
+ } else if (!Object.values(renames).includes(key)) {
534
+ newObservation[key] = value;
535
+ }
536
+ }
537
+ return newObservation;
538
+ });
539
+ return new DataCalc(newObservations, { groups: this._groups });
540
+ }
541
+ /**
542
+ * Performs an inner join with another DataCalc object.
543
+ * Only rows with matching keys in both datasets are included.
544
+ *
545
+ * @param other - The other DataCalc object to join with
546
+ * @param by - The variables to join on
547
+ * @returns A new DataCalc object with joined observations
548
+ *
549
+ * @example
550
+ * ```js
551
+ * const d1 = [
552
+ * { id: 1, x: 'a' },
553
+ * { id: 2, x: 'b' },
554
+ * { id: 3, x: 'c' }
555
+ * ];
556
+ * const d2 = [
557
+ * { id: 1, y: 100 },
558
+ * { id: 2, y: 200 },
559
+ * { id: 4, y: 400 }
560
+ * ];
561
+ * const dc1 = new DataCalc(d1);
562
+ * const dc2 = new DataCalc(d2);
563
+ * console.log(dc1.innerJoin(dc2, ["id"]).observations);
564
+ * // [ { id: 1, x: 'a', y: 100 }, { id: 2, x: 'b', y: 200 } ]
565
+ * ```
566
+ */
567
+ innerJoin(other, by) {
568
+ if (this._groups.length > 0 || other._groups.length > 0) {
569
+ throw new M2Error(
570
+ `innerJoin() cannot be used on grouped data. Ungroup the data first using ungroup().`
571
+ );
572
+ }
573
+ by.forEach((key) => {
574
+ this.verifyObservationsContainVariable(key);
575
+ other.verifyObservationsContainVariable(key);
576
+ });
577
+ const rightMap = /* @__PURE__ */ new Map();
578
+ other.observations.forEach((obs) => {
579
+ if (this.hasNullJoinKeys(obs, by)) {
580
+ return;
581
+ }
582
+ const key = by.map((k) => JSON.stringify(this.normalizeForComparison(obs[k]))).join("|");
583
+ const matches = rightMap.get(key) || [];
584
+ matches.push(obs);
585
+ rightMap.set(key, matches);
586
+ });
587
+ const result = [];
588
+ this._observations.forEach((leftObs) => {
589
+ if (this.hasNullJoinKeys(leftObs, by)) {
590
+ return;
591
+ }
592
+ const key = by.map((k) => JSON.stringify(this.normalizeForComparison(leftObs[k]))).join("|");
593
+ const rightMatches = rightMap.get(key) || [];
594
+ if (rightMatches.length > 0) {
595
+ rightMatches.forEach((rightObs) => {
596
+ const joinedObs = { ...leftObs };
597
+ Object.entries(rightObs).forEach(([k, v]) => {
598
+ if (!by.includes(k)) {
599
+ joinedObs[k] = v;
600
+ }
601
+ });
602
+ result.push(joinedObs);
603
+ });
604
+ }
605
+ });
606
+ return new DataCalc(result);
607
+ }
608
+ /**
609
+ * Performs a left join with another DataCalc object.
610
+ * All rows from the left dataset are included, along with matching rows from the right.
611
+ *
612
+ * @param other - The other DataCalc object to join with
613
+ * @param by - The variables to join on
614
+ * @returns A new DataCalc object with joined observations
615
+ *
616
+ * @example
617
+ * ```js
618
+ * const d1 = [
619
+ * { id: 1, x: 'a' },
620
+ * { id: 2, x: 'b' },
621
+ * { id: 3, x: 'c' }
622
+ * ];
623
+ * const d2 = [
624
+ * { id: 1, y: 100 },
625
+ * { id: 2, y: 200 }
626
+ * ];
627
+ * const dc1 = new DataCalc(d1);
628
+ * const dc2 = new DataCalc(d2);
629
+ * console.log(dc1.leftJoin(dc2, ["id"]).observations);
630
+ * // [ { id: 1, x: 'a', y: 100 }, { id: 2, x: 'b', y: 200 }, { id: 3, x: 'c' } ]
631
+ * ```
632
+ */
633
+ leftJoin(other, by) {
634
+ if (this._groups.length > 0 || other._groups.length > 0) {
635
+ throw new M2Error(
636
+ `leftJoin() cannot be used on grouped data. Ungroup the data first using ungroup().`
637
+ );
638
+ }
639
+ by.forEach((key) => {
640
+ this.verifyObservationsContainVariable(key);
641
+ other.verifyObservationsContainVariable(key);
642
+ });
643
+ const rightMap = /* @__PURE__ */ new Map();
644
+ other.observations.forEach((obs) => {
645
+ if (this.hasNullJoinKeys(obs, by)) {
646
+ return;
647
+ }
648
+ const key = by.map((k) => JSON.stringify(this.normalizeForComparison(obs[k]))).join("|");
649
+ const matches = rightMap.get(key) || [];
650
+ matches.push(obs);
651
+ rightMap.set(key, matches);
652
+ });
653
+ const result = [];
654
+ this._observations.forEach((leftObs) => {
655
+ if (this.hasNullJoinKeys(leftObs, by)) {
656
+ result.push({ ...leftObs });
657
+ return;
658
+ }
659
+ const key = by.map((k) => JSON.stringify(this.normalizeForComparison(leftObs[k]))).join("|");
660
+ const rightMatches = rightMap.get(key) || [];
661
+ if (rightMatches.length > 0) {
662
+ rightMatches.forEach((rightObs) => {
663
+ const joinedObs = { ...leftObs };
664
+ Object.entries(rightObs).forEach(([k, v]) => {
665
+ if (!by.includes(k)) {
666
+ joinedObs[k] = v;
667
+ }
668
+ });
669
+ result.push(joinedObs);
670
+ });
671
+ } else {
672
+ result.push({ ...leftObs });
673
+ }
674
+ });
675
+ return new DataCalc(result);
676
+ }
677
+ /**
678
+ * Performs a right join with another DataCalc object.
679
+ * All rows from the right dataset are included, along with matching rows from the left.
680
+ *
681
+ * @param other - The other DataCalc object to join with
682
+ * @param by - The variables to join on
683
+ * @returns A new DataCalc object with joined observations
684
+ *
685
+ * @example
686
+ * ```js
687
+ * const d1 = [
688
+ * { id: 1, x: 'a' },
689
+ * { id: 2, x: 'b' }
690
+ * ];
691
+ * const d2 = [
692
+ * { id: 1, y: 100 },
693
+ * { id: 2, y: 200 },
694
+ * { id: 4, y: 400 }
695
+ * ];
696
+ * const dc1 = new DataCalc(d1);
697
+ * const dc2 = new DataCalc(d2);
698
+ * console.log(dc1.rightJoin(dc2, ["id"]).observations);
699
+ * // [ { id: 1, x: 'a', y: 100 }, { id: 2, x: 'b', y: 200 }, { id: 4, y: 400 } ]
700
+ * ```
701
+ */
702
+ rightJoin(other, by) {
703
+ if (this._groups.length > 0 || other._groups.length > 0) {
704
+ throw new M2Error(
705
+ `rightJoin() cannot be used on grouped data. Ungroup the data first using ungroup().`
706
+ );
707
+ }
708
+ by.forEach((key) => {
709
+ this.verifyObservationsContainVariable(key);
710
+ other.verifyObservationsContainVariable(key);
711
+ });
712
+ const rightMap = /* @__PURE__ */ new Map();
713
+ other.observations.forEach((obs) => {
714
+ if (this.hasNullJoinKeys(obs, by)) {
715
+ return;
716
+ }
717
+ const key = by.map((k) => JSON.stringify(this.normalizeForComparison(obs[k]))).join("|");
718
+ const matches = rightMap.get(key) || [];
719
+ matches.push(obs);
720
+ rightMap.set(key, matches);
721
+ });
722
+ const result = [];
723
+ const processedRightKeys = /* @__PURE__ */ new Set();
724
+ this._observations.forEach((leftObs) => {
725
+ if (this.hasNullJoinKeys(leftObs, by)) {
726
+ return;
727
+ }
728
+ const key = by.map((k) => JSON.stringify(this.normalizeForComparison(leftObs[k]))).join("|");
729
+ const rightMatches = rightMap.get(key) || [];
730
+ if (rightMatches.length > 0) {
731
+ rightMatches.forEach((rightObs) => {
732
+ const joinedObs = { ...leftObs };
733
+ Object.entries(rightObs).forEach(([k, v]) => {
734
+ if (!by.includes(k)) {
735
+ joinedObs[k] = v;
736
+ }
737
+ });
738
+ result.push(joinedObs);
739
+ });
740
+ processedRightKeys.add(key);
741
+ }
742
+ });
743
+ other.observations.forEach((rightObs) => {
744
+ if (this.hasNullJoinKeys(rightObs, by)) {
745
+ result.push({ ...rightObs });
746
+ return;
747
+ }
748
+ const key = by.map((k) => JSON.stringify(this.normalizeForComparison(rightObs[k]))).join("|");
749
+ if (!processedRightKeys.has(key)) {
750
+ result.push({ ...rightObs });
751
+ processedRightKeys.add(key);
752
+ }
753
+ });
754
+ return new DataCalc(result);
755
+ }
756
+ /**
757
+ * Performs a full join with another DataCalc object.
758
+ * All rows from both datasets are included.
759
+ *
760
+ * @param other - The other DataCalc object to join with
761
+ * @param by - The variables to join on
762
+ * @returns A new DataCalc object with joined observations
763
+ *
764
+ * @example
765
+ * ```js
766
+ * const d1 = [
767
+ * { id: 1, x: 'a' },
768
+ * { id: 2, x: 'b' },
769
+ * { id: 3, x: 'c' }
770
+ * ];
771
+ * const d2 = [
772
+ * { id: 1, y: 100 },
773
+ * { id: 2, y: 200 },
774
+ * { id: 4, y: 400 }
775
+ * ];
776
+ * const dc1 = new DataCalc(d1);
777
+ * const dc2 = new DataCalc(d2);
778
+ * console.log(dc1.fullJoin(dc2, ["id"]).observations);
779
+ * // [
780
+ * // { id: 1, x: 'a', y: 100 },
781
+ * // { id: 2, x: 'b', y: 200 },
782
+ * // { id: 3, x: 'c' },
783
+ * // { id: 4, y: 400 }
784
+ * // ]
785
+ * ```
786
+ */
787
+ fullJoin(other, by) {
788
+ if (this._groups.length > 0 || other._groups.length > 0) {
789
+ throw new M2Error(
790
+ `fullJoin() cannot be used on grouped data. Ungroup the data first using ungroup().`
791
+ );
792
+ }
793
+ by.forEach((key) => {
794
+ this.verifyObservationsContainVariable(key);
795
+ other.verifyObservationsContainVariable(key);
796
+ });
797
+ const rightMap = /* @__PURE__ */ new Map();
798
+ other.observations.forEach((obs) => {
799
+ if (this.hasNullJoinKeys(obs, by)) {
800
+ return;
801
+ }
802
+ const key = by.map((k) => JSON.stringify(this.normalizeForComparison(obs[k]))).join("|");
803
+ const matches = rightMap.get(key) || [];
804
+ matches.push(obs);
805
+ rightMap.set(key, matches);
806
+ });
807
+ const result = [];
808
+ const processedRightKeys = /* @__PURE__ */ new Set();
809
+ this._observations.forEach((leftObs) => {
810
+ if (this.hasNullJoinKeys(leftObs, by)) {
811
+ result.push({ ...leftObs });
812
+ return;
813
+ }
814
+ const key = by.map((k) => JSON.stringify(this.normalizeForComparison(leftObs[k]))).join("|");
815
+ const rightMatches = rightMap.get(key) || [];
816
+ if (rightMatches.length > 0) {
817
+ rightMatches.forEach((rightObs) => {
818
+ const joinedObs = { ...leftObs };
819
+ Object.entries(rightObs).forEach(([k, v]) => {
820
+ if (!by.includes(k)) {
821
+ joinedObs[k] = v;
822
+ }
823
+ });
824
+ result.push(joinedObs);
825
+ });
826
+ processedRightKeys.add(key);
827
+ } else {
828
+ result.push({ ...leftObs });
829
+ }
830
+ });
831
+ other.observations.forEach((rightObs) => {
832
+ if (this.hasNullJoinKeys(rightObs, by)) {
833
+ result.push({ ...rightObs });
834
+ return;
835
+ }
836
+ const key = by.map((k) => JSON.stringify(this.normalizeForComparison(rightObs[k]))).join("|");
837
+ if (!processedRightKeys.has(key)) {
838
+ result.push({ ...rightObs });
839
+ processedRightKeys.add(key);
840
+ }
841
+ });
842
+ return new DataCalc(result);
843
+ }
844
+ /**
845
+ * Slice observations by position.
846
+ *
847
+ * @param start - Starting position (0-based). Negative values count from
848
+ * the end.
849
+ * @param end - Ending position (exclusive)
850
+ * @returns A new DataCalc object with sliced observations
851
+ *
852
+ * @remarks If `end` is not provided, it will return a single observation at
853
+ * `start` position. If `start` is beyond the length of observations,
854
+ * it will return an empty DataCalc.
855
+ *
856
+ * @example
857
+ * ```js
858
+ * const d = [
859
+ * { a: 1, b: 2 },
860
+ * { a: 3, b: 4 },
861
+ * { a: 5, b: 6 },
862
+ * { a: 7, b: 8 }
863
+ * ];
864
+ * const dc = new DataCalc(d);
865
+ * console.log(dc.slice(1, 3).observations);
866
+ * // [ { a: 3, b: 4 }, { a: 5, b: 6 } ]
867
+ * console.log(dc.slice(0).observations);
868
+ * // [ { a: 1, b: 2 } ]
869
+ * ```
870
+ */
871
+ slice(start, end) {
872
+ if (this._groups.length > 0) {
873
+ throw new M2Error(
874
+ `slice() cannot be used on grouped data. Ungroup the data first using ungroup().`
875
+ );
876
+ }
877
+ let sliced;
878
+ if (start >= this._observations.length) {
879
+ return new DataCalc([], { groups: this._groups });
880
+ }
881
+ if (end === void 0) {
882
+ const index = start < 0 ? this._observations.length + start : start;
883
+ sliced = [this._observations[index]];
884
+ } else {
885
+ sliced = this._observations.slice(start, end);
886
+ }
887
+ return new DataCalc(sliced, { groups: this._groups });
888
+ }
889
+ /**
890
+ * Combines observations from two DataCalc objects by rows.
891
+ *
892
+ * @param other - The other DataCalc object to bind with
893
+ * @returns A new DataCalc object with combined observations
894
+ *
895
+ * @example
896
+ * ```js
897
+ * const d1 = [
898
+ * { a: 1, b: 2 },
899
+ * { a: 3, b: 4 }
900
+ * ];
901
+ * const d2 = [
902
+ * { a: 5, b: 6 },
903
+ * { a: 7, b: 8 }
904
+ * ];
905
+ * const dc1 = new DataCalc(d1);
906
+ * const dc2 = new DataCalc(d2);
907
+ * console.log(dc1.bindRows(dc2).observations);
908
+ * // [ { a: 1, b: 2 }, { a: 3, b: 4 }, { a: 5, b: 6 }, { a: 7, b: 8 } ]
909
+ * ```
910
+ */
911
+ bindRows(other) {
912
+ if (this._observations.length > 0 && other.observations.length > 0) {
913
+ const thisVariables = new Set(Object.keys(this._observations[0]));
914
+ const otherVariables = new Set(Object.keys(other.observations[0]));
915
+ const commonVariables = [...thisVariables].filter(
916
+ (variable) => otherVariables.has(variable)
917
+ );
918
+ commonVariables.forEach((variable) => {
919
+ const thisType = this.getVariableType(variable);
920
+ const otherType = other.getVariableType(variable);
921
+ if (thisType !== otherType) {
922
+ console.warn(
923
+ `Warning: bindRows() is combining datasets with different data types for variable '${variable}'. Left dataset has type '${thisType}' and right dataset has type '${otherType}'.`
924
+ );
925
+ }
926
+ });
927
+ }
928
+ return new DataCalc([...this._observations, ...other.observations]);
929
+ }
930
+ /**
931
+ * Helper method to determine the primary type of a variable across observations
932
+ * @internal
933
+ *
934
+ * @param variable - The variable name to check
935
+ * @returns The most common type for the variable or 'mixed' if no clear type exists
936
+ */
937
+ getVariableType(variable) {
938
+ if (this._observations.length === 0) {
939
+ return "unknown";
940
+ }
941
+ const typeCounts = {};
942
+ this._observations.forEach((obs) => {
943
+ if (variable in obs) {
944
+ const value = obs[variable];
945
+ const type = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
946
+ typeCounts[type] = (typeCounts[type] || 0) + 1;
947
+ }
948
+ });
949
+ let maxCount = 0;
950
+ let dominantType = "unknown";
951
+ for (const [type, count] of Object.entries(typeCounts)) {
952
+ if (count > maxCount) {
953
+ maxCount = count;
954
+ dominantType = type;
955
+ }
956
+ }
957
+ return dominantType;
958
+ }
959
+ /**
960
+ * Verifies that the variable exists in each observation in the data.
961
+ *
962
+ * @remarks Throws an error if the variable does not exist in each
963
+ * observation. This is not meant to be called by users of the library, but
964
+ * is used internally.
965
+ * @internal
966
+ *
967
+ * @param variable - The variable to check for
968
+ */
969
+ verifyObservationsContainVariable(variable) {
970
+ if (!this._observations.every((observation) => variable in observation)) {
971
+ throw new M2Error(
972
+ `Variable ${variable} does not exist for each item (row) in the data array.`
973
+ );
974
+ }
975
+ }
976
+ /**
977
+ * Checks if the variable exists for at least one observation in the data.
978
+ *
979
+ * @remarks This is not meant to be called by users of the library, but
980
+ * is used internally.
981
+ * @internal
982
+ *
983
+ * @param variable - The variable to check for
984
+ * @returns true if the variable exists in at least one observation, false
985
+ * otherwise
986
+ */
987
+ variableExists(variable) {
988
+ return this._observations.some((observation) => variable in observation);
989
+ }
990
+ /**
991
+ * Checks if a value is a non-missing numeric value.
992
+ *
993
+ * @remarks A non-missing numeric value is a value that is a number and is
994
+ * not NaN or infinite.
995
+ *
996
+ * @param value - The value to check
997
+ * @returns true if the value is a non-missing numeric value, false otherwise
998
+ */
999
+ isNonMissingNumeric(value) {
1000
+ return typeof value === "number" && !isNaN(value) && isFinite(value);
1001
+ }
1002
+ /**
1003
+ * Checks if a value is a missing numeric value.
1004
+ *
1005
+ * @remarks A missing numeric value is a number that is NaN or infinite, or any
1006
+ * value that is null or undefined. Thus, a null or undefined value is
1007
+ * considered to be a missing numeric value.
1008
+ *
1009
+ * @param value - The value to check
1010
+ * @returns true if the value is a missing numeric value, false otherwise
1011
+ */
1012
+ isMissingNumeric(value) {
1013
+ return typeof value === "number" && (isNaN(value) || !isFinite(value)) || value === null || typeof value === "undefined";
1014
+ }
1015
+ /**
1016
+ * Normalizes an object for stable comparison by sorting keys
1017
+ * @internal
1018
+ *
1019
+ * @remarks Normalizing is needed to handle situations where objects have the
1020
+ * same properties but in different orders because we are using
1021
+ * JSON.stringify() for comparison.
1022
+ */
1023
+ normalizeForComparison(obj) {
1024
+ if (obj === null || typeof obj !== "object") {
1025
+ return obj;
1026
+ }
1027
+ if (Array.isArray(obj)) {
1028
+ return obj.map((item) => this.normalizeForComparison(item));
1029
+ }
1030
+ return Object.keys(obj).sort().reduce((result, key) => {
1031
+ result[key] = this.normalizeForComparison(obj[key]);
1032
+ return result;
1033
+ }, {});
1034
+ }
1035
+ /**
1036
+ * Creates a deep copy of an object.
1037
+ * @internal
1038
+ *
1039
+ * @remarks We create a deep copy of the object, in our case an instance
1040
+ * of `DataCalc`, to ensure that we are working with a new object
1041
+ * without any references to the original object. This is important
1042
+ * to avoid unintended side effects when modifying an object.
1043
+ *
1044
+ * @param source - object to copy
1045
+ * @param map - map of objects that have already been copied
1046
+ * @returns a deep copy of the object
1047
+ */
1048
+ deepCopy(source, map = /* @__PURE__ */ new WeakMap()) {
1049
+ if (source === null || typeof source !== "object") {
1050
+ return source;
1051
+ }
1052
+ if (map.has(source)) {
1053
+ return map.get(source);
1054
+ }
1055
+ const copy = Array.isArray(source) ? [] : Object.create(Object.getPrototypeOf(source));
1056
+ map.set(source, copy);
1057
+ const keys = [
1058
+ ...Object.getOwnPropertyNames(source),
1059
+ ...Object.getOwnPropertySymbols(source)
1060
+ ];
1061
+ for (const key of keys) {
1062
+ const descriptor = Object.getOwnPropertyDescriptor(
1063
+ source,
1064
+ key
1065
+ );
1066
+ if (descriptor) {
1067
+ Object.defineProperty(copy, key, {
1068
+ ...descriptor,
1069
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1070
+ value: this.deepCopy(source[key], map)
1071
+ });
1072
+ }
1073
+ }
1074
+ return copy;
1075
+ }
1076
+ /**
1077
+ * Checks if an observation has null or undefined values in any of the join columns.
1078
+ * @internal
1079
+ *
1080
+ * @param obs - The observation to check
1081
+ * @param keys - The join columns to check
1082
+ * @returns true if any join column has a null or undefined value
1083
+ */
1084
+ hasNullJoinKeys(obs, keys) {
1085
+ return keys.some((key) => obs[key] === null || obs[key] === void 0);
1086
+ }
1087
+ }
1088
+ console.log("\u26AA @m2c2kit/data-calc version 0.8.5 (d289d12c)");
1089
+
4
1090
  class ColorShapes extends Game {
5
1091
  constructor() {
6
1092
  const defaultParameters = {
@@ -63,12 +1149,12 @@ class ColorShapes extends Game {
63
1149
  type: "integer"
64
1150
  },
65
1151
  number_of_different_colors_trials: {
66
- default: 2,
1152
+ default: 6,
67
1153
  type: "integer",
68
1154
  description: "Number of trials where the shapes have different colors."
69
1155
  },
70
1156
  number_of_trials: {
71
- default: 4,
1157
+ default: 12,
72
1158
  description: "How many trials to run.",
73
1159
  type: "integer"
74
1160
  },
@@ -102,6 +1188,16 @@ class ColorShapes extends Game {
102
1188
  type: "boolean",
103
1189
  default: false,
104
1190
  description: "Should the icon that allows the participant to switch the locale be shown?"
1191
+ },
1192
+ seed: {
1193
+ type: ["string", "null"],
1194
+ default: null,
1195
+ description: "Optional seed for the seeded pseudo-random number generator. When null, the default Math.random() is used."
1196
+ },
1197
+ scoring: {
1198
+ type: "boolean",
1199
+ default: false,
1200
+ description: "Should scoring data be generated? Default is false."
105
1201
  }
106
1202
  };
107
1203
  const colorShapesTrialSchema = {
@@ -218,6 +1314,43 @@ class ColorShapes extends Game {
218
1314
  description: "Was the quit button pressed?"
219
1315
  }
220
1316
  };
1317
+ const colorShapesScoringSchema = {
1318
+ activity_begin_iso8601_timestamp: {
1319
+ type: "string",
1320
+ format: "date-time",
1321
+ description: "ISO 8601 timestamp at the beginning of the game activity."
1322
+ },
1323
+ first_trial_begin_iso8601_timestamp: {
1324
+ type: ["string", "null"],
1325
+ format: "date-time",
1326
+ description: "ISO 8601 timestamp at the beginning of the first trial. Null if no trials were completed."
1327
+ },
1328
+ last_trial_end_iso8601_timestamp: {
1329
+ type: ["string", "null"],
1330
+ format: "date-time",
1331
+ description: "ISO 8601 timestamp at the end of the last trial. Null if no trials were completed."
1332
+ },
1333
+ n_trials: {
1334
+ type: "integer",
1335
+ description: "Number of trials completed."
1336
+ },
1337
+ flag_trials_match_expected: {
1338
+ type: "integer",
1339
+ description: "Does the number of completed and expected trials match? 1 = true, 0 = false."
1340
+ },
1341
+ n_trials_correct: {
1342
+ type: "integer",
1343
+ description: "Number of correct trials."
1344
+ },
1345
+ n_trials_incorrect: {
1346
+ type: "integer",
1347
+ description: "Number of incorrect trials."
1348
+ },
1349
+ participant_score: {
1350
+ type: ["number", "null"],
1351
+ description: "Participant-facing score, calculated as (number of correct trials / number of trials attempted) * 100. This is a simple metric to provide feedback to the participant. Null if no trials attempted."
1352
+ }
1353
+ };
221
1354
  const translation = {
222
1355
  configuration: {
223
1356
  baseLocale: "en-US"
@@ -282,8 +1415,8 @@ class ColorShapes extends Game {
282
1415
  */
283
1416
  id: "color-shapes",
284
1417
  publishUuid: "394cb010-2ccf-4a87-9d23-cda7fb07a960",
285
- version: "0.8.30 (f8bdff76)",
286
- moduleMetadata: { "name": "@m2c2kit/assessment-color-shapes", "version": "0.8.30", "dependencies": { "@m2c2kit/addons": "0.3.31", "@m2c2kit/core": "0.3.32" } },
1418
+ version: "0.8.32 (d289d12c)",
1419
+ moduleMetadata: { "name": "@m2c2kit/assessment-color-shapes", "version": "0.8.32", "dependencies": { "@m2c2kit/addons": "0.3.33", "@m2c2kit/core": "0.3.34" } },
287
1420
  translation,
288
1421
  shortDescription: "Color Shapes is a visual array change detection task, measuring intra-item feature binding, where participants determine if shapes change color across two sequential presentations of shape stimuli.",
289
1422
  longDescription: `Color Shapes is a change detection paradigm used to measure visual short-term memory binding (Parra et al., 2009). Participants are asked to memorize the shapes and colors of three different polygons for 3 seconds. The three polygons are then removed from the screen and re-displayed at different locations, either having the same or different colors. Participants are then asked to decide whether the combination of colors and shapes are the "Same" or "Different" between the study and test phases.`,
@@ -291,6 +1424,7 @@ class ColorShapes extends Game {
291
1424
  width: 400,
292
1425
  height: 800,
293
1426
  trialSchema: colorShapesTrialSchema,
1427
+ scoringSchema: colorShapesScoringSchema,
294
1428
  parameters: defaultParameters,
295
1429
  fonts: [
296
1430
  {
@@ -333,6 +1467,10 @@ class ColorShapes extends Game {
333
1467
  async initialize() {
334
1468
  await super.initialize();
335
1469
  const game = this;
1470
+ const seed = game.getParameter("seed");
1471
+ if (typeof seed === "string") {
1472
+ RandomDraws.setSeed(seed);
1473
+ }
336
1474
  const SHAPE_SVG_HEIGHT = 96;
337
1475
  const SQUARE_SIDE_LENGTH = 350;
338
1476
  const numberOfShapesShown = game.getParameter(
@@ -354,6 +1492,13 @@ class ColorShapes extends Game {
354
1492
  game.presentScene(blankScene);
355
1493
  game.addTrialData("quit_button_pressed", true);
356
1494
  game.trialComplete();
1495
+ if (game.getParameter("scoring")) {
1496
+ const scores = game.calculateScores([], {
1497
+ numberOfTrials: game.getParameter("number_of_trials")
1498
+ });
1499
+ game.addScoringData(scores);
1500
+ game.scoringComplete();
1501
+ }
357
1502
  game.cancel();
358
1503
  });
359
1504
  }
@@ -435,7 +1580,7 @@ class ColorShapes extends Game {
435
1580
  break;
436
1581
  }
437
1582
  default: {
438
- throw new M2Error("invalid value for instruction_type");
1583
+ throw new M2Error$1("invalid value for instruction_type");
439
1584
  }
440
1585
  }
441
1586
  }
@@ -465,7 +1610,7 @@ class ColorShapes extends Game {
465
1610
  const numberOfDifferentColorsTrials = game.getParameter(
466
1611
  "number_of_different_colors_trials"
467
1612
  );
468
- const differentColorsTrialIndexes = RandomDraws.FromRangeWithoutReplacement(
1613
+ const differentColorsTrialIndexes = RandomDraws.fromRangeWithoutReplacement(
469
1614
  numberOfDifferentColorsTrials,
470
1615
  0,
471
1616
  numberOfTrials - 1
@@ -473,12 +1618,12 @@ class ColorShapes extends Game {
473
1618
  for (let i = 0; i < numberOfTrials; i++) {
474
1619
  const presentShapes = new Array();
475
1620
  const responseShapes = new Array();
476
- const shapesToShowIndexes = RandomDraws.FromRangeWithoutReplacement(
1621
+ const shapesToShowIndexes = RandomDraws.fromRangeWithoutReplacement(
477
1622
  numberOfShapesShown,
478
1623
  0,
479
1624
  shapeLibrary.length - 1
480
1625
  );
481
- const shapeColorsIndexes = RandomDraws.FromRangeWithoutReplacement(
1626
+ const shapeColorsIndexes = RandomDraws.fromRangeWithoutReplacement(
482
1627
  numberOfShapesShown,
483
1628
  0,
484
1629
  shapeColors.length - 1
@@ -503,7 +1648,7 @@ class ColorShapes extends Game {
503
1648
  let presentLocationsOk = false;
504
1649
  let presentLocations;
505
1650
  do {
506
- presentLocations = RandomDraws.FromGridWithoutReplacement(
1651
+ presentLocations = RandomDraws.fromGridWithoutReplacement(
507
1652
  numberOfShapesShown,
508
1653
  rows,
509
1654
  columns
@@ -527,7 +1672,7 @@ class ColorShapes extends Game {
527
1672
  let responseLocationsOk = false;
528
1673
  let responseLocations;
529
1674
  do {
530
- responseLocations = RandomDraws.FromGridWithoutReplacement(
1675
+ responseLocations = RandomDraws.fromGridWithoutReplacement(
531
1676
  numberOfShapesShown,
532
1677
  rows,
533
1678
  columns
@@ -555,11 +1700,11 @@ class ColorShapes extends Game {
555
1700
  "number_of_shapes_changing_color"
556
1701
  );
557
1702
  if (numberOfShapesToChange > numberOfShapesShown) {
558
- throw new M2Error(
1703
+ throw new M2Error$1(
559
1704
  `number_of_shapes_changing_color is ${numberOfShapesToChange}, but it must be less than or equal to number_of_shapes_shown (which is ${numberOfShapesShown}).`
560
1705
  );
561
1706
  }
562
- const shapesToChangeIndexes = RandomDraws.FromRangeWithoutReplacement(
1707
+ const shapesToChangeIndexes = RandomDraws.fromRangeWithoutReplacement(
563
1708
  numberOfShapesToChange,
564
1709
  0,
565
1710
  numberOfShapesShown - 1
@@ -769,14 +1914,25 @@ class ColorShapes extends Game {
769
1914
  if (game.trialIndex < numberOfTrials) {
770
1915
  game.presentScene(fixationScene);
771
1916
  } else {
772
- game.presentScene(
773
- doneScene,
774
- Transition.slide({
775
- direction: TransitionDirection.Left,
776
- duration: 500,
777
- easing: Easings.sinusoidalInOut
778
- })
779
- );
1917
+ if (game.getParameter("scoring")) {
1918
+ const scores = game.calculateScores(game.data.trials, {
1919
+ numberOfTrials: game.getParameter("number_of_trials")
1920
+ });
1921
+ game.addScoringData(scores);
1922
+ game.scoringComplete();
1923
+ }
1924
+ if (game.getParameter("show_trials_complete_scene")) {
1925
+ game.presentScene(
1926
+ doneScene,
1927
+ Transition.slide({
1928
+ direction: TransitionDirection.Left,
1929
+ duration: 500,
1930
+ easing: Easings.sinusoidalInOut
1931
+ })
1932
+ );
1933
+ } else {
1934
+ game.end();
1935
+ }
780
1936
  }
781
1937
  };
782
1938
  const doneScene = new Scene();
@@ -801,6 +1957,23 @@ class ColorShapes extends Game {
801
1957
  game.removeAllFreeNodes();
802
1958
  });
803
1959
  }
1960
+ calculateScores(data, extras) {
1961
+ const dc = new DataCalc(data);
1962
+ const scores = dc.summarize({
1963
+ activity_begin_iso8601_timestamp: this.beginIso8601Timestamp,
1964
+ first_trial_begin_iso8601_timestamp: dc.arrange("trial_begin_iso8601_timestamp").slice(0).pull("trial_begin_iso8601_timestamp"),
1965
+ last_trial_end_iso8601_timestamp: dc.arrange("-trial_end_iso8601_timestamp").slice(0).pull("trial_end_iso8601_timestamp"),
1966
+ n_trials: dc.length,
1967
+ flag_trials_match_expected: dc.length === extras.numberOfTrials ? 1 : 0,
1968
+ n_trials_correct: dc.filter((obs) => obs.user_response_correct === true).length,
1969
+ n_trials_incorrect: dc.filter(
1970
+ (obs) => obs.user_response_correct === false
1971
+ ).length
1972
+ }).mutate({
1973
+ participant_score: (obs) => obs.n_trials > 0 ? obs.n_trials_correct / obs.n_trials * 100 : null
1974
+ });
1975
+ return scores.observations;
1976
+ }
804
1977
  makeShapes(svgHeight) {
805
1978
  const shape01 = new Shape({
806
1979
  path: {