jspsych-tangram 0.0.11 → 0.0.13

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.
Files changed (46) hide show
  1. package/dist/construct/index.browser.js +4805 -3935
  2. package/dist/construct/index.browser.js.map +1 -1
  3. package/dist/construct/index.browser.min.js +13 -13
  4. package/dist/construct/index.browser.min.js.map +1 -1
  5. package/dist/construct/index.cjs +289 -71
  6. package/dist/construct/index.cjs.map +1 -1
  7. package/dist/construct/index.d.ts +36 -0
  8. package/dist/construct/index.js +289 -71
  9. package/dist/construct/index.js.map +1 -1
  10. package/dist/index.cjs +399 -100
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +84 -0
  13. package/dist/index.js +399 -100
  14. package/dist/index.js.map +1 -1
  15. package/dist/nback/index.browser.js +4629 -3939
  16. package/dist/nback/index.browser.js.map +1 -1
  17. package/dist/nback/index.browser.min.js +12 -12
  18. package/dist/nback/index.browser.min.js.map +1 -1
  19. package/dist/nback/index.cjs +102 -64
  20. package/dist/nback/index.cjs.map +1 -1
  21. package/dist/nback/index.d.ts +24 -0
  22. package/dist/nback/index.js +102 -64
  23. package/dist/nback/index.js.map +1 -1
  24. package/dist/prep/index.browser.js +4803 -3941
  25. package/dist/prep/index.browser.js.map +1 -1
  26. package/dist/prep/index.browser.min.js +13 -13
  27. package/dist/prep/index.browser.min.js.map +1 -1
  28. package/dist/prep/index.cjs +285 -75
  29. package/dist/prep/index.cjs.map +1 -1
  30. package/dist/prep/index.d.ts +24 -0
  31. package/dist/prep/index.js +285 -75
  32. package/dist/prep/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/core/components/board/BoardView.tsx +372 -124
  35. package/src/core/components/board/GameBoard.tsx +128 -91
  36. package/src/core/components/pieces/BlueprintRing.tsx +105 -47
  37. package/src/core/config/config.ts +25 -10
  38. package/src/plugins/tangram-construct/ConstructionApp.tsx +7 -1
  39. package/src/plugins/tangram-construct/index.ts +22 -1
  40. package/src/plugins/tangram-nback/NBackApp.tsx +87 -28
  41. package/src/plugins/tangram-nback/index.ts +14 -0
  42. package/src/plugins/tangram-prep/PrepApp.tsx +7 -1
  43. package/src/plugins/tangram-prep/index.ts +14 -0
  44. package/tangram-construct.min.js +13 -13
  45. package/tangram-nback.min.js +12 -12
  46. package/tangram-prep.min.js +13 -13
@@ -1,30 +1,30 @@
1
1
  /**
2
2
  * GameBoard - Core reusable tangram game board component
3
- *
3
+ *
4
4
  * This component provides the complete tangram game functionality that can be
5
5
  * shared between jsPsych plugins. It handles both construction and preparation
6
6
  * game modes with full interaction support.
7
- *
7
+ *
8
8
  * ## Key Features
9
9
  * - **Dependency Injection**: All game content provided via props (no hardcoded data)
10
10
  * - **Multiple Input Modes**: Supports both click and drag interactions
11
11
  * - **Layout Flexibility**: Circle and semicircle layout modes
12
12
  * - **Event Tracking**: Comprehensive data collection for research
13
13
  * - **State Management**: Uses BaseGameController for robust state handling
14
- *
14
+ *
15
15
  * ## Architecture
16
16
  * - Uses modern React patterns with hooks
17
17
  * - Integrates with BaseGameController for state management
18
18
  * - Renders via Board component with computed layout
19
19
  * - Handles completion events and data collection
20
- *
20
+ *
21
21
  * ## Usage in Plugins
22
22
  * ```typescript
23
23
  * // In jsPsych plugin trial method:
24
24
  * const root = createRoot(display_element);
25
25
  * root.render(React.createElement(GameBoard, {
26
26
  * sectors: convertedSectors,
27
- * quickstash: convertedMacros,
27
+ * quickstash: convertedMacros,
28
28
  * primitives: PRIMITIVE_BLUEPRINTS,
29
29
  * layout: "semicircle",
30
30
  * target: "silhouette",
@@ -32,7 +32,7 @@
32
32
  * onComplete: handleTrialComplete
33
33
  * }));
34
34
  * ```
35
- *
35
+ *
36
36
  * @see {@link BaseGameController} For state management details
37
37
  * @see {@link Board} For rendering implementation
38
38
  * @since Phase 3.3 - Extracted from monolithic Board component
@@ -41,9 +41,9 @@
41
41
 
42
42
  import React from "react";
43
43
  import { BaseGameController } from "@/core/engine/state/BaseGameController";
44
- import type {
45
- Sector,
46
- Blueprint,
44
+ import type {
45
+ Sector,
46
+ Blueprint,
47
47
  PrimitiveBlueprint,
48
48
  LayoutMode,
49
49
  PlacementTarget,
@@ -54,8 +54,8 @@ import { solveLogicalBox } from "@/core/domain/solve";
54
54
  import type { Poly } from "@/core/domain/types";
55
55
  import { CONFIG } from "@/core/config/config";
56
56
  import BoardView, { type AnchorDots as DotsType } from "./BoardView";
57
- import {
58
- inferUnitFromPolys,
57
+ import {
58
+ inferUnitFromPolys,
59
59
  placeSilhouetteGridAlignedAsPolys } from "@/core/engine/geometry";
60
60
  import { anchorsSilhouetteComplete, anchorsWorkspaceComplete } from "@/core/engine/validation/complete";
61
61
  import { scalePolys } from "@/core/components/board/utils";
@@ -67,109 +67,109 @@ import { InteractionTracker } from "@/core/io/InteractionTracker";
67
67
 
68
68
  /**
69
69
  * Configuration interface for GameBoard component
70
- *
70
+ *
71
71
  * This interface defines all the required game content and behavior settings
72
- * that must be provided to the GameBoard component. All properties use
72
+ * that must be provided to the GameBoard component. All properties use
73
73
  * dependency injection - no defaults are provided.
74
74
  */
