jspsych-tangram 0.0.11 → 0.0.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jspsych-tangram",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "Tangram tasks for jsPsych: prep and construct.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -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
  }
@@ -222,25 +222,25 @@ export interface GameBoardProps extends GameBoardConfig {
222
222
 
223
223
  /**
224
224
  * Core GameBoard component that encapsulates all tangram game functionality
225
- *
225
+ *
226
226
  * This is the main reusable component that provides complete tangram game
227
227
  * functionality for jsPsych plugins. It handles all aspects of the game
228
228
  * including piece interaction, collision detection, completion validation,
229
229
  * and data collection.
230
- *
230
+ *
231
231
  * ## Key Responsibilities
232
232
  * - Creates and manages BaseGameController for state management
233
- * - Computes layout geometry for board rendering
233
+ * - Computes layout geometry for board rendering
234
234
  * - Handles event callbacks for plugin integration
235
235
  * - Manages CSS sizing for responsive display
236
- *
236
+ *
237
237
  * ## State Management
238
238
  * Uses BaseGameController internally which provides:
239
239
  * - Piece placement and validation
240
240
  * - Sector completion tracking
241
241
  * - Event logging for data collection
242
242
  * - Collision detection and snapping
243
- *
243
+ *
244
244
  * @param props - Configuration and event callbacks for the game
245
245
  * @returns React component rendering the complete tangram game interface
246
246
  */
@@ -268,10 +268,10 @@ export default function GameBoard(props: GameBoardProps) {
268
268
  onTrialEnd,
269
269
  onControllerReady
270
270
  } = props;
271
-
271
+
272
272
  // Timer state for countdown
273
273
  const [timeRemaining, setTimeRemaining] = React.useState(timeLimitMs > 0 ? Math.floor(timeLimitMs / 1000) : 0);
274
-
274
+
275
275
  // Initialize game controller with injected data
276
276
  const controller = React.useMemo(() => {
277
277
  const gameConfig = {
@@ -286,11 +286,11 @@ export default function GameBoard(props: GameBoardProps) {
286
286
  ...(props.minPiecesPerMacro !== undefined && { minPiecesPerMacro: props.minPiecesPerMacro }),
287
287
  ...(props.requireAllSlots !== undefined && { requireAllSlots: props.requireAllSlots })
288
288
  };
289
-
289
+
290
290
  return new BaseGameController(
291
291
  sectors,
292
292
  quickstash,
293
- primitives,
293
+ primitives,
294
294
  gameConfig
295
295
  );
296
296
  }, [sectors, quickstash, primitives, layoutMode, target, input, timeLimitMs, maxQuickstashSlots, maxCompositeSize, mode, props.minPiecesPerMacro, props.requireAllSlots]);
@@ -322,12 +322,12 @@ export default function GameBoard(props: GameBoardProps) {
322
322
  };
323
323
  }, [tracker]);
324
324
 
