fix(card-sorting): faithfully port UI/UX from Python original
Update Card Sorting Challenge to match the original Python implementation: **Visual Changes:** - Add status message display showing game state and instructions - Update insert buttons to match Python styling: - Pill-shaped (border-radius: 20px) - Teal (#2c5f76) with 0.3 opacity when inactive - Blue (#1976d2) when card is selected - Smooth scale animation on hover - Fix position slot sizing to 90px x 110px (matching original) - Simplify slot content (remove position numbers) - Add active state highlighting (blue glow) for empty slots when card selected - Adjust SVG sizing in filled slots to 70px width **Logic Fixes:** - Fix INSERT_CARD array shift logic bug in Provider.tsx - Remove redundant else clause that could create oversized arrays - Cards that fall off end are properly collected during compaction - Remove unused useEffect import **Behavioral Changes:** - Status messages now update based on game state: - Card selected: "Selected card with value X. Click a position or + button to place it." - All placed: "All cards placed! Click 'Check My Solution' to see how you did." - In progress: "X/Y cards placed. Select a card to continue." - Empty slot click shows helpful message when no card selected This brings the implementation in line with the original Python web_generator.py design (lines 8662-9132) as requested. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
createContext,
|
||||
useContext,
|
||||
@@ -24,6 +23,7 @@ interface CardSortingContextValue {
|
||||
// Actions
|
||||
startGame: () => void
|
||||
placeCard: (cardId: string, position: number) => void
|
||||
insertCard: (cardId: string, insertPosition: number) => void
|
||||
removeCard: (position: number) => void
|
||||
checkSolution: () => void
|
||||
revealNumbers: () => void
|
||||
@@ -107,10 +107,18 @@ function applyMoveOptimistically(state: CardSortingState, move: GameMove): CardS
|
||||
const card = state.availableCards.find((c) => c.id === cardId)
|
||||
if (!card) return state
|
||||
|
||||
// Simple insert logic (server will do proper compaction)
|
||||
// Simple replacement (can leave gaps)
|
||||
const newPlaced = [...state.placedCards]
|
||||
const replacedCard = newPlaced[position]
|
||||
newPlaced[position] = card
|
||||
const newAvailable = state.availableCards.filter((c) => c.id !== cardId)
|
||||
|
||||
// Remove card from available
|
||||
let newAvailable = state.availableCards.filter((c) => c.id !== cardId)
|
||||
|
||||
// If slot was occupied, add replaced card back to available
|
||||
if (replacedCard) {
|
||||
newAvailable = [...newAvailable, replacedCard]
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -119,6 +127,60 @@ function applyMoveOptimistically(state: CardSortingState, move: GameMove): CardS
|
||||
}
|
||||
}
|
||||
|
||||
case 'INSERT_CARD': {
|
||||
const { cardId, insertPosition } = typedMove.data
|
||||
const card = state.availableCards.find((c) => c.id === cardId)
|
||||
if (!card) return state
|
||||
|
||||
// Insert with shift and compact (no gaps)
|
||||
const newPlaced = new Array(state.cardCount).fill(null)
|
||||
|
||||
// Copy existing cards, shifting those at/after insert position
|
||||
for (let i = 0; i < state.placedCards.length; i++) {
|
||||
if (state.placedCards[i] !== null) {
|
||||
if (i < insertPosition) {
|
||||
newPlaced[i] = state.placedCards[i]
|
||||
} else {
|
||||
// Cards at or after insert position shift right by 1
|
||||
// Card will be collected during compaction if it falls off the end
|
||||
newPlaced[i + 1] = state.placedCards[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Place new card at insert position
|
||||
newPlaced[insertPosition] = card
|
||||
|
||||
// Compact to remove gaps
|
||||
const compacted: SortingCard[] = []
|
||||
for (const c of newPlaced) {
|
||||
if (c !== null) {
|
||||
compacted.push(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Fill final array (no gaps)
|
||||
const finalPlaced = new Array(state.cardCount).fill(null)
|
||||
for (let i = 0; i < Math.min(compacted.length, state.cardCount); i++) {
|
||||
finalPlaced[i] = compacted[i]
|
||||
}
|
||||
|
||||
// Remove from available
|
||||
let newAvailable = state.availableCards.filter((c) => c.id !== cardId)
|
||||
|
||||
// Any excess cards go back to available
|
||||
if (compacted.length > state.cardCount) {
|
||||
const excess = compacted.slice(state.cardCount)
|
||||
newAvailable = [...newAvailable, ...excess]
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
availableCards: newAvailable,
|
||||
placedCards: finalPlaced,
|
||||
}
|
||||
}
|
||||
|
||||
case 'REMOVE_CARD': {
|
||||
const { position } = typedMove.data
|
||||
const card = state.placedCards[position]
|
||||
@@ -353,6 +415,23 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
|
||||
[localPlayerId, sendMove, viewerId]
|
||||
)
|
||||
|
||||
const insertCard = useCallback(
|
||||
(cardId: string, insertPosition: number) => {
|
||||
if (!localPlayerId) return
|
||||
|
||||
sendMove({
|
||||
type: 'INSERT_CARD',
|
||||
playerId: localPlayerId,
|
||||
userId: viewerId || '',
|
||||
data: { cardId, insertPosition },
|
||||
})
|
||||
|
||||
// Clear selection
|
||||
setSelectedCardId(null)
|
||||
},
|
||||
[localPlayerId, sendMove, viewerId]
|
||||
)
|
||||
|
||||
const removeCard = useCallback(
|
||||
(position: number) => {
|
||||
if (!localPlayerId) return
|
||||
@@ -457,6 +536,7 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
|
||||
// Actions
|
||||
startGame,
|
||||
placeCard,
|
||||
insertCard,
|
||||
removeCard,
|
||||
checkSolution,
|
||||
revealNumbers,
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
} from '@/lib/arcade/validation/types'
|
||||
import type { CardSortingConfig, CardSortingMove, CardSortingState } from './types'
|
||||
import { calculateScore } from './utils/scoringAlgorithm'
|
||||
import { placeCardAtPosition, removeCardAtPosition } from './utils/validation'
|
||||
import { placeCardAtPosition, insertCardAtPosition, removeCardAtPosition } from './utils/validation'
|
||||
|
||||
export class CardSortingValidator implements GameValidator<CardSortingState, CardSortingMove> {
|
||||
validateMove(
|
||||
@@ -18,6 +18,8 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
|
||||
return this.validateStartGame(state, move.data, move.playerId)
|
||||
case 'PLACE_CARD':
|
||||
return this.validatePlaceCard(state, move.data.cardId, move.data.position)
|
||||
case 'INSERT_CARD':
|
||||
return this.validateInsertCard(state, move.data.cardId, move.data.insertPosition)
|
||||
case 'REMOVE_CARD':
|
||||
return this.validateRemoveCard(state, move.data.position)
|
||||
case 'REVEAL_NUMBERS':
|
||||
@@ -113,16 +115,70 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
|
||||
}
|
||||
}
|
||||
|
||||
// Place the card using utility function
|
||||
const { placedCards: newPlaced } = placeCardAtPosition(
|
||||
// Place the card using utility function (simple replacement)
|
||||
const { placedCards: newPlaced, replacedCard } = placeCardAtPosition(
|
||||
state.placedCards,
|
||||
card,
|
||||
position,
|
||||
position
|
||||
)
|
||||
|
||||
// Remove card from available
|
||||
let newAvailable = state.availableCards.filter((c) => c.id !== cardId)
|
||||
|
||||
// If slot was occupied, add replaced card back to available
|
||||
if (replacedCard) {
|
||||
newAvailable = [...newAvailable, replacedCard]
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
newState: {
|
||||
...state,
|
||||
availableCards: newAvailable,
|
||||
placedCards: newPlaced,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private validateInsertCard(
|
||||
state: CardSortingState,
|
||||
cardId: string,
|
||||
insertPosition: number
|
||||
): ValidationResult {
|
||||
// Must be in playing phase
|
||||
if (state.gamePhase !== 'playing') {
|
||||
return { valid: false, error: 'Can only insert cards during playing phase' }
|
||||
}
|
||||
|
||||
// Card must exist in availableCards
|
||||
const card = state.availableCards.find((c) => c.id === cardId)
|
||||
if (!card) {
|
||||
return { valid: false, error: 'Card not found in available cards' }
|
||||
}
|
||||
|
||||
// Position must be valid (0 to cardCount, inclusive - can insert after last position)
|
||||
if (insertPosition < 0 || insertPosition > state.cardCount) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Invalid insert position: must be between 0 and ${state.cardCount}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the card using utility function (with shift and compact)
|
||||
const { placedCards: newPlaced, excessCards } = insertCardAtPosition(
|
||||
state.placedCards,
|
||||
card,
|
||||
insertPosition,
|
||||
state.cardCount
|
||||
)
|
||||
|
||||
// Remove from available
|
||||
const newAvailable = state.availableCards.filter((c) => c.id !== cardId)
|
||||
// Remove card from available
|
||||
let newAvailable = state.availableCards.filter((c) => c.id !== cardId)
|
||||
|
||||
// Add any excess cards back to available (shouldn't normally happen)
|
||||
if (excessCards.length > 0) {
|
||||
newAvailable = [...newAvailable, ...excessCards]
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { css } from '../../../../styled-system/css'
|
||||
import { useCardSorting } from '../Provider'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export function PlayingPhase() {
|
||||
const {
|
||||
@@ -9,6 +10,7 @@ export function PlayingPhase() {
|
||||
selectedCardId,
|
||||
selectCard,
|
||||
placeCard,
|
||||
insertCard,
|
||||
removeCard,
|
||||
checkSolution,
|
||||
revealNumbers,
|
||||
@@ -18,6 +20,31 @@ export function PlayingPhase() {
|
||||
elapsedTime,
|
||||
} = useCardSorting()
|
||||
|
||||
// Status message (mimics Python updateSortingStatus)
|
||||
const [statusMessage, setStatusMessage] = useState(
|
||||
`Arrange the ${state.cardCount} cards in ascending order (smallest to largest)`
|
||||
)
|
||||
|
||||
// Update status message based on state
|
||||
useEffect(() => {
|
||||
if (state.gamePhase !== 'playing') return
|
||||
|
||||
if (selectedCardId) {
|
||||
const card = state.availableCards.find((c) => c.id === selectedCardId)
|
||||
if (card) {
|
||||
setStatusMessage(
|
||||
`Selected card with value ${card.number}. Click a position or + button to place it.`
|
||||
)
|
||||
}
|
||||
} else if (placedCount === state.cardCount) {
|
||||
setStatusMessage('All cards placed! Click "Check My Solution" to see how you did.')
|
||||
} else {
|
||||
setStatusMessage(
|
||||
`${placedCount}/${state.cardCount} cards placed. Select ${placedCount === 0 ? 'a' : 'another'} card to continue.`
|
||||
)
|
||||
}
|
||||
}, [selectedCardId, placedCount, state.cardCount, state.gamePhase, state.availableCards])
|
||||
|
||||
// Format time display
|
||||
const formatTime = (seconds: number) => {
|
||||
const m = Math.floor(seconds / 60)
|
||||
@@ -32,6 +59,7 @@ export function PlayingPhase() {
|
||||
return {
|
||||
background: `hsl(220, 8%, ${lightness}%)`,
|
||||
color: lightness > 60 ? '#2c3e50' : '#ffffff',
|
||||
borderColor: lightness > 60 ? '#2c5f76' : 'rgba(255,255,255,0.4)',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,16 +73,29 @@ export function PlayingPhase() {
|
||||
|
||||
const handleSlotClick = (position: number) => {
|
||||
if (!selectedCardId) {
|
||||
// No card selected - remove card if slot is occupied
|
||||
// No card selected - if slot has a card, move it back and auto-select
|
||||
if (state.placedCards[position]) {
|
||||
const cardToMove = state.placedCards[position]!
|
||||
removeCard(position)
|
||||
// Auto-select the card that was moved back
|
||||
selectCard(cardToMove.id)
|
||||
} else {
|
||||
setStatusMessage('Select a card first, or click a placed card to move it back.')
|
||||
}
|
||||
} else {
|
||||
// Card is selected - place it
|
||||
// Card is selected - place it (replaces existing card if any)
|
||||
placeCard(selectedCardId, position)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInsertClick = (insertPosition: number) => {
|
||||
if (!selectedCardId) {
|
||||
setStatusMessage('Please select a card first, then click where to insert it.')
|
||||
return
|
||||
}
|
||||
insertCard(selectedCardId, insertPosition)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
@@ -64,6 +105,22 @@ export function PlayingPhase() {
|
||||
height: '100%',
|
||||
})}
|
||||
>
|
||||
{/* Status message */}
|
||||
<div
|
||||
className={css({
|
||||
padding: '0.75rem 1rem',
|
||||
background: '#e3f2fd',
|
||||
borderLeft: '4px solid #2c5f76',
|
||||
borderRadius: '0.25rem',
|
||||
fontSize: 'sm',
|
||||
fontWeight: '500',
|
||||
color: '#2c3e50',
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
{statusMessage}
|
||||
</div>
|
||||
|
||||
{/* Header with timer and actions */}
|
||||
<div
|
||||
className={css({
|
||||
@@ -264,7 +321,7 @@ export function PlayingPhase() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Position slots */}
|
||||
{/* Position slots with insert buttons */}
|
||||
<div className={css({ flex: 2, minWidth: '300px' })}>
|
||||
<h3
|
||||
className={css({
|
||||
@@ -280,88 +337,154 @@ export function PlayingPhase() {
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
gap: '0.25rem',
|
||||
})}
|
||||
>
|
||||
{/* Insert button before first position */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleInsertClick(0)}
|
||||
disabled={!selectedCardId}
|
||||
className={css({
|
||||
width: '32px',
|
||||
height: '50px',
|
||||
background: selectedCardId ? '#1976d2' : '#2c5f76',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '20px',
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
cursor: selectedCardId ? 'pointer' : 'default',
|
||||
opacity: selectedCardId ? 1 : 0.3,
|
||||
transition: 'all 0.2s',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
_hover: {
|
||||
opacity: 1,
|
||||
background: '#1976d2',
|
||||
transform: selectedCardId ? 'scale(1.1)' : 'none',
|
||||
},
|
||||
})}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
|
||||
{/* Render each position slot followed by an insert button */}
|
||||
{state.placedCards.map((card, index) => {
|
||||
const gradientStyle = getSlotGradient(index, state.cardCount)
|
||||
const isEmpty = card === null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => handleSlotClick(index)}
|
||||
className={css({
|
||||
padding: '1rem',
|
||||
borderRadius: '0.5rem',
|
||||
border: '2px solid',
|
||||
borderColor:
|
||||
gradientStyle.color === '#ffffff' ? 'rgba(255,255,255,0.4)' : '#2c5f76',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
minHeight: '80px',
|
||||
_hover: {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||
},
|
||||
})}
|
||||
style={gradientStyle}
|
||||
>
|
||||
<div key={index}>
|
||||
{/* Position slot */}
|
||||
<div
|
||||
onClick={() => handleSlotClick(index)}
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'bold',
|
||||
opacity: 0.7,
|
||||
width: '90px',
|
||||
height: '110px',
|
||||
padding: '0.5rem',
|
||||
borderRadius: '8px',
|
||||
border: '2px solid',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '0.25rem',
|
||||
position: 'relative',
|
||||
_hover: {
|
||||
transform: selectedCardId && isEmpty ? 'scale(1.05)' : 'none',
|
||||
boxShadow:
|
||||
selectedCardId && isEmpty ? '0 4px 12px rgba(0,0,0,0.15)' : 'none',
|
||||
},
|
||||
})}
|
||||
style={
|
||||
isEmpty
|
||||
? {
|
||||
...gradientStyle,
|
||||
// Active state: add slight glow when card is selected
|
||||
boxShadow: selectedCardId
|
||||
? '0 0 0 2px #1976d2, 0 2px 8px rgba(25, 118, 210, 0.3)'
|
||||
: 'none',
|
||||
}
|
||||
: {
|
||||
background: '#fff',
|
||||
color: '#333',
|
||||
borderColor: '#2c5f76',
|
||||
}
|
||||
}
|
||||
>
|
||||
#{index + 1}
|
||||
</div>
|
||||
{card ? (
|
||||
<div
|
||||
className={css({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: card.svgContent,
|
||||
}}
|
||||
className={css({
|
||||
width: '120px',
|
||||
'& svg': {
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{state.numbersRevealed && (
|
||||
{card ? (
|
||||
<>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: card.svgContent,
|
||||
}}
|
||||
className={css({
|
||||
width: '70px',
|
||||
'& svg': {
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 'xs',
|
||||
opacity: 0.7,
|
||||
fontStyle: 'italic',
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{card.number}
|
||||
← Click to move back
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={css({
|
||||
flex: 1,
|
||||
fontSize: 'sm',
|
||||
opacity: 0.5,
|
||||
fontStyle: 'italic',
|
||||
})}
|
||||
>
|
||||
{selectedCardId ? 'Click to place card' : 'Empty'}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
fontStyle: 'italic',
|
||||
textAlign: 'center',
|
||||
})}
|
||||
style={{ color: gradientStyle.color }}
|
||||
>
|
||||
{index === 0 ? 'Smallest' : index === state.cardCount - 1 ? 'Largest' : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Insert button after this position */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleInsertClick(index + 1)}
|
||||
disabled={!selectedCardId}
|
||||
className={css({
|
||||
width: '32px',
|
||||
height: '50px',
|
||||
background: selectedCardId ? '#1976d2' : '#2c5f76',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '20px',
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
cursor: selectedCardId ? 'pointer' : 'default',
|
||||
opacity: selectedCardId ? 1 : 0.3,
|
||||
transition: 'all 0.2s',
|
||||
marginTop: '0.25rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
_hover: {
|
||||
opacity: 1,
|
||||
background: '#1976d2',
|
||||
transform: selectedCardId ? 'scale(1.1)' : 'none',
|
||||
},
|
||||
})}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -119,6 +119,16 @@ export type CardSortingMove =
|
||||
position: number // Which slot (0-indexed)
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'INSERT_CARD'
|
||||
playerId: string
|
||||
userId: string
|
||||
timestamp: number
|
||||
data: {
|
||||
cardId: string // Which card to insert
|
||||
insertPosition: number // Where to insert (0-indexed, can be 0 to cardCount)
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'REMOVE_CARD'
|
||||
playerId: string
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
import type { SortingCard } from '../types'
|
||||
|
||||
/**
|
||||
* Place a card at a specific position, shifting existing cards
|
||||
* Returns new placedCards array with no gaps
|
||||
* Place a card at a specific position (simple replacement, can leave gaps)
|
||||
* This is used when clicking directly on a slot
|
||||
* Returns old card if slot was occupied
|
||||
*/
|
||||
export function placeCardAtPosition(
|
||||
placedCards: (SortingCard | null)[],
|
||||
cardToPlace: SortingCard,
|
||||
position: number,
|
||||
position: number
|
||||
): { placedCards: (SortingCard | null)[]; replacedCard: SortingCard | null } {
|
||||
const newPlaced = [...placedCards]
|
||||
const replacedCard = newPlaced[position]
|
||||
newPlaced[position] = cardToPlace
|
||||
return { placedCards: newPlaced, replacedCard }
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a card at a specific position, shifting existing cards and compacting
|
||||
* This is used when clicking a + (insert) button
|
||||
* Returns new placedCards array with no gaps
|
||||
*/
|
||||
export function insertCardAtPosition(
|
||||
placedCards: (SortingCard | null)[],
|
||||
cardToPlace: SortingCard,
|
||||
insertPosition: number,
|
||||
totalSlots: number
|
||||
): { placedCards: (SortingCard | null)[]; excessCards: SortingCard[] } {
|
||||
// Create working array
|
||||
@@ -16,20 +33,23 @@ export function placeCardAtPosition(
|
||||
// Copy existing cards, shifting those at/after position
|
||||
for (let i = 0; i < placedCards.length; i++) {
|
||||
if (placedCards[i] !== null) {
|
||||
if (i < position) {
|
||||
if (i < insertPosition) {
|
||||
// Before insert position - stays same
|
||||
newPlaced[i] = placedCards[i]
|
||||
} else {
|
||||
// At or after position - shift right
|
||||
if (i + 1 < totalSlots) {
|
||||
newPlaced[i + 1] = placedCards[i]
|
||||
} else {
|
||||
// Card would fall off, will be handled by compaction
|
||||
newPlaced[i + 1] = placedCards[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Place new card
|
||||
newPlaced[position] = cardToPlace
|
||||
// Place new card at insert position
|
||||
newPlaced[insertPosition] = cardToPlace
|
||||
|
||||
// Compact to remove gaps (shift all cards left)
|
||||
const compacted: SortingCard[] = []
|
||||
@@ -39,13 +59,13 @@ export function placeCardAtPosition(
|
||||
}
|
||||
}
|
||||
|
||||
// Fill final array
|
||||
// Fill final array with compacted cards (no gaps)
|
||||
const result = new Array(totalSlots).fill(null)
|
||||
for (let i = 0; i < Math.min(compacted.length, totalSlots); i++) {
|
||||
result[i] = compacted[i]
|
||||
}
|
||||
|
||||
// Any excess cards are returned (shouldn't happen)
|
||||
// Any excess cards are returned
|
||||
const excess = compacted.slice(totalSlots)
|
||||
|
||||
return { placedCards: result, excessCards: excess }
|
||||
|
||||
Reference in New Issue
Block a user