@m2c2kit/cli 0.3.6 → 0.3.8

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.
@@ -1,586 +0,0 @@
1
- import {
2
- Game,
3
- Action,
4
- Scene,
5
- Label,
6
- WebColors,
7
- RandomDraws,
8
- GameParameters,
9
- GameOptions,
10
- TrialSchema,
11
- Timer,
12
- Session,
13
- SessionLifecycleEvent,
14
- ActivityResultsEvent,
15
- ActivityLifecycleEvent,
16
- ActivityType,
17
- EventType,
18
- RgbaColor,
19
- } from "@m2c2kit/core";
20
- import { Button, Instructions } from "@m2c2kit/addons";
21
-
22
- class {{className}} extends Game {
23
- constructor() {
24
- /**
25
- * These are configurable game parameters and their defaults.
26
- * Each game parameter should have a type, default (this is the default
27
- * value), and a description.
28
- */
29
- const defaultParameters: GameParameters = {
30
- preparation_duration_ms: {
31
- type: "number",
32
- default: 500,
33
- description: "How long the 'get ready' message is shown, milliseconds.",
34
- },
35
- number_of_trials: {
36
- type: "integer",
37
- default: 5,
38
- description: "How many trials to run.",
39
- },
40
- show_fps: {
41
- type: "boolean",
42
- default: false,
43
- description: "Should the FPS be shown?",
44
- },
45
- };
46
-
47
- /**
48
- * This describes all the data that will be generated by the assessment
49
- * in a single trial. Each variable should have a type and description.
50
- * If a variable might be null, the type can be an array:
51
- * type: ["string", "null"]
52
- * Object and array types are also allowed, but this example uses only
53
- * simple types.
54
- *
55
- * More advanced schema parameters such as format or enum are optional.
56
- *
57
- * At runtime, when a trial completes, the data will be returned to the
58
- * session with a callback, along with this schema transformed into
59
- * JSON Schema.
60
- */
61
- const trialSchema: TrialSchema = {
62
- activity_uuid: {
63
- type: "string",
64
- format: "uuid",
65
- description: "Unique identifier for all trials in this activity.",
66
- },
67
- trial_index: {
68
- type: ["integer", "null"],
69
- description: "Index of the trial within this assessment, 0-based.",
70
- },
71
- presented_word_text: {
72
- type: "string",
73
- description: "The text that was presented.",
74
- },
75
- presented_word_color: {
76
- type: "string",
77
- description: "The color of the text that was presented.",
78
- },
79
- selected_text: {
80
- type: "string",
81
- description: "The text that was selected by the user.",
82
- },
83
- selection_correct: {
84
- type: "boolean",
85
- description: "Was the text selected correctly?",
86
- },
87
- response_time_ms: {
88
- type: "number",
89
- description:
90
- "How long, in milliseconds, from when the word was presented until the user made a selection.",
91
- },
92
- };
93
-
94
- const options: GameOptions = {
95
- name: "{{appName}}",
96
- id: "{{appName}}",
97
- version: "1.0.0",
98
- shortDescription:
99
- "A starter assessment created by the m2c2kit cli demonstrating the Stroop effect.",
100
- longDescription: `In psychology, the Stroop effect is the delay in \
101
- reaction time between congruent and incongruent stimuli. The effect has \
102
- been used to create a psychological test (the Stroop test) that is widely \
103
- used in clinical practice and investigation. A basic task that demonstrates \
104
- this effect occurs when there is a mismatch between the name of a color \
105
- (e.g., "blue", "green", or "red") and the color it is printed on (i.e., the \
106
- word "red" printed in blue ink instead of red ink). When asked to name the \
107
- color of the word it takes longer and is more prone to errors when the color \
108
- of the ink does not match the name of the color. The effect is named after \
109
- John Ridley Stroop, who first published the effect in English in 1935. The \
110
- effect had previously been published in Germany in 1929 by other authors. \
111
- The original paper by Stroop has been one of the most cited papers in the \
112
- history of experimental psychology, leading to more than 700 Stroop-related \
113
- articles in literature. Source: https://en.wikipedia.org/wiki/Stroop_effect`,
114
- uri: "An external link to your assessment repository or informational website.",
115
- showFps: defaultParameters.show_fps.default,
116
- /**
117
- * Actual pixel resolution will be scaled to fit the device, while
118
- * preserving the aspect ratio. It is important, however, to specify
119
- * a width and height to obtain the desired aspect ratio. In most
120
- * cases, you should not change this. 1:2 is a good aspect ratio
121
- * for modern phones.
122
- */
123
- width: 400,
124
- height: 800,
125
- trialSchema: trialSchema,
126
- parameters: defaultParameters,
127
- /**
128
- * IMPORTANT: fonts is an array of FontAsset objects. The url for each
129
- * fontAsset must be a string literal. If you use anything else, the
130
- * cache busting functionality will not work when building for
131
- * production.
132
- * The following are examples of what should NOT be used, even though
133
- * they are syntactically correct:
134
- *
135
- * url: "fonts/" + "roboto/Roboto-Regular.ttf"
136
- *
137
- * const prefix = "fonts/";
138
- * ...
139
- * url: prefix + "roboto/Roboto-Regular.ttf"
140
- */
141
- /**
142
- * The Roboto-Regular.ttf font is licensed under the Apache License,
143
- * and its LICENSE.TXT will be copied as part of the build.
144
- */
145
- fonts: [
146
- {
147
- fontName: "roboto",
148
- url: "fonts/roboto/Roboto-Regular.ttf",
149
- },
150
- ],
151
- /**
152
- * IMPORTANT: Similar to FontAsset.url, the url for an image must be
153
- * a string literal.
154
- */
155
- images: [
156
- {
157
- imageName: "assessmentImage",
158
- /**
159
- * The image will be resized to the height and width specified.
160
- */
161
- height: 441,
162
- width: 255,
163
- /**
164
- * The image url must match the location of the image under the
165
- * src folder.
166
- */
167
- url: "images/assessmentExample.png",
168
- },
169
- ],
170
- };
171
-
172
- super(options);
173
- }
174
-
175
- override async init() {
176
- await super.init();
177
- /**
178
- * Just for convenience, alias the variable game to "this"
179
- * (even though eslint doesn't like it)
180
- */
181
- // eslint-disable-next-line @typescript-eslint/no-this-alias
182
- const game = this;
183
-
184
- // ==============================================================
185
- /**
186
- * Create the trial configuration of all trials.
187
- * It is often necessary to create the full trial configuration before
188
- * starting any trials. For example, in the stroop task, one could
189
- * add game parameters to specify a certain percent of the correct
190
- * responses are the left versus right buttons. Or, one might want an
191
- * equal number of trials for each font color.
192
- */
193
-
194
- interface StroopColor {
195
- name: string;
196
- rgba: RgbaColor;
197
- }
198
-
199
- /**
200
- * These are the colors that will be used in the game.
201
- */
202
- const stroopColors: StroopColor[] = [
203
- { name: "Red", rgba: [255, 0, 0, 1] },
204
- { name: "Green", rgba: [0, 255, 0, 1] },
205
- { name: "Blue", rgba: [0, 0, 255, 1] },
206
- { name: "Orange", rgba: [255, 165, 0, 1] },
207
- ];
208
-
209
- /**
210
- * This completely describes the configuration of single trial.
211
- */
212
- interface TrialConfiguration {
213
- presented_text: string;
214
- presented_color: StroopColor;
215
- selection_options_text: string[];
216
- correct_option_index: number;
217
- }
218
-
219
- const trialConfigurations: TrialConfiguration[] = [];
220
-
221
- /**
222
- * Note: TypeScript will try to infer the type of the game parameter that
223
- * you request in game.getParameter(). If the type cannot be inferred, you
224
- * will get a compiler error, and you must specify the type, as in the
225
- * next statement.
226
- */
227
- for (let i = 0; i < game.getParameter<number>("number_of_trials"); i++) {
228
- const presentedTextIndex = RandomDraws.SingleFromRange(
229
- 0,
230
- stroopColors.length - 1
231
- );
232
- const presentedColorIndex = RandomDraws.SingleFromRange(
233
- 0,
234
- stroopColors.length - 1
235
- );
236
-
237
- const selection_options_text = new Array<string>();
238
- const firstIsCorrect = RandomDraws.SingleFromRange(0, 1);
239
- let correctOptionIndex;
240
-
241
- if (firstIsCorrect === 1) {
242
- correctOptionIndex = 0;
243
- selection_options_text.push(stroopColors[presentedColorIndex].name);
244
- const remainingColors = stroopColors.filter(
245
- (c) => c.name !== stroopColors[presentedColorIndex].name
246
- );
247
- const secondOptionIndex = RandomDraws.SingleFromRange(
248
- 0,
249
- remainingColors.length - 1
250
- );
251
- selection_options_text.push(remainingColors[secondOptionIndex].name);
252
- } else {
253
- correctOptionIndex = 1;
254
- const remainingColors = stroopColors.filter(
255
- (c) => c.name !== stroopColors[presentedColorIndex].name
256
- );
257
- const secondOptionIndex = RandomDraws.SingleFromRange(
258
- 0,
259
- remainingColors.length - 1
260
- );
261
- selection_options_text.push(remainingColors[secondOptionIndex].name);
262
- selection_options_text.push(stroopColors[presentedColorIndex].name);
263
- }
264
-
265
- trialConfigurations.push({
266
- presented_text: stroopColors[presentedTextIndex].name,
267
- presented_color: stroopColors[presentedColorIndex],
268
- selection_options_text: selection_options_text,
269
- correct_option_index: correctOptionIndex,
270
- });
271
- }
272
-
273
- // ==============================================================
274
- // SCENES: instructions
275
- const instructionsScenes = Instructions.Create({
276
- instructionScenes: [
277
- {
278
- title: "{{appName}}",
279
- text: `Select the color that matches the font color. This is commonly known as the Stroop task.`,
280
- textFontSize: 20,
281
- titleFontSize: 30,
282
- },
283
- {
284
- title: "{{appName}}",
285
- text: `For example, the word Blue is colored Orange, so the correct response is Orange.`,
286
- textFontSize: 20,
287
- titleFontSize: 30,
288
- textVerticalBias: 0.15,
289
- imageName: "assessmentImage",
290
- imageAboveText: false,
291
- imageMarginTop: 20,
292
- /**
293
- * We override the next button's default text and color
294
- */
295
- nextButtonText: "START",
296
- nextButtonBackgroundColor: WebColors.Green,
297
- },
298
- ],
299
- });
300
- game.addScenes(instructionsScenes);
301
-
302
- // ==============================================================
303
- // SCENE: preparation. Show get ready message, then advance after
304
- // preparation_duration_ms milliseconds
305
-
306
- /**
307
- * For entities that are persistent across trials, such as the
308
- * scenes themsevles and labels that are always displayed, we create
309
- * them here.
310
- */
311
- const preparationScene = new Scene();
312
- game.addScene(preparationScene);
313
-
314
- const getReadyMessage = new Label({
315
- text: "Get Ready",
316
- fontSize: 24,
317
- position: { x: 200, y: 400 },
318
- });
319
- preparationScene.addChild(getReadyMessage);
320
-
321
- /**
322
- * For entities that are displayed or actions that are run only when a
323
- * scene has been presented, we do them within the scene's onAppear()
324
- * or onSetup() callbacks. When a scene is presented, the order of
325
- * execution is:
326
- * OnSetup() -> transitions -> OnAppear()
327
- * If there are no transitions, such as a scene sliding in, then
328
- * it makes no difference if you put code in OnSetup() or OnAppear().
329
- */
330
- preparationScene.onAppear(() => {
331
- preparationScene.run(
332
- Action.sequence([
333
- Action.wait({
334
- duration: game.getParameter("preparation_duration_ms"),
335
- }),
336
- // Custom actions are used execute code.
337
- Action.custom({
338
- callback: () => {
339
- game.presentScene(presentationScene);
340
- },
341
- }),
342
- ])
343
- );
344
- });
345
-
346
- // ==============================================================
347
- // SCENE: Present the word and get user selection
348
- const presentationScene = new Scene();
349
- game.addScene(presentationScene);
350
-
351
- /**
352
- * The "What colors is the font?" label will always be displayed in
353
- * the presentation scene, so we create it here and add it to the scene.
354
- */
355
- const whatColorIsFont = new Label({
356
- text: "What color is the font?",
357
- fontSize: 24,
358
- position: { x: 200, y: 100 },
359
- });
360
- presentationScene.addChild(whatColorIsFont);
361
-
362
- presentationScene.onAppear(() => {
363
- Timer.start("responseTime");
364
- const trialConfiguration = trialConfigurations[game.trialIndex];
365
-
366
- /**
367
- * The presented word will vary across trials. Thus, we create the
368
- * presented word label here within the scene's onAppear() callback.
369
- */
370
- const presentedWord = new Label({
371
- text: trialConfiguration.presented_text,
372
- position: { x: 200, y: 400 },
373
- fontSize: 48,
374
- fontColor: trialConfiguration.presented_color.rgba,
375
- });
376
- presentationScene.addChild(presentedWord);
377
-
378
- /**
379
- * Similarly, we create the buttons within the scene's onAppear()
380
- * callback because the buttons are different across trials.
381
- */
382
- const button0 = new Button({
383
- text: trialConfiguration.selection_options_text[0],
384
- size: { width: 150, height: 50 },
385
- position: { x: 100, y: 700 },
386
- isUserInteractionEnabled: true,
387
- });
388
- button0.onTapDown(() => {
389
- handleUserSelection(0);
390
- });
391
- presentationScene.addChild(button0);
392
-
393
- const button1 = new Button({
394
- text: trialConfiguration.selection_options_text[1],
395
- size: { width: 150, height: 50 },
396
- position: { x: 300, y: 700 },
397
- isUserInteractionEnabled: true,
398
- });
399
- button1.onTapDown(() => {
400
- handleUserSelection(1);
401
- });
402
- presentationScene.addChild(button1);
403
-
404
- function handleUserSelection(selectionIndex: number): void {
405
- /**
406
- * Set both buttons' isUserInteractionEnabled to false to prevent
407
- * double taps.
408
- */
409
- button0.isUserInteractionEnabled = false;
410
- button1.isUserInteractionEnabled = false;
411
- Timer.stop("responseTime");
412
- game.addTrialData("response_time_ms", Timer.elapsed("responseTime"));
413
- Timer.remove("responseTime");
414
- game.addTrialData(
415
- "presented_word_text",
416
- trialConfiguration.presented_text
417
- );
418
- game.addTrialData(
419
- "presented_word_color",
420
- trialConfiguration.presented_color.name
421
- );
422
- game.addTrialData(
423
- "selected_text",
424
- trialConfiguration.selection_options_text[selectionIndex]
425
- );
426
- const correct =
427
- trialConfiguration.correct_option_index === selectionIndex;
428
- game.addTrialData("selection_correct", correct);
429
- game.addTrialData("trial_index", game.trialIndex);
430
- game.addTrialData("activity_uuid", game.uuid);
431
- /**
432
- * When the trial has completed, you must call game.trialComplete() to
433
- * 1) Increase the game.trialIndex counter
434
- * 2) Trigger events that send the trial data to event subscribers
435
- */
436
- game.trialComplete();
437
- /**
438
- * When this trial is done, we must remove the presented word and the
439
- * buttons because they are not persistent across trials. We will
440
- * create new, different buttons and presented word labels in the next
441
- * trial.
442
- */
443
- presentationScene.removeChildren([presentedWord, button0, button1]);
444
- /**
445
- * Are we done all the trials, or should we do another trial
446
- * iteration?
447
- */
448
- if (game.trialIndex === game.getParameter<number>("number_of_trials")) {
449
- game.presentScene(doneScene);
450
- } else {
451
- game.presentScene(preparationScene);
452
- }
453
- }
454
- });
455
-
456
- // ==============================================================
457
- // SCENE: Done. Show done message, with a button to exit.
458
- const doneScene = new Scene();
459
- game.addScene(doneScene);
460
-
461
- const doneSceneText = new Label({
462
- text: "You have completed all the cli starter trials",
463
- position: { x: 200, y: 400 },
464
- });
465
- doneScene.addChild(doneSceneText);
466
-
467
- const okButton = new Button({
468
- text: "OK",
469
- position: { x: 200, y: 600 },
470
- });
471
- okButton.isUserInteractionEnabled = true;
472
- okButton.onTapDown(() => {
473
- // Don't allow repeat taps of ok button
474
- okButton.isUserInteractionEnabled = false;
475
- doneScene.removeAllChildren();
476
- /**
477
- * When the game is done, you must call game.end() to transfer control
478
- * back to the Session, which will then start the next activity or
479
- * send a session end event to the event subscribers.
480
- */
481
- game.end();
482
- });
483
- doneScene.addChild(okButton);
484
- }
485
- }
486
-
487
- const activity = new {{className}}();
488
- const session = new Session({
489
- activities: [activity],
490
- canvasKitWasmUrl: "canvaskit.wasm",
491
- sessionCallbacks: {
492
- /**
493
- * onSessionLifecycle() will be called on events such
494
- * as when the session initialization is complete or when the
495
- * session ends.
496
- *
497
- * Once initialized, the below code will start the session.
498
- */
499
- onSessionLifecycle: async (ev: SessionLifecycleEvent) => {
500
- if (ev.type === EventType.SessionInitialize) {
501
- await session.start();
502
- }
503
- if (ev.type === EventType.SessionEnd) {
504
- console.log("🔴 ended session");
505
- }
506
- },
507
- },
508
- activityCallbacks: {
509
- /**
510
- * onActivityResults() callback is where you insert code to post data
511
- * to an API or interop with a native function in the host app,
512
- * if applicable.
513
- *
514
- * newData is the data that was just generated by the completed trial or
515
- * survey question.
516
- * data is all the data, cumulative of all trials or questions in the
517
- * activity, that have been generated.
518
- *
519
- * We separate out newData from data in case you want to alter the execution
520
- * based on the most recent trial, e.g., maybe you want to stop after
521
- * a certain user behavior or performance threshold in the just completed
522
- * trial.
523
- *
524
- * activityConfiguration is the game parameters that were used.
525
- *
526
- * The schema for all of the above are in JSON Schema format.
527
- * Currently, only games generate schema.
528
- */
529
- onActivityResults: (ev: ActivityResultsEvent) => {
530
- if (ev.target.type === ActivityType.Game) {
531
- console.log(`✅ trial complete:`);
532
- } else if (ev.target.type === ActivityType.Survey) {
533
- console.log(`✅ question answered:`);
534
- }
535
- console.log(" newData: " + JSON.stringify(ev.newData));
536
- console.log(" newData schema: " + JSON.stringify(ev.newDataSchema));
537
- console.log(" data: " + JSON.stringify(ev.data));
538
- console.log(" data schema: " + JSON.stringify(ev.dataSchema));
539
- console.log(
540
- " activity parameters: " + JSON.stringify(ev.activityConfiguration)
541
- );
542
- console.log(
543
- " activity parameters schema: " +
544
- JSON.stringify(ev.activityConfigurationSchema)
545
- );
546
- console.log(" activity metrics: " + JSON.stringify(ev.activityMetrics));
547
- },
548
- /**
549
- * onActivityLifecycle() notifies us when an activity, such
550
- * as a game (assessment) or a survey, has ended or canceled.
551
- * Usually, however, we want to know when all the activities are done,
552
- * so we'll look for the session ending via onSessionLifecycleChange
553
- */
554
- onActivityLifecycle: async (ev: ActivityLifecycleEvent) => {
555
- const activityType =
556
- ev.target.type === ActivityType.Game ? "game" : "survey";
557
-
558
- if (ev.type === EventType.ActivityStart) {
559
- console.log(`🟢 started activity (${activityType}) ${ev.target.name}`);
560
- }
561
-
562
- if (
563
- ev.type === EventType.ActivityCancel ||
564
- ev.type === EventType.ActivityEnd
565
- ) {
566
- const status =
567
- ev.type === EventType.ActivityEnd ? "🔴 ended" : "🚫 canceled";
568
- console.log(`${status} activity (${activityType}) ${ev.target.name}`);
569
- if (session.nextActivity) {
570
- await session.goToNextActivity();
571
- } else {
572
- session.end();
573
- }
574
- }
575
- },
576
- },
577
- });
578
-
579
- /**
580
- * Make session also available on window in case we want to control
581
- * the session through another means, such as other javascript or
582
- * browser code, or a mobile WebView's invocation of session.start().
583
- * */
584
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
585
- (window as unknown as any).session = session;
586
- session.init();
@@ -1,15 +0,0 @@
1
- {
2
- // Use IntelliSense to learn about possible attributes.
3
- // Hover to view descriptions of existing attributes.
4
- // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
- "version": "0.2.0",
6
- "configurations": [
7
- {
8
- "type": "chrome",
9
- "request": "launch",
10
- "name": "Launch Chrome against localhost",
11
- "url": "http://localhost:3000",
12
- "webRoot": "${workspaceFolder}/build"
13
- }
14
- ]
15
- }
@@ -1,5 +0,0 @@
1
- {
2
- "server": {
3
- "studyCode": "{{studyCode}}"
4
- }
5
- }
@@ -1,26 +0,0 @@
1
- {
2
- "name": "{{appName}}",
3
- "version": "1.0.0",
4
- "scripts": {
5
- "serve": "rollup -c rollup.config.mjs --watch --configServe",
6
- "build": "npm run clean && rollup -c rollup.config.mjs --configProd",
7
- "clean": "rimraf build dist .rollup.cache tsconfig.tsbuildinfo"
8
- },
9
- "private": true,
10
- "dependencies": {
11
- "@m2c2kit/addons": "0.3.3",
12
- "@m2c2kit/core": "0.3.6"
13
- },
14
- "devDependencies": {
15
- "@m2c2kit/build-helpers": "0.3.3",
16
- "@rollup/plugin-node-resolve": "15.0.2",
17
- "@rollup/plugin-typescript": "11.1.1",
18
- "rimraf": "5.0.0",
19
- "rollup": "3.22.0",
20
- "rollup-plugin-copy": "3.4.0",
21
- "rollup-plugin-livereload": "2.0.5",
22
- "rollup-plugin-serve": "2.0.2",
23
- "tslib": "2.5.0",
24
- "typescript": "5.0.4"
25
- }
26
- }