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:
parent
22634aa337
commit
ea5e3e838b
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue