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

View File

@ -22,8 +22,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
return this.validateInsertCard(state, move.data.cardId, move.data.insertPosition)
case 'REMOVE_CARD':
return this.validateRemoveCard(state, move.data.position)
case 'REVEAL_NUMBERS':
return this.validateRevealNumbers(state)
case 'CHECK_SOLUTION':
return this.validateCheckSolution(state, move.data.finalSequence)
case 'GO_TO_SETUP':
@ -84,7 +82,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
availableCards: selectedCards as typeof state.availableCards,
placedCards: new Array(state.cardCount).fill(null),
cardPositions: [], // Will be set by first position update
numbersRevealed: false,
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(
state: CardSortingState,
finalSequence?: typeof state.selectedCards
@ -291,8 +260,7 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
const scoreBreakdown = calculateScore(
userSequence,
correctSequence,
state.gameStartTime || Date.now(),
state.numbersRevealed
state.gameStartTime || Date.now()
)
// If finalSequence was provided, update placedCards with it
@ -321,13 +289,11 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
newState: {
...this.getInitialState({
cardCount: state.cardCount,
showNumbers: state.showNumbers,
timeLimit: state.timeLimit,
gameMode: state.gameMode,
}),
originalConfig: {
cardCount: state.cardCount,
showNumbers: state.showNumbers,
timeLimit: state.timeLimit,
gameMode: state.gameMode,
},
@ -338,7 +304,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
placedCards: state.placedCards,
cardPositions: state.cardPositions,
gameStartTime: state.gameStartTime || Date.now(),
numbersRevealed: state.numbersRevealed,
},
},
}
@ -349,7 +314,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
valid: true,
newState: this.getInitialState({
cardCount: state.cardCount,
showNumbers: state.showNumbers,
timeLimit: state.timeLimit,
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':
if (value !== null && (typeof value !== 'number' || value < 30)) {
return {
@ -463,7 +412,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
placedCards: state.pausedGameState.placedCards,
cardPositions: state.pausedGameState.cardPositions,
gameStartTime: state.pausedGameState.gameStartTime,
numbersRevealed: state.pausedGameState.numbersRevealed,
pausedGamePhase: undefined,
pausedGameState: undefined,
},
@ -519,7 +467,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
getInitialState(config: CardSortingConfig): CardSortingState {
return {
cardCount: config.cardCount,
showNumbers: config.showNumbers,
timeLimit: config.timeLimit,
gameMode: config.gameMode,
gamePhase: 'setup',
@ -541,7 +488,6 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
cardPositions: [],
cursorPositions: new Map(),
selectedCardId: null,
numbersRevealed: false,
scoreBreakdown: null,
}
}

View File

@ -998,7 +998,6 @@ export function PlayingPhaseDrag() {
}, [activityFeed])
// Track previous state for detecting changes
const prevNumbersRevealedRef = useRef(state.numbersRevealed)
const prevDraggingPlayersRef = useRef<Set<string>>(new Set())
// Detect state changes and generate activity notifications
@ -1023,21 +1022,7 @@ export function PlayingPhaseDrag() {
}
prevDraggingPlayersRef.current = currentlyDragging
// 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,
])
}, [state.cardPositions, state.gameMode, localPlayerId, addActivityNotification])
// Handle viewport resize
useEffect(() => {
@ -1696,39 +1681,6 @@ export function PlayingPhaseDrag() {
Cards in correct position
</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>
)}
@ -1745,32 +1697,6 @@ export function PlayingPhaseDrag() {
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 */}
<div
className={css({

View File

@ -2,8 +2,7 @@
import { css } from '../../../../styled-system/css'
import { useCardSorting } from '../Provider'
import { useSpring, animated, config, useSprings } from '@react-spring/web'
import { useState, useEffect, useRef, useMemo } from 'react'
import { useState, useEffect } from 'react'
import type { SortingCard } from '../types'
// Add result animations
@ -32,12 +31,6 @@ if (typeof document !== 'undefined') {
document.head.appendChild(style)
}
interface CardPosition {
x: number
y: number
rotation: number
}
export function ResultsPhase() {
const { state, startGame, goToSetup, exitSession, players } = useCardSorting()
const { scoreBreakdown } = state
@ -49,168 +42,14 @@ export function ResultsPhase() {
// Get user's sequence from placedCards
const userSequence = state.placedCards.filter((c): c is SortingCard => c !== null)
// Get viewport dimensions for converting percentage positions to pixels
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
// Show corrections after a delay
useEffect(() => {
const timer = setTimeout(() => {
setShowCorrections(true)
}, 1500)
}, 1000)
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) {
return (
<div
@ -255,6 +94,7 @@ export function ResultsPhase() {
className={css({
width: '100%',
height: '100%',
display: 'flex',
position: 'fixed',
top: 0,
left: 0,
@ -263,146 +103,115 @@ export function ResultsPhase() {
background: 'linear-gradient(135deg, #f0f9ff, #e0f2fe)',
})}
>
{/* Full viewport for cards (same coordinate system as game board) */}
{/* Cards Grid Area */}
<div
ref={containerRef}
className={css({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
overflow: 'hidden',
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '40px',
overflow: 'auto',
})}
>
{/* Cards with animated positions */}
{userSequence.map((card, userIndex) => {
const spring = springs[userIndex]
if (!spring) return null
<div
className={css({
display: 'grid',
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
// 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 */}
return (
<div
key={card.id}
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',
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)',
width: '160px',
height: '200px',
})}
dangerouslySetInnerHTML={{ __html: card.svgContent }}
/>
{/* Correct/Incorrect indicator */}
{showCorrections && (
>
{/* Card */}
<div
className={css({
position: 'absolute',
top: '-12px',
right: '-12px',
width: '32px',
height: '32px',
borderRadius: '50%',
background: isCorrect ? '#22c55e' : '#ef4444',
width: '100%',
height: '100%',
background: 'white',
borderRadius: '8px',
border: '3px solid',
borderColor: isCorrect ? '#22c55e' : showCorrections ? '#ef4444' : '#0369a1',
display: 'flex',
alignItems: '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',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: 'bold',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.2)',
animation: 'scoreReveal 0.4s ease-out',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
})}
>
{isCorrect ? '✓' : '✗'}
#{showCorrections ? correctIndex + 1 : userIndex + 1}
</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>
</animated.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 */}
<animated.div
style={panelSpring}
<div
className={css({
position: 'fixed',
right: 0,
top: 0,
bottom: 0,
width: '400px',
background: 'rgba(255, 255, 255, 0.95)',
borderLeft: '3px solid rgba(59, 130, 246, 0.3)',
@ -800,7 +609,7 @@ export function ResultsPhase() {
🚪 Exit
</button>
</div>
</animated.div>
</div>
</div>
)
}

View File

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

View File

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