75
75
  export interface GameBoardConfig {
76
- /**
76
+ /**
77
77
  * Array of sector definitions containing target silhouettes to construct
78
78
  * Each sector represents one puzzle to solve
79
79
  */
80
80
  sectors: Sector[];
81
-
82
- /**
81
+
82
+ /**
83
83
  * Array of pre-made composite piece blueprints (macros)
84
84
  * Usually created in prep trials and passed to construction trials
85
85
  */
86
86
  quickstash: Blueprint[];
87
-
88
- /**
87
+
88
+ /**
89
89
  * Array of primitive tangram piece definitions
90
90
  * These are the basic 7 tangram shapes that never change
91
91
  */
92
92
  primitives: PrimitiveBlueprint[];
93
-
94
- /**
93
+
94
+ /**
95
95
  * Layout mode for the game board
96
96
  * - "circle": Full circular layout with pieces arranged in complete circle
97
97
  * - "semicircle": Half-circle layout with pieces in arc above workspace
98
98
  */
99
99
  layout: LayoutMode;
100
-
101
- /**
100
+
101
+ /**
102
102
  * Target placement mode for piece validation
103
103
  * - "workspace": Pieces can be placed freely in workspace area
104
104
  * - "silhouette": Pieces must be placed within target silhouette bounds
105
105
  */
106
106
  target: PlacementTarget;
107
-
108
- /**
107
+
108
+ /**
109
109
  * Input interaction mode
110
110
  * - "click": Click to select pieces, click to place (better for touch)
111
111
  * - "drag": Drag pieces directly with mouse/touch
112
112
  */
113
113
  input: InputMode;
114
-
115
- /**
114
+
115
+ /**
116
116
  * Time limit for the trial in milliseconds
117
117
  * Set to 0 for no time limit
118
118
  */
119
119
  timeLimitMs: number;
120
-
121
- /**
120
+
121
+ /**
122
122
  * Maximum number of quickstash/macro slots to display
123
123
  * Controls size of the macro selection ring
124
124
  */
125
125
  maxQuickstashSlots: number;
126
-
127
- /**
126
+
127
+ /**
128
128
  * Maximum number of primitive pieces allowed in a composite
129
129
  * Used for validation when creating macros (prep mode only)
130
130
  */
131
131
  maxCompositeSize?: number;
132
-
132
+
133
133
  /**
134
134
  * Game mode to determine behavior differences
135
135
  * - "construction": Standard puzzle-solving mode with toggle
136
136
  * - "prep": Macro creation mode with primitives-only center
137
137
  */
138
138
  mode?: "construction" | "prep";
139
-
139
+
140
140
  /**
141
141
  * Minimum pieces required per macro (prep mode only)
142
142
  * Used for submit button validation
143
143
  */
144
144
  minPiecesPerMacro?: number;
145
-
145
+
146
146
  /**
147
147
  * Whether all slots must be filled to complete trial (prep mode only)
148
148
  * Used for submit button validation
149
149
  */
150
150
  requireAllSlots?: boolean;
151
-
151
+
152
152
  /**
153
153
  * Whether to show tangram target shapes decomposed into individual primitives with borders
154
154
  * Independent from showBorders/hideTouchingBorders (which control spawned pieces)
155
155
  */
156
156
  showTangramDecomposition?: boolean;
157
-
157
+
158
158
  /**
159
159
  * HTML content to display above the gameboard as instructions
160
160
  * Allows rich formatting via HTML
161
161
  */
162
162
  instructions?: string;
163
-
163
+
164
164
  /**
165
165
  * Cleaned jsPsych plugin parameters (excluding callbacks)
166
166
  * Saved to trial data for analysis
167
167
  */
168
168
  trialParams?: any;
169
-
169
+
170
170
  /** Optional CSS width for the game board SVG */
171
171
  width?: number;
172
-
172
+
173
173
  /** Optional CSS height for the game board SVG */
174
174
  height?: number;
175
175
  }
