refactor(card-sorting): remove reveal numbers feature

Remove the reveal numbers toggle that allowed players to see numeric
values on cards during gameplay. This feature added unnecessary
complexity and detracted from the core visual pattern recognition
gameplay.

Changes:
- Remove showNumbers and numbersRevealed from game state and config
- Remove REVEAL_NUMBERS move type and validation
- Remove reveal numbers button from playing phase UI
- Remove numbersRevealed from score calculation
- Clean up all references in Provider, Validator, and components

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-24 00:12:20 -05:00
parent 22634aa337
commit ea5e3e838b
6 changed files with 100 additions and 460 deletions

View File

@@ -25,10 +25,9 @@ interface CardSortingContextValue {
insertCard: (cardId: string, insertPosition: number) => void insertCard: (cardId: string, insertPosition: number) => void
removeCard: (position: number) => void removeCard: (position: number) => void
checkSolution: (finalSequence?: SortingCard[]) => void checkSolution: (finalSequence?: SortingCard[]) => void
revealNumbers: () => void
goToSetup: () => void goToSetup: () => void
resumeGame: () => void resumeGame: () => void
setConfig: (field: 'cardCount' | 'showNumbers' | 'timeLimit' | 'gameMode', value: unknown) => void setConfig: (field: 'cardCount' | 'timeLimit' | 'gameMode', value: unknown) => void
updateCardPositions: (positions: CardPosition[]) => void updateCardPositions: (positions: CardPosition[]) => void
exitSession: () => void exitSession: () => void
// Computed // Computed
@@ -53,7 +52,6 @@ const CardSortingContext = createContext<CardSortingContextValue | null>(null)
// Initial state matching validator's getInitialState // Initial state matching validator's getInitialState
const createInitialState = (config: Partial<CardSortingConfig>): CardSortingState => ({ const createInitialState = (config: Partial<CardSortingConfig>): CardSortingState => ({
cardCount: config.cardCount ?? 8, cardCount: config.cardCount ?? 8,
showNumbers: config.showNumbers ?? true,
timeLimit: config.timeLimit ?? null, timeLimit: config.timeLimit ?? null,
gameMode: config.gameMode ?? 'solo', gameMode: config.gameMode ?? 'solo',
gamePhase: 'setup', gamePhase: 'setup',
@@ -75,7 +73,6 @@ const createInitialState = (config: Partial<CardSortingConfig>): CardSortingStat
cardPositions: [], cardPositions: [],
cursorPositions: new Map(), cursorPositions: new Map(),
selectedCardId: null, selectedCardId: null,
numbersRevealed: false,
scoreBreakdown: null, scoreBreakdown: null,
}) })
@@ -103,11 +100,9 @@ function applyMoveOptimistically(state: CardSortingState, move: GameMove): CardS
// Use cards in the order they were sent (already shuffled by initiating client) // Use cards in the order they were sent (already shuffled by initiating client)
availableCards: selectedCards, availableCards: selectedCards,
placedCards: new Array(state.cardCount).fill(null), placedCards: new Array(state.cardCount).fill(null),
numbersRevealed: false,
// Save original config for pause/resume // Save original config for pause/resume
originalConfig: { originalConfig: {
cardCount: state.cardCount, cardCount: state.cardCount,
showNumbers: state.showNumbers,
timeLimit: state.timeLimit, timeLimit: state.timeLimit,
gameMode: state.gameMode, gameMode: state.gameMode,
}, },
@@ -213,13 +208,6 @@ function applyMoveOptimistically(state: CardSortingState, move: GameMove): CardS
} }
} }
case 'REVEAL_NUMBERS': {
return {
...state,
numbersRevealed: true,
}
}
case 'CHECK_SOLUTION': { case 'CHECK_SOLUTION': {
// Don't apply optimistic update - wait for server to calculate and return score // Don't apply optimistic update - wait for server to calculate and return score
return state return state
@@ -231,8 +219,8 @@ function applyMoveOptimistically(state: CardSortingState, move: GameMove): CardS
return { return {
...createInitialState({ ...createInitialState({
cardCount: state.cardCount, cardCount: state.cardCount,
showNumbers: state.showNumbers,
timeLimit: state.timeLimit, timeLimit: state.timeLimit,
gameMode: state.gameMode,
}), }),
// Save paused state if coming from active game // Save paused state if coming from active game
originalConfig: state.originalConfig, originalConfig: state.originalConfig,
@@ -244,7 +232,6 @@ function applyMoveOptimistically(state: CardSortingState, move: GameMove): CardS
placedCards: state.placedCards, placedCards: state.placedCards,
cardPositions: state.cardPositions, cardPositions: state.cardPositions,
gameStartTime: state.gameStartTime || Date.now(), gameStartTime: state.gameStartTime || Date.now(),
numbersRevealed: state.numbersRevealed,
} }
: undefined, : undefined,
} }
@@ -288,7 +275,6 @@ function applyMoveOptimistically(state: CardSortingState, move: GameMove): CardS
placedCards: state.pausedGameState.placedCards, placedCards: state.pausedGameState.placedCards,
cardPositions: state.pausedGameState.cardPositions, cardPositions: state.pausedGameState.cardPositions,
gameStartTime: state.pausedGameState.gameStartTime, gameStartTime: state.pausedGameState.gameStartTime,
numbersRevealed: state.pausedGameState.numbersRevealed,
pausedGamePhase: undefined, pausedGamePhase: undefined,
pausedGameState: undefined, pausedGameState: undefined,
} }
@@ -389,10 +375,10 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
if (!state.originalConfig) return false if (!state.originalConfig) return false
return ( return (
state.cardCount !== state.originalConfig.cardCount || state.cardCount !== state.originalConfig.cardCount ||
state.showNumbers !== state.originalConfig.showNumbers || state.timeLimit !== state.originalConfig.timeLimit ||
state.timeLimit !== state.originalConfig.timeLimit state.gameMode !== state.originalConfig.gameMode
) )
}, [state.cardCount, state.showNumbers, state.timeLimit, state.originalConfig]) }, [state.cardCount, state.timeLimit, state.gameMode, state.originalConfig])
const canResumeGame = useMemo(() => { const canResumeGame = useMemo(() => {
return !!state.pausedGamePhase && !!state.pausedGameState && !hasConfigChanged return !!state.pausedGamePhase && !!state.pausedGameState && !hasConfigChanged
@@ -503,17 +489,6 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
[localPlayerId, canCheckSolution, sendMove, viewerId] [localPlayerId, canCheckSolution, sendMove, viewerId]
) )
const revealNumbers = useCallback(() => {
if (!localPlayerId) return
sendMove({
type: 'REVEAL_NUMBERS',
playerId: localPlayerId,
userId: viewerId || '',
data: {},
})
}, [localPlayerId, sendMove, viewerId])
const goToSetup = useCallback(() => { const goToSetup = useCallback(() => {
if (!localPlayerId) return if (!localPlayerId) return
@@ -540,7 +515,7 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
}, [localPlayerId, canResumeGame, sendMove, viewerId]) }, [localPlayerId, canResumeGame, sendMove, viewerId])
const setConfig = useCallback( const setConfig = useCallback(
(field: 'cardCount' | 'showNumbers' | 'timeLimit' | 'gameMode', value: unknown) => { (field: 'cardCount' | 'timeLimit' | 'gameMode', value: unknown) => {
if (!localPlayerId) return if (!localPlayerId) return
sendMove({ sendMove({
@@ -595,7 +570,6 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
insertCard, insertCard,
removeCard, removeCard,
checkSolution, checkSolution,
revealNumbers,
goToSetup, goToSetup,
resumeGame, resumeGame,
setConfig, setConfig,

View File

@@ -22,8 +22,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
return this.validateInsertCard(state, move.data.cardId, move.data.insertPosition) return this.validateInsertCard(state, move.data.cardId, move.data.insertPosition)
case 'REMOVE_CARD': case 'REMOVE_CARD':
return this.validateRemoveCard(state, move.data.position) return this.validateRemoveCard(state, move.data.position)
case 'REVEAL_NUMBERS':
return this.validateRevealNumbers(state)
case 'CHECK_SOLUTION': case 'CHECK_SOLUTION':
return this.validateCheckSolution(state, move.data.finalSequence) return this.validateCheckSolution(state, move.data.finalSequence)
case 'GO_TO_SETUP': case 'GO_TO_SETUP':
@@ -84,7 +82,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
availableCards: selectedCards as typeof state.availableCards, availableCards: selectedCards as typeof state.availableCards,
placedCards: new Array(state.cardCount).fill(null), placedCards: new Array(state.cardCount).fill(null),
cardPositions: [], // Will be set by first position update cardPositions: [], // Will be set by first position update
numbersRevealed: false,
scoreBreakdown: null, scoreBreakdown: null,
}, },
} }
@@ -234,34 +231,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
} }
} }
private validateRevealNumbers(state: CardSortingState): ValidationResult {
// Must be in playing phase
if (state.gamePhase !== 'playing') {
return {
valid: false,
error: 'Can only reveal numbers during playing phase',
}
}
// Must be enabled in config
if (!state.showNumbers) {
return { valid: false, error: 'Reveal numbers is not enabled' }
}
// Already revealed
if (state.numbersRevealed) {
return { valid: false, error: 'Numbers already revealed' }
}
return {
valid: true,
newState: {
...state,
numbersRevealed: true,
},
}
}
private validateCheckSolution( private validateCheckSolution(
state: CardSortingState, state: CardSortingState,
finalSequence?: typeof state.selectedCards finalSequence?: typeof state.selectedCards
@@ -291,8 +260,7 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
const scoreBreakdown = calculateScore( const scoreBreakdown = calculateScore(
userSequence, userSequence,
correctSequence, correctSequence,
state.gameStartTime || Date.now(), state.gameStartTime || Date.now()
state.numbersRevealed
) )
// If finalSequence was provided, update placedCards with it // If finalSequence was provided, update placedCards with it
@@ -321,13 +289,11 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
newState: { newState: {
...this.getInitialState({ ...this.getInitialState({
cardCount: state.cardCount, cardCount: state.cardCount,
showNumbers: state.showNumbers,
timeLimit: state.timeLimit, timeLimit: state.timeLimit,
gameMode: state.gameMode, gameMode: state.gameMode,
}), }),
originalConfig: { originalConfig: {
cardCount: state.cardCount, cardCount: state.cardCount,
showNumbers: state.showNumbers,
timeLimit: state.timeLimit, timeLimit: state.timeLimit,
gameMode: state.gameMode, gameMode: state.gameMode,
}, },
@@ -338,7 +304,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
placedCards: state.placedCards, placedCards: state.placedCards,
cardPositions: state.cardPositions, cardPositions: state.cardPositions,
gameStartTime: state.gameStartTime || Date.now(), gameStartTime: state.gameStartTime || Date.now(),
numbersRevealed: state.numbersRevealed,
}, },
}, },
} }
@@ -349,7 +314,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
valid: true, valid: true,
newState: this.getInitialState({ newState: this.getInitialState({
cardCount: state.cardCount, cardCount: state.cardCount,
showNumbers: state.showNumbers,
timeLimit: state.timeLimit, timeLimit: state.timeLimit,
gameMode: state.gameMode, gameMode: state.gameMode,
}), }),
@@ -384,21 +348,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
}, },
} }
case 'showNumbers':
if (typeof value !== 'boolean') {
return { valid: false, error: 'showNumbers must be a boolean' }
}
return {
valid: true,
newState: {
...state,
showNumbers: value,
// Clear pause state if config changed
pausedGamePhase: undefined,
pausedGameState: undefined,
},
}
case 'timeLimit': case 'timeLimit':
if (value !== null && (typeof value !== 'number' || value < 30)) { if (value !== null && (typeof value !== 'number' || value < 30)) {
return { return {
@@ -463,7 +412,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
placedCards: state.pausedGameState.placedCards, placedCards: state.pausedGameState.placedCards,
cardPositions: state.pausedGameState.cardPositions, cardPositions: state.pausedGameState.cardPositions,
gameStartTime: state.pausedGameState.gameStartTime, gameStartTime: state.pausedGameState.gameStartTime,
numbersRevealed: state.pausedGameState.numbersRevealed,
pausedGamePhase: undefined, pausedGamePhase: undefined,
pausedGameState: undefined, pausedGameState: undefined,
}, },
@@ -519,7 +467,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
getInitialState(config: CardSortingConfig): CardSortingState { getInitialState(config: CardSortingConfig): CardSortingState {
return { return {
cardCount: config.cardCount, cardCount: config.cardCount,
showNumbers: config.showNumbers,
timeLimit: config.timeLimit, timeLimit: config.timeLimit,
gameMode: config.gameMode, gameMode: config.gameMode,
gamePhase: 'setup', gamePhase: 'setup',
@@ -541,7 +488,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
cardPositions: [], cardPositions: [],
cursorPositions: new Map(), cursorPositions: new Map(),
selectedCardId: null, selectedCardId: null,
numbersRevealed: false,
scoreBreakdown: null, scoreBreakdown: null,
} }
} }

View File

@@ -998,7 +998,6 @@ export function PlayingPhaseDrag() {
}, [activityFeed]) }, [activityFeed])
// Track previous state for detecting changes // Track previous state for detecting changes
const prevNumbersRevealedRef = useRef(state.numbersRevealed)
const prevDraggingPlayersRef = useRef<Set<string>>(new Set()) const prevDraggingPlayersRef = useRef<Set<string>>(new Set())
// Detect state changes and generate activity notifications // Detect state changes and generate activity notifications
@@ -1023,21 +1022,7 @@ export function PlayingPhaseDrag() {
} }
prevDraggingPlayersRef.current = currentlyDragging prevDraggingPlayersRef.current = currentlyDragging
}, [state.cardPositions, state.gameMode, localPlayerId, addActivityNotification])
// Detect revealed numbers
if (state.numbersRevealed && !prevNumbersRevealedRef.current) {
// We don't know who revealed them without player metadata in state
// Skip for now
}
prevNumbersRevealedRef.current = state.numbersRevealed
}, [
state.cardPositions,
state.numbersRevealed,
state.gameMode,
localPlayerId,
addActivityNotification,
])
// Handle viewport resize // Handle viewport resize
useEffect(() => { useEffect(() => {
@@ -1696,39 +1681,6 @@ export function PlayingPhaseDrag() {
Cards in correct position Cards in correct position
</div> </div>
</div> </div>
{/* Numbers Revealed */}
<div
className={css({
marginBottom: '16px',
padding: '12px',
background: state.numbersRevealed
? 'linear-gradient(135deg, #fce7f3, #fbcfe8)'
: 'linear-gradient(135deg, #f1f5f9, #e2e8f0)',
borderRadius: '8px',
border: '1px solid',
borderColor: state.numbersRevealed ? '#f9a8d4' : '#cbd5e1',
})}
>
<div
className={css({
fontSize: '12px',
color: state.numbersRevealed ? '#9f1239' : '#475569',
marginBottom: '4px',
})}
>
👁 Numbers Revealed
</div>
<div
className={css({
fontSize: '20px',
fontWeight: '600',
color: state.numbersRevealed ? '#9f1239' : '#475569',
})}
>
{state.numbersRevealed ? '✓ Yes' : '✗ No'}
</div>
</div>
</div> </div>
</div> </div>
)} )}
@@ -1745,32 +1697,6 @@ export function PlayingPhaseDrag() {
zIndex: 10, zIndex: 10,
})} })}
> >
{/* Reveal Numbers Button */}
{!state.showNumbers && (
<button
type="button"
onClick={revealNumbers}
title="Reveal Numbers"
className={css({
width: '56px',
height: '56px',
background: 'linear-gradient(135deg, #fef3c7, #fde68a)',
border: '3px solid #f59e0b',
borderRadius: '50%',
fontSize: '24px',
cursor: 'pointer',
transition: 'all 0.2s ease',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
_hover: {
transform: 'scale(1.1)',
boxShadow: '0 6px 20px rgba(245, 158, 11, 0.4)',
},
})}
>
👁
</button>
)}
{/* Check Solution Button with Label */} {/* Check Solution Button with Label */}
<div <div
className={css({ className={css({

View File

@@ -2,8 +2,7 @@
import { css } from '../../../../styled-system/css' import { css } from '../../../../styled-system/css'
import { useCardSorting } from '../Provider' import { useCardSorting } from '../Provider'
import { useSpring, animated, config, useSprings } from '@react-spring/web' import { useState, useEffect } from 'react'
import { useState, useEffect, useRef, useMemo } from 'react'
import type { SortingCard } from '../types' import type { SortingCard } from '../types'
// Add result animations // Add result animations
@@ -32,12 +31,6 @@ if (typeof document !== 'undefined') {
document.head.appendChild(style) document.head.appendChild(style)
} }
interface CardPosition {
x: number
y: number
rotation: number
}
export function ResultsPhase() { export function ResultsPhase() {
const { state, startGame, goToSetup, exitSession, players } = useCardSorting() const { state, startGame, goToSetup, exitSession, players } = useCardSorting()
const { scoreBreakdown } = state const { scoreBreakdown } = state
@@ -49,168 +42,14 @@ export function ResultsPhase() {
// Get user's sequence from placedCards // Get user's sequence from placedCards
const userSequence = state.placedCards.filter((c): c is SortingCard => c !== null) const userSequence = state.placedCards.filter((c): c is SortingCard => c !== null)
// Get viewport dimensions for converting percentage positions to pixels // Show corrections after a delay
const containerRef = useRef<HTMLDivElement>(null)
const viewportDimensionsRef = useRef({
width: window.innerWidth,
height: window.innerHeight,
})
const [, forceUpdate] = useState({})
useEffect(() => {
const updateDimensions = () => {
viewportDimensionsRef.current = {
width: window.innerWidth,
height: window.innerHeight,
}
forceUpdate({}) // Force re-render for viewport updates
}
window.addEventListener('resize', updateDimensions)
return () => window.removeEventListener('resize', updateDimensions)
}, [])
// Calculate grid positions for cards (final positions)
// Use percentage-based coordinates (same as game board)
const calculateGridPosition = (cardIndex: number) => {
const gridCols = 3
const cardWidthPct = 14 // ~140px on ~1000px viewport
const cardHeightPct = 22.5 // ~180px on ~800px viewport
const gapPct = 2
// Center the grid horizontally
const gridWidth = gridCols * cardWidthPct + (gridCols - 1) * gapPct
const startXPct = (100 - gridWidth) / 2
const col = cardIndex % gridCols
const row = Math.floor(cardIndex / gridCols)
return {
x: startXPct + col * (cardWidthPct + gapPct),
y: 15 + row * (cardHeightPct + gapPct), // Start from 15% down
rotation: 0,
}
}
// Get initial positions from game table (already in percentage)
const getInitialPosition = (cardId: string) => {
const cardPos = state.cardPositions.find((p) => p.cardId === cardId)
if (!cardPos) {
return { x: 50, y: 50, rotation: 0 }
}
// Already in percentage, just pass through
return {
x: cardPos.x,
y: cardPos.y,
rotation: cardPos.rotation,
}
}
// Create springs for each card - memoize initial positions
const initialPositions = useMemo(() => {
return userSequence.map((card) => getInitialPosition(card.id))
}, []) // Empty deps - only calculate once on mount
// Use ref to ensure springs are truly only created once
const springsInitializedRef = useRef(false)
const gridPositionsRef = useRef<{ x: number; y: number; rotation: number }[]>([])
const [springs, api] = useSprings(userSequence.length, (index) => {
// If already initialized (on re-render), use the grid position
if (springsInitializedRef.current) {
const gridPos = gridPositionsRef.current[index] || calculateGridPosition(index)
console.log('[ResultsPhase] Re-creating spring', index, 'at GRID position', gridPos)
return {
from: gridPos,
to: gridPos,
immediate: true, // Already at grid position
config: config.gentle,
}
}
// First time - use initial game board positions
console.log('[ResultsPhase] Creating spring', index, 'from', initialPositions[index])
return {
from: initialPositions[index],
to: initialPositions[index],
immediate: false,
config: config.gentle,
}
})
console.log(
'[ResultsPhase] Component render, springs.length:',
springs.length,
'initialized:',
springsInitializedRef.current
)
// Immediately start animating to grid positions (only once)
useEffect(() => {
console.log('[ResultsPhase] Animation effect running')
// Small delay to ensure mount
const timer = setTimeout(() => {
console.log('[ResultsPhase] Starting animation to grid positions')
api.start((index) => {
const card = userSequence[index]
const correctIndex = state.correctOrder.findIndex((c) => c.id === card.id)
const gridPos = calculateGridPosition(correctIndex)
console.log('[ResultsPhase] Animating card', index, 'to', gridPos)
return {
to: gridPos,
immediate: false,
config: { ...config.gentle, tension: 120, friction: 26 },
}
})
}, 100)
// After animation completes, lock positions by setting immediate: true
const lockTimer = setTimeout(() => {
console.log('[ResultsPhase] Locking positions with immediate: true')
// Store grid positions in ref
gridPositionsRef.current = userSequence.map((card, index) => {
const correctIndex = state.correctOrder.findIndex((c) => c.id === card.id)
return calculateGridPosition(correctIndex)
})
api.start((index) => {
const card = userSequence[index]
const correctIndex = state.correctOrder.findIndex((c) => c.id === card.id)
const gridPos = calculateGridPosition(correctIndex)
console.log('[ResultsPhase] Locking card', index, 'at', gridPos)
return {
to: gridPos,
immediate: true, // No more animations - locked in place
}
})
springsInitializedRef.current = true // Mark as initialized
console.log('[ResultsPhase] Springs locked and marked as initialized')
}, 1100) // Wait for animation to complete (100ms + 1000ms)
return () => {
console.log('[ResultsPhase] Animation effect cleanup')
clearTimeout(timer)
clearTimeout(lockTimer)
}
}, []) // Empty deps - only run once
// Show corrections after animation completes
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setShowCorrections(true) setShowCorrections(true)
}, 1500) }, 1000)
return () => clearTimeout(timer) return () => clearTimeout(timer)
}, []) }, [])
// Panel slide-in animation
const panelSpring = useSpring({
from: { opacity: 0, transform: 'translateX(50px)' },
to: { opacity: 1, transform: 'translateX(0px)' },
config: config.gentle,
})
if (!scoreBreakdown) { if (!scoreBreakdown) {
return ( return (
<div <div
@@ -255,6 +94,7 @@ export function ResultsPhase() {
className={css({ className={css({
width: '100%', width: '100%',
height: '100%', height: '100%',
display: 'flex',
position: 'fixed', position: 'fixed',
top: 0, top: 0,
left: 0, left: 0,
@@ -263,146 +103,115 @@ export function ResultsPhase() {
background: 'linear-gradient(135deg, #f0f9ff, #e0f2fe)', background: 'linear-gradient(135deg, #f0f9ff, #e0f2fe)',
})} })}
> >
{/* Full viewport for cards (same coordinate system as game board) */} {/* Cards Grid Area */}
<div <div
ref={containerRef}
className={css({ className={css({
position: 'absolute', flex: 1,
top: 0, display: 'flex',
left: 0, alignItems: 'center',
width: '100%', justifyContent: 'center',
height: '100%', padding: '40px',
overflow: 'hidden', overflow: 'auto',
})} })}
> >
{/* Cards with animated positions */} <div
{userSequence.map((card, userIndex) => { className={css({
const spring = springs[userIndex] display: 'grid',
if (!spring) return null gridTemplateColumns: 'repeat(3, 1fr)',
gap: '16px',
maxWidth: '600px',
})}
>
{userSequence.map((card, userIndex) => {
const isCorrect = state.correctOrder[userIndex]?.id === card.id
const correctIndex = state.correctOrder.findIndex((c) => c.id === card.id)
// Check if this card is correct for its position in the user's sequence return (
// Same logic as during gameplay: does the card at this position match the correct card for this position?
const isCorrect = state.correctOrder[userIndex]?.id === card.id
const correctIndex = state.correctOrder.findIndex((c) => c.id === card.id)
return (
<animated.div
key={card.id}
style={{
position: 'absolute',
left: spring.x.to((x) => `${(x / 100) * viewportDimensionsRef.current.width}px`),
top: spring.y.to((y) => `${(y / 100) * viewportDimensionsRef.current.height}px`),
transform: spring.rotation.to((r) => `rotate(${r}deg)`),
width: '140px',
height: '180px',
zIndex: 5,
}}
>
{/* Card */}
<div <div
key={card.id}
className={css({ className={css({
width: '100%',
height: '100%',
background: 'white',
borderRadius: '8px',
border: '3px solid',
borderColor: isCorrect ? '#22c55e' : showCorrections ? '#ef4444' : '#0369a1',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '8px',
boxSizing: 'border-box',
position: 'relative', position: 'relative',
boxShadow: isCorrect width: '160px',
? '0 0 20px rgba(34, 197, 94, 0.4)' height: '200px',
: showCorrections
? '0 0 20px rgba(239, 68, 68, 0.4)'
: '0 4px 8px rgba(0, 0, 0, 0.1)',
})} })}
dangerouslySetInnerHTML={{ __html: card.svgContent }} >
/> {/* Card */}
{/* Correct/Incorrect indicator */}
{showCorrections && (
<div <div
className={css({ className={css({
position: 'absolute', width: '100%',
top: '-12px', height: '100%',
right: '-12px', background: 'white',
width: '32px', borderRadius: '8px',
height: '32px', border: '3px solid',
borderRadius: '50%', borderColor: isCorrect ? '#22c55e' : showCorrections ? '#ef4444' : '#0369a1',
background: isCorrect ? '#22c55e' : '#ef4444',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
fontSize: '20px', padding: '8px',
boxSizing: 'border-box',
position: 'relative',
boxShadow: isCorrect
? '0 0 20px rgba(34, 197, 94, 0.4)'
: showCorrections
? '0 0 20px rgba(239, 68, 68, 0.4)'
: '0 4px 8px rgba(0, 0, 0, 0.1)',
animation: 'scoreReveal 0.5s ease-out',
})}
dangerouslySetInnerHTML={{ __html: card.svgContent }}
/>
{/* Correct/Incorrect indicator */}
{showCorrections && (
<div
className={css({
position: 'absolute',
top: '-12px',
right: '-12px',
width: '32px',
height: '32px',
borderRadius: '50%',
background: isCorrect ? '#22c55e' : '#ef4444',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
color: 'white',
fontWeight: 'bold',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.2)',
animation: 'scoreReveal 0.4s ease-out',
})}
>
{isCorrect ? '✓' : '✗'}
</div>
)}
{/* Position number */}
<div
className={css({
position: 'absolute',
bottom: '-8px',
left: '50%',
transform: 'translateX(-50%)',
background: isCorrect ? '#22c55e' : showCorrections ? '#ef4444' : '#0369a1',
color: 'white', color: 'white',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: 'bold', fontWeight: 'bold',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.2)', boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
animation: 'scoreReveal 0.4s ease-out',
})} })}
> >
{isCorrect ? '✓' : '✗'} #{showCorrections ? correctIndex + 1 : userIndex + 1}
</div> </div>
)}
{/* Position number */}
<div
className={css({
position: 'absolute',
bottom: '-8px',
left: '50%',
transform: 'translateX(-50%)',
background: isCorrect ? '#22c55e' : showCorrections ? '#ef4444' : '#0369a1',
color: 'white',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: 'bold',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
})}
>
#{showCorrections ? correctIndex + 1 : userIndex + 1}
</div> </div>
</animated.div> )
) })}
})} </div>
{/* Correction message */}
{showCorrections && (
<div
className={css({
position: 'absolute',
bottom: '20px',
left: '20px',
right: '20px',
padding: '12px 16px',
background: 'rgba(59, 130, 246, 0.2)',
border: '2px solid rgba(59, 130, 246, 0.4)',
borderRadius: '12px',
fontSize: '14px',
fontWeight: '600',
color: '#1e3a8a',
textAlign: 'center',
animation: 'scoreReveal 0.6s ease-out 0.5s both',
})}
>
{isPerfect
? '🎉 Perfect arrangement!'
: '↗️ Cards have moved to their correct positions'}
</div>
)}
</div> </div>
{/* Right side: Score panel */} {/* Right side: Score panel */}
<animated.div <div
style={panelSpring}
className={css({ className={css({
position: 'fixed',
right: 0,
top: 0,
bottom: 0,
width: '400px', width: '400px',
background: 'rgba(255, 255, 255, 0.95)', background: 'rgba(255, 255, 255, 0.95)',
borderLeft: '3px solid rgba(59, 130, 246, 0.3)', borderLeft: '3px solid rgba(59, 130, 246, 0.3)',
@@ -800,7 +609,7 @@ export function ResultsPhase() {
🚪 Exit 🚪 Exit
</button> </button>
</div> </div>
</animated.div> </div>
</div> </div>
) )
} }

View File

@@ -19,7 +19,6 @@ export type GameMode = 'solo' | 'collaborative' | 'competitive' | 'relay'
export interface CardSortingConfig extends GameConfig { export interface CardSortingConfig extends GameConfig {
cardCount: 5 | 8 | 12 | 15 // Difficulty (number of cards) cardCount: 5 | 8 | 12 | 15 // Difficulty (number of cards)
showNumbers: boolean // Allow reveal numbers button
timeLimit: number | null // Optional time limit (seconds), null = unlimited timeLimit: number | null // Optional time limit (seconds), null = unlimited
gameMode: GameMode // Game mode (solo, collaborative, competitive, relay) gameMode: GameMode // Game mode (solo, collaborative, competitive, relay)
} }
@@ -60,7 +59,6 @@ export interface ScoreBreakdown {
exactPositionScore: number // 0-100 based on exact matches exactPositionScore: number // 0-100 based on exact matches
inversionScore: number // 0-100 based on inversions inversionScore: number // 0-100 based on inversions
elapsedTime: number // Seconds taken elapsedTime: number // Seconds taken
numbersRevealed: boolean // Whether player used reveal
} }
// ============================================================================ // ============================================================================
@@ -70,7 +68,6 @@ export interface ScoreBreakdown {
export interface CardSortingState extends GameState { export interface CardSortingState extends GameState {
// Configuration // Configuration
cardCount: 5 | 8 | 12 | 15 cardCount: 5 | 8 | 12 | 15
showNumbers: boolean
timeLimit: number | null timeLimit: number | null
gameMode: GameMode gameMode: GameMode
@@ -97,7 +94,6 @@ export interface CardSortingState extends GameState {
// UI state (client-only, not in server state) // UI state (client-only, not in server state)
selectedCardId: string | null // Currently selected card selectedCardId: string | null // Currently selected card
numbersRevealed: boolean // If player revealed numbers
// Results // Results
scoreBreakdown: ScoreBreakdown | null // Final score details scoreBreakdown: ScoreBreakdown | null // Final score details
@@ -111,7 +107,6 @@ export interface CardSortingState extends GameState {
placedCards: (SortingCard | null)[] placedCards: (SortingCard | null)[]
cardPositions: CardPosition[] cardPositions: CardPosition[]
gameStartTime: number gameStartTime: number
numbersRevealed: boolean
} }
} }
@@ -159,13 +154,6 @@ export type CardSortingMove =
position: number // Which slot to remove from position: number // Which slot to remove from
} }
} }
| {
type: 'REVEAL_NUMBERS'
playerId: string
userId: string
timestamp: number
data: Record<string, never>
}
| { | {
type: 'CHECK_SOLUTION' type: 'CHECK_SOLUTION'
playerId: string playerId: string
@@ -188,7 +176,7 @@ export type CardSortingMove =
userId: string userId: string
timestamp: number timestamp: number
data: { data: {
field: 'cardCount' | 'showNumbers' | 'timeLimit' | 'gameMode' field: 'cardCount' | 'timeLimit' | 'gameMode'
value: unknown value: unknown
} }
} }
@@ -245,7 +233,6 @@ export interface SortingCardProps {
isPlaced: boolean isPlaced: boolean
isCorrect?: boolean // After checking solution isCorrect?: boolean // After checking solution
onClick: () => void onClick: () => void
showNumber: boolean // If revealed
} }
export interface PositionSlotProps { export interface PositionSlotProps {

View File

@@ -57,8 +57,7 @@ export function countInversions(userSeq: number[], correctSeq: number[]): number
export function calculateScore( export function calculateScore(
userSequence: number[], userSequence: number[],
correctSequence: number[], correctSequence: number[],
startTime: number, startTime: number
numbersRevealed: boolean
): ScoreBreakdown { ): ScoreBreakdown {
// LCS-based score (relative order) // LCS-based score (relative order)
const lcsLength = longestCommonSubsequence(userSequence, correctSequence) const lcsLength = longestCommonSubsequence(userSequence, correctSequence)
@@ -95,6 +94,5 @@ export function calculateScore(
exactPositionScore: Math.round(exactPositionScore), exactPositionScore: Math.round(exactPositionScore),
inversionScore: Math.round(inversionScore), inversionScore: Math.round(inversionScore),
elapsedTime: Math.floor((Date.now() - startTime) / 1000), elapsedTime: Math.floor((Date.now() - startTime) / 1000),
numbersRevealed,
} }
} }