feat(card-sorting): add CardPosition type and position syncing

Add viewport-relative position tracking for card-sorting game:
- Add CardPosition interface (x, y as % of viewport, rotation, zIndex)
- Add UPDATE_CARD_POSITIONS move type
- Add validateUpdateCardPositions validator method
- Add cardPositions array to CardSortingState

This enables real-time card position syncing across browser windows
using percentage-based coordinates that work across different viewport sizes.

🤖 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-23 14:32:51 -05:00
parent 5d6c800cee
commit 656f5a7838
2 changed files with 65 additions and 1 deletions

View File

@ -3,7 +3,7 @@ import type {
ValidationContext,
ValidationResult,
} from '@/lib/arcade/validation/types'
import type { CardSortingConfig, CardSortingMove, CardSortingState } from './types'
import type { CardSortingConfig, CardSortingMove, CardSortingState, CardPosition } from './types'
import { calculateScore } from './utils/scoringAlgorithm'
import { placeCardAtPosition, insertCardAtPosition, removeCardAtPosition } from './utils/validation'
@ -32,6 +32,8 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
return this.validateSetConfig(state, move.data.field, move.data.value)
case 'RESUME_GAME':
return this.validateResumeGame(state)
case 'UPDATE_CARD_POSITIONS':
return this.validateUpdateCardPositions(state, move.data.positions)
default:
return {
valid: false,
@ -81,6 +83,7 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
correctOrder: correctOrder as typeof state.correctOrder,
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,
},
@ -444,6 +447,48 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
}
}
private validateUpdateCardPositions(
state: CardSortingState,
positions: CardPosition[]
): ValidationResult {
// Must be in playing phase
if (state.gamePhase !== 'playing') {
return { valid: false, error: 'Can only update positions during playing phase' }
}
// Validate positions array
if (!Array.isArray(positions)) {
return { valid: false, error: 'positions must be an array' }
}
// Basic validation of position values
for (const pos of positions) {
if (typeof pos.x !== 'number' || pos.x < 0 || pos.x > 100) {
return { valid: false, error: 'x must be between 0 and 100' }
}
if (typeof pos.y !== 'number' || pos.y < 0 || pos.y > 100) {
return { valid: false, error: 'y must be between 0 and 100' }
}
if (typeof pos.rotation !== 'number') {
return { valid: false, error: 'rotation must be a number' }
}
if (typeof pos.zIndex !== 'number') {
return { valid: false, error: 'zIndex must be a number' }
}
if (typeof pos.cardId !== 'string') {
return { valid: false, error: 'cardId must be a string' }
}
}
return {
valid: true,
newState: {
...state,
cardPositions: positions,
},
}
}
isGameComplete(state: CardSortingState): boolean {
return state.gamePhase === 'results'
}
@ -467,6 +512,7 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
correctOrder: [],
availableCards: [],
placedCards: new Array(config.cardCount).fill(null),
cardPositions: [],
selectedCardId: null,
numbersRevealed: false,
scoreBreakdown: null,

View File

@ -33,6 +33,14 @@ export interface SortingCard {
svgContent: string // Serialized AbacusReact SVG
}
export interface CardPosition {
cardId: string
x: number // % of viewport width (0-100)
y: number // % of viewport height (0-100)
rotation: number // degrees (-15 to 15)
zIndex: number
}
export interface PlacedCard {
card: SortingCard // The card data
position: number // Which slot it's in (0-indexed)
@ -74,6 +82,7 @@ export interface CardSortingState extends GameState {
correctOrder: SortingCard[] // Sorted by number (answer key)
availableCards: SortingCard[] // Cards not yet placed
placedCards: (SortingCard | null)[] // Array of N slots (null = empty)
cardPositions: CardPosition[] // Viewport-relative positions for all cards
// UI state (client-only, not in server state)
selectedCardId: string | null // Currently selected card
@ -178,6 +187,15 @@ export type CardSortingMove =
timestamp: number
data: Record<string, never>
}
| {
type: 'UPDATE_CARD_POSITIONS'
playerId: string
userId: string
timestamp: number
data: {
positions: CardPosition[]
}
}
// ============================================================================
// Component Props