@@ -218,29 +218,47 @@ export interface GameBoardProps extends GameBoardConfig {
218
218
  * Useful for spawning initial pieces or setting up state
219
219
  */
220
220
  onControllerReady?: (controller: BaseGameController, layout?: any, force?: () => void) => void;
221
+
222
+ /**
223
+ * Whether to use distinct colors for each primitive shape type in blueprints
224
+ * When true, each primitive type (square, triangle, etc.) gets its own color in dock area
225
+ */
226
+ usePrimitiveColorsBlueprints?: boolean;
227
+
228
+ /**
229
+ * Whether to use distinct colors for each primitive shape type in target tangrams
230
+ * When true, each primitive type gets its own color in target silhouettes
231
+ */
232
+ usePrimitiveColorsTargets?: boolean;
233
+
234
+ /**
235
+ * Array of 5 integers mapping primitives to colors from CONFIG.color.primitiveColors
236
+ * Maps [square, smalltriangle, parallelogram, medtriangle, largetriangle] to color indices
237
+ */
238
+ primitiveColorIndices?: number[];
221
239
  }
222
240
 
223
241
  /**
224
242
  * Core GameBoard component that encapsulates all tangram game functionality
225
- *
243
+ *
226
244
  * This is the main reusable component that provides complete tangram game
227
245
  * functionality for jsPsych plugins. It handles all aspects of the game
228
246
  * including piece interaction, collision detection, completion validation,
229
247
  * and data collection.
230
- *
248
+ *
231
249
  * ## Key Responsibilities
232
250
  * - Creates and manages BaseGameController for state management
233
- * - Computes layout geometry for board rendering
251
+ * - Computes layout geometry for board rendering
234
252
  * - Handles event callbacks for plugin integration
235
253
  * - Manages CSS sizing for responsive display
236
- *
254
+ *
237
255
  * ## State Management
238
256
  * Uses BaseGameController internally which provides:
239
257
  * - Piece placement and validation
240
258
  * - Sector completion tracking
241
259
  * - Event logging for data collection
242
260
  * - Collision detection and snapping
243
- *
261
+ *
244
262
  * @param props - Configuration and event callbacks for the game
245
263
  * @returns React component rendering the complete tangram game interface
246
264
  */
@@ -266,12 +284,15 @@ export default function GameBoard(props: GameBoardProps) {
266
284
  onPieceRemove,
267
285
  onInteraction,
268
286
  onTrialEnd,
269
- onControllerReady
287
+ onControllerReady,
288
+ usePrimitiveColorsBlueprints,
289
+ usePrimitiveColorsTargets,
290
+ primitiveColorIndices,
270
291
  } = props;
271
-
292
+
272
293
  // Timer state for countdown
273
294
  const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1000) : 0);
274
-
295
+
275
296
  // Initialize game controller with injected data
276
297
  const controller = React.useMemo(() => {
277
298
  const gameConfig = {
@@ -286,11 +307,11 @@ export default function GameBoard(props: GameBoardProps) {
286
307
  ...(props.minPiecesPerMacro !== undefined && { minPiecesPerMacro: props.minPiecesPerMacro }),
287
308
  ...(props.requireAllSlots !== undefined && { requireAllSlots: props.requireAllSlots })
288
309
  };
289
-
310
+
290
311
  return new BaseGameController(
291
312
  sectors,
292
313
  quickstash,
293
- primitives,
314
+ primitives,
294
315
  gameConfig
295
316
  );
296
317
  }, [sectors, quickstash, primitives, layoutMode, target, input, timeLimitMs, maxQuickstashSlots, maxCompositeSize, mode, props.minPiecesPerMacro, props.requireAllSlots]);
@@ -322,12 +343,12 @@ export default function GameBoard(props: GameBoardProps) {
322
343
  };
323
344
  }, [tracker]);
