@adriansteffan/reactive 0.0.9 → 0.0.11
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/README.md +1 -2
- package/dist/mod.d.ts +0 -8
- package/dist/{reactivepsych.es.js → reactive.es.js} +6101 -6513
- package/dist/{reactivepsych.umd.js → reactive.umd.js} +31 -42
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/experiment.tsx +1 -3
- package/src/components/text.tsx +1 -1
- package/src/index.css +0 -6
- package/src/mod.tsx +1 -2
- package/template/package-lock.json +46 -1
- package/template/package.json +1 -0
- package/template/src/App.tsx +1 -1
- package/template/tailwind.config.js +1 -1
- package/vite.config.ts +1 -1
- package/src/components/mastermindlewrapper.tsx +0 -662
|
@@ -1,662 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Bounce, toast } from 'react-toastify';
|
|
3
|
-
import Quest from './quest';
|
|
4
|
-
import { now } from '../utils/common';
|
|
5
|
-
|
|
6
|
-
const COLORS = {
|
|
7
|
-
red: '#D81B60',
|
|
8
|
-
blue: '#1E88E5',
|
|
9
|
-
yellow: '#FFC107',
|
|
10
|
-
green: '#01463A',
|
|
11
|
-
grey: '#DADADA',
|
|
12
|
-
} as const;
|
|
13
|
-
|
|
14
|
-
type ColorKey = keyof typeof COLORS;
|
|
15
|
-
type Size = 8 | 10 | 12 | 14 | 16 | 20 | 24 | 28 | 32;
|
|
16
|
-
|
|
17
|
-
interface ColorOrbProps {
|
|
18
|
-
color: ColorKey;
|
|
19
|
-
/** Size in Tailwind units (8-32). Defaults to 12 */
|
|
20
|
-
size?: Size;
|
|
21
|
-
interactive?: boolean;
|
|
22
|
-
pressed?: boolean;
|
|
23
|
-
hoverborder?: boolean;
|
|
24
|
-
onClick?: () => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
type GuessResult = {
|
|
28
|
-
color: ColorKey;
|
|
29
|
-
status: 'correct' | 'wrong-position' | 'incorrect';
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const ColorOrb: React.FC<ColorOrbProps> = ({
|
|
33
|
-
color,
|
|
34
|
-
size = 12,
|
|
35
|
-
interactive = false,
|
|
36
|
-
hoverborder = false,
|
|
37
|
-
pressed = false,
|
|
38
|
-
onClick,
|
|
39
|
-
}) => {
|
|
40
|
-
const letter = color === 'grey' ? '?' : color[0].toUpperCase();
|
|
41
|
-
|
|
42
|
-
const sizeClasses = {
|
|
43
|
-
8: 'h-8 w-8 text-sm',
|
|
44
|
-
10: 'h-10 w-10 text-base',
|
|
45
|
-
12: 'h-12 w-12 text-lg',
|
|
46
|
-
14: 'h-14 w-14 text-xl',
|
|
47
|
-
16: 'h-16 w-16 text-xl',
|
|
48
|
-
20: 'h-20 w-20 text-2xl',
|
|
49
|
-
24: 'h-24 w-24 text-3xl',
|
|
50
|
-
28: 'h-28 w-28 text-3xl',
|
|
51
|
-
32: 'h-32 w-32 text-4xl',
|
|
52
|
-
}[size];
|
|
53
|
-
|
|
54
|
-
const handleInteraction = (e: React.MouseEvent | React.TouchEvent) => {
|
|
55
|
-
e.preventDefault(); // Prevent double-firing on mobile devices
|
|
56
|
-
onClick?.();
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const handleTouchEnd = (e: React.MouseEvent | React.TouchEvent) => {
|
|
60
|
-
e.preventDefault();
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<div
|
|
65
|
-
style={{
|
|
66
|
-
backgroundColor: COLORS[color],
|
|
67
|
-
color: color === 'grey' ? '#000000' : '#FFFFFF',
|
|
68
|
-
}}
|
|
69
|
-
className={`
|
|
70
|
-
${sizeClasses}
|
|
71
|
-
rounded-full
|
|
72
|
-
flex items-center justify-center
|
|
73
|
-
font-bold
|
|
74
|
-
border-2
|
|
75
|
-
border-black
|
|
76
|
-
|
|
77
|
-
${interactive ? ' shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-none cursor-pointer touch-manipulation' : `border-[${size / 8}px]`}
|
|
78
|
-
${pressed ? ' translate-x-[2px] translate-y-[2px] shadow-none' : ''}
|
|
79
|
-
${hoverborder ? ' hover:border-4 cursor-pointer' : ''}
|
|
80
|
-
`}
|
|
81
|
-
onClick={handleInteraction}
|
|
82
|
-
onTouchStart={handleInteraction}
|
|
83
|
-
onTouchEnd={handleTouchEnd}
|
|
84
|
-
>
|
|
85
|
-
{letter}
|
|
86
|
-
</div>
|
|
87
|
-
);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// to pass up to the next function
|
|
91
|
-
interface GuessData {
|
|
92
|
-
index: number;
|
|
93
|
-
colors: ColorKey[];
|
|
94
|
-
results: GuessResult[];
|
|
95
|
-
isCorrect: boolean;
|
|
96
|
-
start: number;
|
|
97
|
-
end: number;
|
|
98
|
-
duration: number;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function useScreenWidth() {
|
|
102
|
-
const [width, setWidth] = useState(0);
|
|
103
|
-
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
const updateWidth = () => {
|
|
106
|
-
setWidth(window.innerWidth);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
updateWidth();
|
|
110
|
-
window.addEventListener('resize', updateWidth);
|
|
111
|
-
|
|
112
|
-
return () => window.removeEventListener('resize', updateWidth);
|
|
113
|
-
}, []);
|
|
114
|
-
|
|
115
|
-
return width;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function MasterMindle({
|
|
119
|
-
feedback,
|
|
120
|
-
next,
|
|
121
|
-
maxTime,
|
|
122
|
-
timeLeft,
|
|
123
|
-
maxGuesses,
|
|
124
|
-
setTimeLeft,
|
|
125
|
-
setQuitLastGame,
|
|
126
|
-
}: {
|
|
127
|
-
feedback: 1 | 2 | 3 | 4 | 5;
|
|
128
|
-
next: (data: object) => void;
|
|
129
|
-
maxTime: number;
|
|
130
|
-
timeLeft: number;
|
|
131
|
-
maxGuesses: number;
|
|
132
|
-
setTimeLeft: (time: number) => void;
|
|
133
|
-
setQuitLastGame: (quit: boolean) => void;
|
|
134
|
-
}) {
|
|
135
|
-
const [selectedColor, setSelectedColor] = useState<ColorKey | null>(null);
|
|
136
|
-
const [currentGuess, setCurrentGuess] = useState<(ColorKey | null)[]>([null, null, null, null]);
|
|
137
|
-
const [localTimeLeft, setLocalTimeLeft] = useState<number>(timeLeft);
|
|
138
|
-
const [guessesLeft, setGuessesLeft] = useState<number>(maxGuesses - 1);
|
|
139
|
-
const [roundOver, setRoundOver] = useState<boolean>(false);
|
|
140
|
-
|
|
141
|
-
const [guessStartTime, setGuessStartTime] = useState<number>(now());
|
|
142
|
-
const [accumulatedGuesses, setAccumulatedGuesses] = useState<GuessData[]>([]);
|
|
143
|
-
const screenWidth = useScreenWidth();
|
|
144
|
-
|
|
145
|
-
const warningShownRef = useRef(false);
|
|
146
|
-
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
warningShownRef.current = false;
|
|
149
|
-
}, [maxTime]);
|
|
150
|
-
|
|
151
|
-
useEffect(() => {
|
|
152
|
-
// Only start the timer if the round is not over
|
|
153
|
-
if (roundOver) return;
|
|
154
|
-
|
|
155
|
-
const timer = setInterval(() => {
|
|
156
|
-
setLocalTimeLeft((prev) => {
|
|
157
|
-
const newTime = Math.max(0, prev - 1);
|
|
158
|
-
|
|
159
|
-
if (newTime === 30 && !warningShownRef.current) {
|
|
160
|
-
warningShownRef.current = true;
|
|
161
|
-
toast('30 seconds remaining!', {
|
|
162
|
-
position: 'top-center',
|
|
163
|
-
hideProgressBar: true,
|
|
164
|
-
closeOnClick: true,
|
|
165
|
-
pauseOnHover: true,
|
|
166
|
-
draggable: false,
|
|
167
|
-
progress: undefined,
|
|
168
|
-
theme: 'light',
|
|
169
|
-
transition: Bounce,
|
|
170
|
-
autoClose: 4000,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return newTime;
|
|
175
|
-
});
|
|
176
|
-
}, 1000);
|
|
177
|
-
|
|
178
|
-
// Cleanup timer when component unmounts or roundOver changes
|
|
179
|
-
return () => clearInterval(timer);
|
|
180
|
-
}, [roundOver, setLocalTimeLeft]);
|
|
181
|
-
|
|
182
|
-
const [previousGuesses, setPreviousGuesses] = useState<
|
|
183
|
-
{ colors: ColorKey[]; results: GuessResult[] }[]
|
|
184
|
-
>([]);
|
|
185
|
-
|
|
186
|
-
const [solution] = useState<ColorKey[]>(() => {
|
|
187
|
-
const colors = Object.keys(COLORS).filter((color) => color !== 'grey') as ColorKey[];
|
|
188
|
-
return Array(4)
|
|
189
|
-
.fill(null)
|
|
190
|
-
.map(() => colors[Math.floor(Math.random() * colors.length)]);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// Add effect to scroll to bottom when previousGuesses changes
|
|
194
|
-
const guessesContainerRef = useRef<HTMLDivElement>(null);
|
|
195
|
-
useEffect(() => {
|
|
196
|
-
if (guessesContainerRef.current) {
|
|
197
|
-
guessesContainerRef.current.scrollTop = guessesContainerRef.current.scrollHeight;
|
|
198
|
-
}
|
|
199
|
-
}, [previousGuesses]);
|
|
200
|
-
|
|
201
|
-
const checkGuess = (guess: ColorKey[]): GuessResult[] => {
|
|
202
|
-
// Count color frequencies in solution
|
|
203
|
-
const solutionColorCounts = solution.reduce(
|
|
204
|
-
(counts, color) => {
|
|
205
|
-
counts[color] = (counts[color] || 0) + 1;
|
|
206
|
-
return counts;
|
|
207
|
-
},
|
|
208
|
-
{} as Record<ColorKey, number>,
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
// First pass: Mark correct positions
|
|
212
|
-
const results: GuessResult[] = guess.map((color, i) => {
|
|
213
|
-
if (color === solution[i]) {
|
|
214
|
-
solutionColorCounts[color]--;
|
|
215
|
-
return { color, status: 'correct' as const };
|
|
216
|
-
}
|
|
217
|
-
return { color, status: 'incorrect' as const };
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// Second pass: Check wrong positions
|
|
221
|
-
guess.forEach((color, i) => {
|
|
222
|
-
if (results[i].status === 'correct') return;
|
|
223
|
-
if (solutionColorCounts[color] > 0) {
|
|
224
|
-
results[i] = { color, status: 'wrong-position' as const };
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
return results;
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
const handleCheck = () => {
|
|
232
|
-
if (currentGuess.some((color) => color === null)) {
|
|
233
|
-
toast('Please complete your guess!', {
|
|
234
|
-
position: 'top-center',
|
|
235
|
-
hideProgressBar: true,
|
|
236
|
-
closeOnClick: true,
|
|
237
|
-
pauseOnHover: true,
|
|
238
|
-
draggable: false,
|
|
239
|
-
progress: undefined,
|
|
240
|
-
theme: 'light',
|
|
241
|
-
transition: Bounce,
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const currentTime = now();
|
|
248
|
-
const guessResults = checkGuess(currentGuess as ColorKey[]);
|
|
249
|
-
const isCorrect = guessResults.every((result) => result.status === 'correct');
|
|
250
|
-
|
|
251
|
-
const guessData: GuessData = {
|
|
252
|
-
index: previousGuesses.length,
|
|
253
|
-
colors: currentGuess as ColorKey[],
|
|
254
|
-
results: guessResults,
|
|
255
|
-
isCorrect: isCorrect,
|
|
256
|
-
start: guessStartTime,
|
|
257
|
-
end: currentTime,
|
|
258
|
-
duration: currentTime - guessStartTime,
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
setAccumulatedGuesses((prev) => [...prev, guessData]);
|
|
262
|
-
|
|
263
|
-
setPreviousGuesses((prev) => [
|
|
264
|
-
...prev,
|
|
265
|
-
{
|
|
266
|
-
colors: currentGuess as ColorKey[],
|
|
267
|
-
results: guessResults,
|
|
268
|
-
},
|
|
269
|
-
]);
|
|
270
|
-
|
|
271
|
-
setSelectedColor(null);
|
|
272
|
-
|
|
273
|
-
if (isCorrect) {
|
|
274
|
-
toast.success('You found the solution! Continue to the next trial.', {
|
|
275
|
-
closeOnClick: true,
|
|
276
|
-
transition: Bounce,
|
|
277
|
-
});
|
|
278
|
-
setSelectedColor(null);
|
|
279
|
-
setRoundOver(true);
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
setGuessesLeft((prev) => prev - 1);
|
|
284
|
-
if (guessesLeft == 0) {
|
|
285
|
-
toast.error('Out of guesses! Continue to the next trial.', {
|
|
286
|
-
closeOnClick: true,
|
|
287
|
-
transition: Bounce,
|
|
288
|
-
});
|
|
289
|
-
setSelectedColor(null);
|
|
290
|
-
setRoundOver(true);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (localTimeLeft == 0) {
|
|
294
|
-
toast.error('Out of time! Continue to the next trial.', {
|
|
295
|
-
closeOnClick: true,
|
|
296
|
-
transition: Bounce,
|
|
297
|
-
});
|
|
298
|
-
setSelectedColor(null);
|
|
299
|
-
setRoundOver(true);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
setCurrentGuess([null, null, null, null]);
|
|
303
|
-
setGuessStartTime(currentTime);
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const handleNext = (skipped: boolean) => {
|
|
307
|
-
setTimeLeft(localTimeLeft);
|
|
308
|
-
next({
|
|
309
|
-
solution: solution,
|
|
310
|
-
solved: accumulatedGuesses.some((guess: GuessData) => guess.isCorrect),
|
|
311
|
-
skipped: skipped,
|
|
312
|
-
timeLeft_s: timeLeft,
|
|
313
|
-
guesses: accumulatedGuesses,
|
|
314
|
-
});
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
return (
|
|
318
|
-
<div className='mt-16 md:p-8 lg:mt-16 max-w-7xl h-[calc(100vh-230px)] lg:h-full w-fit mx-auto flex flex-col lg:flex-row xl:gap-x-12 lg:gap-x-8 justify-between lg:justify-center'>
|
|
319
|
-
<div className='absolute inset-0 -z-10 h-full w-full bg-white bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:16px_16px]'></div>
|
|
320
|
-
|
|
321
|
-
{/* Action Buttons */}
|
|
322
|
-
<div className='flex gap-6 xl:w-56 xl:px-12 lg:p-4 flex-row justify-center lg:justify-start lg:flex-col'>
|
|
323
|
-
{!roundOver && (
|
|
324
|
-
<button
|
|
325
|
-
className='bg-white px-6 md:px-8 py-3 text-sm md:text-lg border-2 border-black font-bold rounded-full shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-none'
|
|
326
|
-
onClick={handleCheck}
|
|
327
|
-
>
|
|
328
|
-
CHECK
|
|
329
|
-
</button>
|
|
330
|
-
)}
|
|
331
|
-
{!roundOver && (
|
|
332
|
-
<button
|
|
333
|
-
className='bg-white px-6 md:px-8 py-1 md:py-3 text-sm md:text-lg border-2 border-black font-bold rounded-full shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-none'
|
|
334
|
-
onClick={() => setCurrentGuess([null, null, null, null])}
|
|
335
|
-
>
|
|
336
|
-
CLEAR
|
|
337
|
-
</button>
|
|
338
|
-
)}
|
|
339
|
-
{!roundOver && (
|
|
340
|
-
<button
|
|
341
|
-
className='bg-white px-6 md:px-8 py-1 md:py-3 text-sm md:text-lg border-2 border-black font-bold border-red-500 text-red-500 rounded-full shadow-[2px_2px_0px_rgba(239,68,68,1)] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-none'
|
|
342
|
-
onClick={() => {
|
|
343
|
-
setQuitLastGame(true);
|
|
344
|
-
handleNext(true);
|
|
345
|
-
}}
|
|
346
|
-
>
|
|
347
|
-
SKIP
|
|
348
|
-
</button>
|
|
349
|
-
)}
|
|
350
|
-
{roundOver && (
|
|
351
|
-
<button
|
|
352
|
-
className='bg-white px-6 md:px-8 py-3 md:py-3 text-sm md:text-lg border-2 border-black font-bold text-black rounded-full shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-none'
|
|
353
|
-
onClick={() => {
|
|
354
|
-
handleNext(false);
|
|
355
|
-
}}
|
|
356
|
-
>
|
|
357
|
-
NEXT
|
|
358
|
-
</button>
|
|
359
|
-
)}
|
|
360
|
-
|
|
361
|
-
</div>
|
|
362
|
-
|
|
363
|
-
{/* Gameboard */}
|
|
364
|
-
<div className='flex flex-col justify-between order-first items-center lg:order-none min-h-0'>
|
|
365
|
-
<div className='space-y-4 -mt-8 sm:mt-0 md:space-y-8 flex-1'>
|
|
366
|
-
{/* Timer */}
|
|
367
|
-
<div className='flex justify-center items-center gap-6'>
|
|
368
|
-
<div className='text-lg text-center sm:text-2xl font-bold w-20 sm:text-left'>
|
|
369
|
-
{Math.floor(localTimeLeft / 60)}:{(localTimeLeft % 60).toString().padStart(2, '0')}
|
|
370
|
-
</div>
|
|
371
|
-
</div>
|
|
372
|
-
{/* Current Guess Slots */}
|
|
373
|
-
<div className='py-5 sm:py-10 md:p-10 rounded-lg'>
|
|
374
|
-
<div className='flex gap-4 sm:gap-8 justify-center relative w-fit mx-auto'>
|
|
375
|
-
<div className='absolute top-1/2 h-1 left-0 right-0 bg-gray-300 -z-10' />
|
|
376
|
-
{currentGuess.map((color: ColorKey | null, index: number) => (
|
|
377
|
-
<ColorOrb
|
|
378
|
-
key={index}
|
|
379
|
-
color={color ?? 'grey'}
|
|
380
|
-
size={screenWidth >= 600 ? 24 : 16}
|
|
381
|
-
hoverborder={selectedColor != null || (!!color && color !== 'grey')}
|
|
382
|
-
onClick={() => {
|
|
383
|
-
if (roundOver) {
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
if (!selectedColor || selectedColor === 'grey') {
|
|
387
|
-
if (!!color && color != 'grey') {
|
|
388
|
-
setCurrentGuess((prevGuess) =>
|
|
389
|
-
prevGuess.map((color, i) => (i === index ? null : color)),
|
|
390
|
-
);
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
toast('Please select a color first!', {
|
|
394
|
-
position: 'top-center',
|
|
395
|
-
hideProgressBar: true,
|
|
396
|
-
closeOnClick: true,
|
|
397
|
-
pauseOnHover: true,
|
|
398
|
-
draggable: false,
|
|
399
|
-
progress: undefined,
|
|
400
|
-
theme: 'light',
|
|
401
|
-
transition: Bounce,
|
|
402
|
-
});
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (selectedColor === color) {
|
|
407
|
-
setCurrentGuess((prevGuess) =>
|
|
408
|
-
prevGuess.map((color, i) => (i === index ? null : color)),
|
|
409
|
-
);
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
setCurrentGuess((prevGuess) =>
|
|
414
|
-
prevGuess.map((color, i) => (i === index ? selectedColor : color)),
|
|
415
|
-
);
|
|
416
|
-
}}
|
|
417
|
-
/>
|
|
418
|
-
))}
|
|
419
|
-
</div>
|
|
420
|
-
</div>
|
|
421
|
-
|
|
422
|
-
{/* Previous Guesses */}
|
|
423
|
-
<div
|
|
424
|
-
ref={guessesContainerRef}
|
|
425
|
-
className='space-y-6 md:p-4 lg:p-16 border-gray-400 h-[25vh] lg:h-[40vh] overflow-y-auto'
|
|
426
|
-
>
|
|
427
|
-
{previousGuesses.map((guess, rowNum) => (
|
|
428
|
-
<div
|
|
429
|
-
key={rowNum}
|
|
430
|
-
className={`flex items-center gap-4 sm:gap-8 justify-center ${feedback == 3 ? 'flex-col sm:flex-row' : ''}`}
|
|
431
|
-
>
|
|
432
|
-
<div className='hidden sm:block w-4 sm:w-8 text-xl'>{rowNum + 1}</div>
|
|
433
|
-
<div className='flex gap-2 sm:gap-4 flex-row items-center'>
|
|
434
|
-
<div className='w-4 sm:hidden sm:w-8 text-xl'>{rowNum + 1}</div>
|
|
435
|
-
{guess.colors.map((color, index) => (
|
|
436
|
-
<div key={index} className='flex flex-col items-center'>
|
|
437
|
-
<ColorOrb key={index} color={color} size={12} />
|
|
438
|
-
{feedback == 4 && (
|
|
439
|
-
<span>
|
|
440
|
-
{guess.results[index].status === 'correct' && '✓'}
|
|
441
|
-
{guess.results[index].status !== 'correct' && <> </>}
|
|
442
|
-
</span>
|
|
443
|
-
)}
|
|
444
|
-
{feedback == 5 && (
|
|
445
|
-
<span>
|
|
446
|
-
{guess.results[index].status == 'correct' && '✓'}
|
|
447
|
-
{guess.results[index].status == 'incorrect' && '✗'}
|
|
448
|
-
{guess.results[index].status == 'wrong-position' && 'C'}
|
|
449
|
-
</span>
|
|
450
|
-
)}
|
|
451
|
-
</div>
|
|
452
|
-
))}
|
|
453
|
-
</div>
|
|
454
|
-
<div className='flex items-center gap-4 text-lg'>
|
|
455
|
-
{feedback == 1 && (
|
|
456
|
-
<span>
|
|
457
|
-
{guess.results.filter((result) => result.status !== 'correct').length == 0 ? (
|
|
458
|
-
<span className='font-bold text-blue-600'>✓</span>
|
|
459
|
-
) : (
|
|
460
|
-
<span className='font-bold text-red-600'>✗</span>
|
|
461
|
-
)}
|
|
462
|
-
</span>
|
|
463
|
-
)}
|
|
464
|
-
{feedback == 2 && (
|
|
465
|
-
<>
|
|
466
|
-
<span className='font-bold text-blue-600'>✓</span>
|
|
467
|
-
<span>
|
|
468
|
-
{guess.results.filter((result) => result.status === 'correct').length}
|
|
469
|
-
</span>
|
|
470
|
-
<span className='font-bold text-red-600'>✗</span>
|
|
471
|
-
<span>
|
|
472
|
-
{guess.results.filter((result) => result.status !== 'correct').length}
|
|
473
|
-
</span>
|
|
474
|
-
</>
|
|
475
|
-
)}
|
|
476
|
-
{feedback == 3 && (
|
|
477
|
-
<>
|
|
478
|
-
<span className='font-bold text-blue-600'>✓</span>
|
|
479
|
-
<span>
|
|
480
|
-
{guess.results.filter((result) => result.status === 'correct').length}
|
|
481
|
-
</span>
|
|
482
|
-
<span className='font-bold text-red-600'>✗</span>
|
|
483
|
-
<span>
|
|
484
|
-
{guess.results.filter((result) => result.status === 'incorrect').length}
|
|
485
|
-
</span>
|
|
486
|
-
<span className='font-bold'>C</span>
|
|
487
|
-
<span>
|
|
488
|
-
{
|
|
489
|
-
guess.results.filter((result) => result.status === 'wrong-position')
|
|
490
|
-
.length
|
|
491
|
-
}
|
|
492
|
-
</span>
|
|
493
|
-
</>
|
|
494
|
-
)}
|
|
495
|
-
{feedback == 4 && (
|
|
496
|
-
<>
|
|
497
|
-
<span className='font-bold text-red-600'>✗</span>
|
|
498
|
-
<span>
|
|
499
|
-
{guess.results.filter((result) => result.status === 'incorrect').length}
|
|
500
|
-
</span>
|
|
501
|
-
<span className='font-bold'>C</span>
|
|
502
|
-
<span>
|
|
503
|
-
{
|
|
504
|
-
guess.results.filter((result) => result.status === 'wrong-position')
|
|
505
|
-
.length
|
|
506
|
-
}
|
|
507
|
-
</span>
|
|
508
|
-
</>
|
|
509
|
-
)}
|
|
510
|
-
</div>
|
|
511
|
-
</div>
|
|
512
|
-
))}
|
|
513
|
-
</div>
|
|
514
|
-
</div>
|
|
515
|
-
</div>
|
|
516
|
-
|
|
517
|
-
{/* Right Side - Color Selection */}
|
|
518
|
-
<div className='lg:space-y-6 xl:px-8 flex flex-row justify-center gap-x-4 sm:gap-x-12 lg:gap-x-0 lg:justify-start lg:flex-col'>
|
|
519
|
-
{(Object.keys(COLORS) as ColorKey[])
|
|
520
|
-
.filter((color) => color !== 'grey')
|
|
521
|
-
.map((color) => (
|
|
522
|
-
<div key={color} className='flex items-center gap-4'>
|
|
523
|
-
<ColorOrb
|
|
524
|
-
color={color}
|
|
525
|
-
size={16}
|
|
526
|
-
interactive={selectedColor != color}
|
|
527
|
-
pressed={selectedColor == color}
|
|
528
|
-
onClick={() => {
|
|
529
|
-
if (roundOver) {
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
if (selectedColor == color) {
|
|
533
|
-
setSelectedColor(null);
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
setSelectedColor(color);
|
|
537
|
-
}}
|
|
538
|
-
/>
|
|
539
|
-
<span
|
|
540
|
-
className={`hidden lg:inline uppercase text-lg ${selectedColor == color ? 'underline underline-offset-2' : ''}`}
|
|
541
|
-
>
|
|
542
|
-
{color}
|
|
543
|
-
</span>
|
|
544
|
-
</div>
|
|
545
|
-
))}
|
|
546
|
-
</div>
|
|
547
|
-
</div>
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
interface MMTrialData {
|
|
552
|
-
type: 'game' | 'survey';
|
|
553
|
-
index: number;
|
|
554
|
-
start: number;
|
|
555
|
-
end: number;
|
|
556
|
-
duration: number;
|
|
557
|
-
data: object;
|
|
558
|
-
quitLastGame?: boolean;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function MasterMindleWrapper({
|
|
562
|
-
next,
|
|
563
|
-
blockIndex,
|
|
564
|
-
feedback,
|
|
565
|
-
timeLimit = 120,
|
|
566
|
-
maxGuesses = 10,
|
|
567
|
-
}: {
|
|
568
|
-
next: (data: object) => void;
|
|
569
|
-
blockIndex: number;
|
|
570
|
-
feedback: 1 | 2 | 3 | 4 | 5;
|
|
571
|
-
timeLimit: number;
|
|
572
|
-
maxGuesses: number;
|
|
573
|
-
}) {
|
|
574
|
-
const [gameState, setGameState] = useState<'game' | 'survey'>('game');
|
|
575
|
-
const [timeLeft, setTimeLeft] = useState(timeLimit);
|
|
576
|
-
const [trialStartTime, setTrialStartTime] = useState(now());
|
|
577
|
-
const [accumulatedData, setAccumulatedData] = useState<MMTrialData[]>([]);
|
|
578
|
-
const [quitLastGame, setQuitLastGame] = useState<boolean>(false);
|
|
579
|
-
const [trialIndex, setTrialIndex] = useState(0);
|
|
580
|
-
|
|
581
|
-
function switchGameState(newData: object) {
|
|
582
|
-
const currentTime = now();
|
|
583
|
-
|
|
584
|
-
const trialData: MMTrialData = {
|
|
585
|
-
type: gameState,
|
|
586
|
-
index: trialIndex,
|
|
587
|
-
start: trialStartTime,
|
|
588
|
-
end: currentTime,
|
|
589
|
-
duration: currentTime - trialStartTime,
|
|
590
|
-
data: newData,
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
if (gameState === 'survey' && timeLeft <= 0) {
|
|
594
|
-
next({
|
|
595
|
-
blockIndex: blockIndex,
|
|
596
|
-
feedbacktype: feedback,
|
|
597
|
-
timelimit_s: timeLimit,
|
|
598
|
-
data: [...accumulatedData, trialData],
|
|
599
|
-
});
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
setAccumulatedData((prev) => [...prev, trialData]);
|
|
604
|
-
|
|
605
|
-
if (gameState === 'survey') {
|
|
606
|
-
setQuitLastGame(false);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
setTrialStartTime(currentTime);
|
|
610
|
-
setTrialIndex((prev) => prev + 1);
|
|
611
|
-
setGameState(gameState === 'survey' ? 'game' : 'survey');
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
if (gameState === 'survey') {
|
|
615
|
-
return (
|
|
616
|
-
<Quest
|
|
617
|
-
next={switchGameState}
|
|
618
|
-
surveyJson={{
|
|
619
|
-
pages: [
|
|
620
|
-
{
|
|
621
|
-
elements: [
|
|
622
|
-
{
|
|
623
|
-
type: 'rating',
|
|
624
|
-
name: 'intensityofeffort',
|
|
625
|
-
title: 'How effortful was guessing this combination for you?',
|
|
626
|
-
isRequired: true,
|
|
627
|
-
rateMin: 1,
|
|
628
|
-
rateMax: 6,
|
|
629
|
-
minRateDescription: 'Minimal Effort',
|
|
630
|
-
maxRateDescription: 'Maximum Effort',
|
|
631
|
-
},
|
|
632
|
-
...(quitLastGame
|
|
633
|
-
? [
|
|
634
|
-
{
|
|
635
|
-
type: 'voicerecorder',
|
|
636
|
-
name: 'whyskip',
|
|
637
|
-
title: 'Why did you chose to quit before you found the solution?',
|
|
638
|
-
isRequired: true,
|
|
639
|
-
},
|
|
640
|
-
]
|
|
641
|
-
: [{}]),
|
|
642
|
-
],
|
|
643
|
-
},
|
|
644
|
-
],
|
|
645
|
-
}}
|
|
646
|
-
/>
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
return (
|
|
650
|
-
<MasterMindle
|
|
651
|
-
feedback={feedback}
|
|
652
|
-
next={switchGameState}
|
|
653
|
-
maxTime={timeLimit}
|
|
654
|
-
maxGuesses={maxGuesses}
|
|
655
|
-
timeLeft={timeLeft}
|
|
656
|
-
setTimeLeft={setTimeLeft}
|
|
657
|
-
setQuitLastGame={setQuitLastGame}
|
|
658
|
-
/>
|
|
659
|
-
);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
export default MasterMindleWrapper;
|