325
- // Compute layout geometry
325
+ // Compute layout geometry
326
326
  const { layout, viewBox } = React.useMemo(() => {
327
327
  const nSectors = sectors.length;
328
328
  const sectorIds = sectors.map(s => s.id);
329
329
  const masksPerSector: Poly[][] = sectors.map(s => s.silhouette.mask ?? []);
330
-
330
+
331
331
  const logicalBox = solveLogicalBox({
332
332
  n: nSectors,
333
333
  layoutMode,
@@ -337,7 +337,7 @@ export default function GameBoard(props: GameBoardProps) {
337
337
  layoutPadPx: CONFIG.layout.paddingPx,
338
338
  masks: masksPerSector,
339
339
  });
340
-
340
+
341
341
  const layout = computeCircleLayout(
342
342
  { w: logicalBox.LOGICAL_W, h: logicalBox.LOGICAL_H },
343
343
  nSectors,
@@ -350,7 +350,7 @@ export default function GameBoard(props: GameBoardProps) {
350
350
  masks: masksPerSector,
351
351
  }
352
352
  );
353
-
353
+
354
354
  return {
355
355
  layout,
356
356
  viewBox: { w: logicalBox.LOGICAL_W, h: logicalBox.LOGICAL_H }
@@ -366,10 +366,10 @@ export default function GameBoard(props: GameBoardProps) {
366
366
  onControllerReady(controller, layout, force);
367
367
  }
368
368
  }, [controller, layout, onControllerReady, force]);
369
-
369
+
370
370
  // Game completion tracking
371
371
  const [gameCompleted, setGameCompleted] = React.useState(false);
372
-
372
+
373
373
  // Watch for game completion
374
374
  React.useEffect(() => {
375
375
  const checkGameCompletion = () => {
@@ -393,21 +393,21 @@ export default function GameBoard(props: GameBoardProps) {
393
393
  // Check completion whenever controller state updates
394
394
  checkGameCompletion();
395
395
  }, [controller.updateCount, sectors, gameCompleted, controller, tracker]);
396
-
396
+
397
397
  // Sector completion callback
398
398
  const handleSectorComplete = React.useCallback((sectorId: string) => {
399
399
  if (onSectorComplete) {
400
400
  onSectorComplete(sectorId, controller.snapshot());
401
401
  }
402
402
  }, [onSectorComplete, controller]);
403
-
403
+
404
404
  // Event callbacks for plugin integration
405
405
  const eventCallbacks = React.useMemo(() => ({
406
406
  onSectorComplete: handleSectorComplete,
407
407
  onPiecePlace: onPiecePlace || (() => {}),
408
408
  onPieceRemove: onPieceRemove || (() => {})
409
409
  }), [handleSectorComplete, onPiecePlace, onPieceRemove]);
410
-
410
+
411
411
  // Calculate sizing based on layout mode
412
412
  const getGameboardStyle = () => {
413
413
  const baseStyle = {
@@ -560,14 +560,14 @@ export default function GameBoard(props: GameBoardProps) {
560
560
  tracker // Pass tracker for data collection
561
561
  );
562
562
 
563
- const {
564
- draggingId,
565
- dragInvalid,
566
- svgRef,
567
- onPiecePointerDown,
568
- onBlueprintPointerDown,
569
- onPointerMove,
570
- onPointerUp,
563
+ const {
564
+ draggingId,
565
+ dragInvalid,
566
+ svgRef,
567
+ onPiecePointerDown,
568
+ onBlueprintPointerDown,
569
+ onPointerMove,
570
+ onPointerUp,
571
571
  setPieceRef,
572
572
  setDraggingId,
573
573
  lockedPieceId,
@@ -640,7 +640,7 @@ export default function GameBoard(props: GameBoardProps) {
640
640
  // Timer effect
641
641
  React.useEffect(() => {
642
642
  if (timeLimitMs === 0) return;
643
-
643
+
644
644
  const interval = setInterval(() => {
645
645
  setTimeRemaining(prev => {
646
646
  if (prev <= 1) {
@@ -650,7 +650,7 @@ export default function GameBoard(props: GameBoardProps) {
650
650
  return prev - 1;
651
651
  });
652
652
  }, 1000);
653
-
653
+
654
654
  return () => clearInterval(interval);
655
655
  }, [timeLimitMs]);
656
656
 
@@ -669,27 +669,38 @@ export default function GameBoard(props: GameBoardProps) {
669
669
  // minHeight: '100vh'
670
670
  };
671
671
 
672
- // Header style
672
+ // Header style (constrained to gameboard width)
673
673
  const headerStyle: React.CSSProperties = {
674
674
  display: 'flex',
675
675
  flexDirection: 'row',
676
- justifyContent: 'space-between',
676
+ justifyContent: 'center',
677
677
  alignItems: 'center',
678
678
  padding: '20px',
679
679
  background: '#f5f5f5',
680
680
  flex: '0 0 auto'
681
681
  };
682
682
 
683
- // Instructions style
683
+ // Header content wrapper (matches gameboard width)
684
+ const headerContentStyle: React.CSSProperties = {
685
+ position: 'relative',
686
+ width: `${svgDimensions.width}px`,
687
+ maxWidth: '100%',
688
+ display: 'flex',
689
+ justifyContent: 'center',
690
+ alignItems: 'center'
691
+ };
692
+
693
+ // Instructions style (always centered)
684
694
  const instructionsStyle: React.CSSProperties = {
685
- flexGrow: 1,
686
695
  fontSize: '20px',
687
696
  lineHeight: 1.5,
688
- marginRight: '20px'
697
+ textAlign: 'center'
689
698
  };
690
699
 
691
- // Timer style
700
+ // Timer style (positioned absolutely to not affect centering)
692
701
  const timerStyle: React.CSSProperties = {
702
+ position: 'absolute',
703
+ right: 0,
693
704
  fontSize: '24px',
694
705
  fontWeight: 'bold',
695
706
  fontFamily: 'monospace',
@@ -711,18 +722,20 @@ export default function GameBoard(props: GameBoardProps) {
711
722
  <div className="tangram-trial-container" style={containerStyle}>
712
723
  {(instructions || timeLimitMs > 0) && (
713
724
  <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
- )}
725
+ <div className="tangram-header-content" style={headerContentStyle}>
726
+ {instructions && (
727
+ <div
728
+ className="tangram-instructions"
729
+ style={instructionsStyle}
730
+ dangerouslySetInnerHTML={{ __html: instructions }}
731
+ />
732
+ )}
733
+ {timeLimitMs > 0 && (
734
+ <div className="tangram-timer" style={timerStyle}>
735
+ {formatTime(timeRemaining)}
736
+ </div>
737
+ )}
738
+ </div>
726
739
  </div>
727
740
  )}
728
741
  <div className="tangram-gameboard-wrapper" style={gameboardWrapperStyle}>
@@ -779,7 +792,7 @@ export function useGameBoard(config: GameBoardConfig) {
779
792
  ...(config.minPiecesPerMacro !== undefined && { minPiecesPerMacro: config.minPiecesPerMacro }),
780
793
  ...(config.requireAllSlots !== undefined && { requireAllSlots: config.requireAllSlots })
781
794
  };
782
-
795
+
783
796
  return new BaseGameController(
784
797
  config.sectors,
785
798
  config.quickstash,
@@ -787,12 +800,12 @@ export function useGameBoard(config: GameBoardConfig) {
787
800
  gameConfig
788
801
  );
789
802
  }, [config]);
790
-
791
- const snapshot = React.useMemo(() =>
792
- controller.snapshot(),
803
+
804
+ const snapshot = React.useMemo(() =>
805
+ controller.snapshot(),
793
806
  [controller.updateCount]
794
807
  );
795
-
808
+
796
809
  return {
797
810
  controller,
798
811
  snapshot,