324
345
 
325
- // Compute layout geometry
346
+ // Compute layout geometry
326
347
  const { layout, viewBox } = React.useMemo(() => {
327
348
  const nSectors = sectors.length;
328
349
  const sectorIds = sectors.map(s => s.id);
329
350
  const masksPerSector: Poly[][] = sectors.map(s => s.silhouette.mask ?? []);
330
-
351
+
331
352
  const logicalBox = solveLogicalBox({
332
353
  n: nSectors,
333
354
  layoutMode,
@@ -337,7 +358,7 @@ export default function GameBoard(props: GameBoardProps) {
337
358
  layoutPadPx: CONFIG.layout.paddingPx,
338
359
  masks: masksPerSector,
339
360
  });
340
-
361
+
341
362
  const layout = computeCircleLayout(
342
363
  { w: logicalBox.LOGICAL_W, h: logicalBox.LOGICAL_H },
343
364
  nSectors,
@@ -350,7 +371,7 @@ export default function GameBoard(props: GameBoardProps) {
350
371
  masks: masksPerSector,
351
372
  }
352
373
  );
353
-
374
+
354
375
  return {
355
376
  layout,
356
377
  viewBox: { w: logicalBox.LOGICAL_W, h: logicalBox.LOGICAL_H }
@@ -366,10 +387,10 @@ export default function GameBoard(props: GameBoardProps) {
366
387
  onControllerReady(controller, layout, force);
367
388
  }
368
389
  }, [controller, layout, onControllerReady, force]);
369
-
390
+
370
391
  // Game completion tracking
371
392
  const [gameCompleted, setGameCompleted] = React.useState(false);
372
-
393
+
373
394
  // Watch for game completion
374
395
  React.useEffect(() => {
375
396
  const checkGameCompletion = () => {
@@ -393,21 +414,21 @@ export default function GameBoard(props: GameBoardProps) {
393
414
  // Check completion whenever controller state updates
394
415
  checkGameCompletion();
395
416
  }, [controller.updateCount, sectors, gameCompleted, controller, tracker]);
396
-
417
+
397
418
  // Sector completion callback
398
419
  const handleSectorComplete = React.useCallback((sectorId: string) => {
399
420
  if (onSectorComplete) {
400
421
  onSectorComplete(sectorId, controller.snapshot());
401
422
  }
402
423
  }, [onSectorComplete, controller]);
403
-
424
+
404
425
  // Event callbacks for plugin integration
405
426
  const eventCallbacks = React.useMemo(() => ({
406
427
  onSectorComplete: handleSectorComplete,
407
428
  onPiecePlace: onPiecePlace || (() => {}),
408
429
  onPieceRemove: onPieceRemove || (() => {})
409
430
  }), [handleSectorComplete, onPiecePlace, onPieceRemove]);
410
-
431
+
411
432
  // Calculate sizing based on layout mode
412
433
  const getGameboardStyle = () => {
413
434
  const baseStyle = {
@@ -560,14 +581,14 @@ export default function GameBoard(props: GameBoardProps) {
560
581
  tracker // Pass tracker for data collection
561
582
  );
562
583
 
563
- const {
564
- draggingId,
565
- dragInvalid,
566
- svgRef,
567
- onPiecePointerDown,
568
- onBlueprintPointerDown,
569
- onPointerMove,
570
- onPointerUp,
584
+ const {
585
+ draggingId,
586
+ dragInvalid,
587
+ svgRef,
588
+ onPiecePointerDown,
589
+ onBlueprintPointerDown,
590
+ onPointerMove,
591
+ onPointerUp,
571
592
  setPieceRef,
572
593
  setDraggingId,
573
594
  lockedPieceId,
@@ -640,7 +661,7 @@ export default function GameBoard(props: GameBoardProps) {
640
661
  // Timer effect
641
662
  React.useEffect(() => {
642
663
  if (timeLimitMs === 0) return;
643
-
664
+
644
665
  const interval = setInterval(() => {
645
666
  setTimeRemaining(prev => {
646
667
  if (prev <= 1) {
@@ -650,7 +671,7 @@ export default function GameBoard(props: GameBoardProps) {
650
671
  return prev - 1;
651
672
  });
652
673
  }, 1000);
653
-
674
+
654
675
  return () => clearInterval(interval);
655
676
  }, [timeLimitMs]);
656
677
 
@@ -669,27 +690,38 @@ export default function GameBoard(props: GameBoardProps) {
669
690
  // minHeight: '100vh'
670
691
  };
671
692
 
672
- // Header style
693
+ // Header style (constrained to gameboard width)
673
694
  const headerStyle: React.CSSProperties = {
674
695
  display: 'flex',
675
696
  flexDirection: 'row',
676
- justifyContent: 'space-between',
697
+ justifyContent: 'center',
677
698
  alignItems: 'center',
678
699
  padding: '20px',
679
- background: '#f5f5f5',
700
+ background: CONFIG.color.background,
680
701
  flex: '0 0 auto'
681
702
  };
682
703
 
683
- // Instructions style
704
+ // Header content wrapper (matches gameboard width)
705
+ const headerContentStyle: React.CSSProperties = {
706
+ position: 'relative',
707
+ width: `${svgDimensions.width}px`,
708
+ maxWidth: '100%',
709
+ display: 'flex',
710
+ justifyContent: 'center',
711
+ alignItems: 'center'
712
+ };
713
+
714
+ // Instructions style (always centered)
684
715
  const instructionsStyle: React.CSSProperties = {
685
- flexGrow: 1,
686
716
  fontSize: '20px',
687
717
  lineHeight: 1.5,
688
- marginRight: '20px'
718
+ textAlign: 'center'
689
719
  };
690
720
 
691
- // Timer style
721
+ // Timer style (positioned absolutely to not affect centering)
692
722
  const timerStyle: React.CSSProperties = {
723
+ position: 'absolute',
724
+ right: 0,
693
725
  fontSize: '24px',
694
726
  fontWeight: 'bold',
695
727
  fontFamily: 'monospace',
@@ -711,18 +743,20 @@ export default function GameBoard(props: GameBoardProps) {
711
743
  <div className="tangram-trial-container" style={containerStyle}>
712
744
  {(instructions || timeLimitMs > 0) && (
713
745
  <div className="tangram-header" style={headerStyle}>
714
- {instructions && (
715
- <div
716
- className="tangram-instructions"
717
- style={instructionsStyle}
718
- dangerouslySetInnerHTML={{ __html: instructions }}
719
- />
720
- )}
721
- {timeLimitMs > 0 && (
722
- <div className="tangram-timer" style={timerStyle}>
723
- {formatTime(timeRemaining)}
724
- </div>
725
- )}
746
+ <div className="tangram-header-content" style={headerContentStyle}>
747
+ {instructions && (
748
+ <div
749
+ className="tangram-instructions"
750
+ style={instructionsStyle}
751
+ dangerouslySetInnerHTML={{ __html: instructions }}
752
+ />
753
+ )}
754
+ {timeLimitMs > 0 && (
755
+ <div className="tangram-timer" style={timerStyle}>
756
+ {formatTime(timeRemaining)}
757
+ </div>
758
+ )}
759
+ </div>
726
760
  </div>
727
761
  )}
728
762
  <div className="tangram-gameboard-wrapper" style={gameboardWrapperStyle}>
@@ -753,6 +787,9 @@ export default function GameBoard(props: GameBoardProps) {
753
787
  onCenterBadgePointerDown={onCenterBadgePointerDown}
754
788
  showTangramDecomposition={showTangramDecomposition ?? false}
755
789
  scaleS={scaleS}
790
+ usePrimitiveColorsBlueprints={usePrimitiveColorsBlueprints ?? false}
791
+ usePrimitiveColorsTargets={usePrimitiveColorsTargets ?? false}
792
+ primitiveColorIndices={primitiveColorIndices ?? [0, 1, 2, 3, 4]}
756
793
  {...eventCallbacks}
757
794
  />
758
795
  </div>
@@ -779,7 +816,7 @@ export function useGameBoard(config: GameBoardConfig) {
779
816
  ...(config.minPiecesPerMacro !== undefined && { minPiecesPerMacro: config.minPiecesPerMacro }),
780
817
  ...(config.requireAllSlots !== undefined && { requireAllSlots: config.requireAllSlots })
781
818
  };
782
-
819
+
783
820
  return new BaseGameController(
784
821
  config.sectors,
785
822
  config.quickstash,
@@ -787,12 +824,12 @@ export function useGameBoard(config: GameBoardConfig) {
787
824
  gameConfig
788
825
  );
789
826
  }, [config]);
790
-
791
- const snapshot = React.useMemo(() =>
792
- controller.snapshot(),
827
+
828
+ const snapshot = React.useMemo(() =>
829
+ controller.snapshot(),
793
830
  [controller.updateCount]
794
831
  );
795
-
832
+
796
833
  return {
797
834
  controller,
798
835
  snapshot,