Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f160d2e4af | ||
|
|
d14979907c |
@@ -1,3 +1,10 @@
|
||||
## [2.12.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.11.0...v2.12.0) (2025-10-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add networked hover state infrastructure for multiplayer presence ([d149799](https://github.com/antialias/soroban-abacus-flashcards/commit/d14979907c5df9b793a1c110028fc5b54457f507))
|
||||
|
||||
## [2.11.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.10.1...v2.11.0) (2025-10-09)
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ const initialState: MemoryPairsState = {
|
||||
originalConfig: undefined,
|
||||
pausedGamePhase: undefined,
|
||||
pausedGameState: undefined,
|
||||
// HOVER: Initialize hover state
|
||||
playerHovers: {},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,6 +196,17 @@ function applyMoveOptimistically(state: MemoryPairsState, move: GameMove): Memor
|
||||
}
|
||||
}
|
||||
|
||||
case 'HOVER_CARD': {
|
||||
// Update player hover state for networked presence
|
||||
return {
|
||||
...state,
|
||||
playerHovers: {
|
||||
...state.playerHovers,
|
||||
[move.playerId]: move.data.cardId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
@@ -514,6 +527,22 @@ export function RoomMemoryPairsProvider({ children }: { children: ReactNode }) {
|
||||
})
|
||||
}, [canResumeGame, activePlayers, state.currentPlayer, sendMove])
|
||||
|
||||
const hoverCard = useCallback(
|
||||
(cardId: string | null) => {
|
||||
// HOVER: Send hover state for networked presence
|
||||
// Use current player as the one hovering
|
||||
const playerId = state.currentPlayer || activePlayers[0] || ''
|
||||
if (!playerId) return // No active player to send hover for
|
||||
|
||||
sendMove({
|
||||
type: 'HOVER_CARD',
|
||||
playerId,
|
||||
data: { cardId },
|
||||
})
|
||||
},
|
||||
[state.currentPlayer, activePlayers, sendMove]
|
||||
)
|
||||
|
||||
// NO MORE effectiveState merging! Just use session state directly with gameMode added
|
||||
const effectiveState = { ...state, gameMode } as MemoryPairsState & { gameMode: GameMode }
|
||||
|
||||
@@ -536,6 +565,7 @@ export function RoomMemoryPairsProvider({ children }: { children: ReactNode }) {
|
||||
setGameType,
|
||||
setDifficulty,
|
||||
setTurnTimer,
|
||||
hoverCard,
|
||||
exitSession,
|
||||
gameMode,
|
||||
activePlayers,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ArcadeGuardedPage } from '@/components/ArcadeGuardedPage'
|
||||
import { MemoryPairsGame } from './components/MemoryPairsGame'
|
||||
import { ArcadeMemoryPairsProvider } from './context/ArcadeMemoryPairsContext'
|
||||
import { RoomMemoryPairsProvider } from './context/RoomMemoryPairsProvider'
|
||||
|
||||
export default function MatchingPage() {
|
||||
return (
|
||||
<ArcadeGuardedPage>
|
||||
<ArcadeMemoryPairsProvider>
|
||||
<RoomMemoryPairsProvider>
|
||||
<MemoryPairsGame />
|
||||
</ArcadeMemoryPairsProvider>
|
||||
</RoomMemoryPairsProvider>
|
||||
</ArcadeGuardedPage>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -103,6 +103,9 @@ export interface MemoryPairsState {
|
||||
isProcessingMove: boolean
|
||||
showMismatchFeedback: boolean
|
||||
lastMatchedPair: [string, string] | null
|
||||
|
||||
// Hover state for networked presence
|
||||
playerHovers: { [playerId: string]: string | null } // playerId -> cardId (or null if not hovering)
|
||||
}
|
||||
|
||||
export type MemoryPairsAction =
|
||||
@@ -143,6 +146,7 @@ export interface MemoryPairsContextValue {
|
||||
setGameType: (type: GameType) => void
|
||||
setDifficulty: (difficulty: Difficulty) => void
|
||||
setTurnTimer: (timer: number) => void
|
||||
hoverCard: (cardId: string | null) => void // Send hover state for networked presence
|
||||
goToSetup: () => void
|
||||
exitSession: () => void // Exit arcade session (no-op for non-arcade mode)
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ export class MatchingGameValidator implements GameValidator<MemoryPairsState, Ma
|
||||
case 'RESUME_GAME':
|
||||
return this.validateResumeGame(state)
|
||||
|
||||
case 'HOVER_CARD':
|
||||
return this.validateHoverCard(state, move.data.cardId, move.playerId)
|
||||
|
||||
default:
|
||||
return {
|
||||
valid: false,
|
||||
@@ -479,6 +482,31 @@ export class MatchingGameValidator implements GameValidator<MemoryPairsState, Ma
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate hover state update for networked presence
|
||||
*
|
||||
* Hover moves are lightweight and always valid - they just update
|
||||
* which card a player is hovering over for UI feedback to other players.
|
||||
*/
|
||||
private validateHoverCard(
|
||||
state: MemoryPairsState,
|
||||
cardId: string | null,
|
||||
playerId: string
|
||||
): ValidationResult {
|
||||
// Hover is always valid - it's just UI state for networked presence
|
||||
// Update the player's hover state
|
||||
return {
|
||||
valid: true,
|
||||
newState: {
|
||||
...state,
|
||||
playerHovers: {
|
||||
...state.playerHovers,
|
||||
[playerId]: cardId,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
isGameComplete(state: MemoryPairsState): boolean {
|
||||
return state.gamePhase === 'results' || state.matchedPairs === state.totalPairs
|
||||
}
|
||||
@@ -516,6 +544,8 @@ export class MatchingGameValidator implements GameValidator<MemoryPairsState, Ma
|
||||
originalConfig: undefined,
|
||||
pausedGamePhase: undefined,
|
||||
pausedGameState: undefined,
|
||||
// HOVER: Initialize hover state
|
||||
playerHovers: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,13 @@ export interface MatchingResumeGameMove extends GameMove {
|
||||
data: Record<string, never>
|
||||
}
|
||||
|
||||
export interface MatchingHoverCardMove extends GameMove {
|
||||
type: 'HOVER_CARD'
|
||||
data: {
|
||||
cardId: string | null // null when mouse leaves card
|
||||
}
|
||||
}
|
||||
|
||||
export type MatchingGameMove =
|
||||
| MatchingFlipCardMove
|
||||
| MatchingStartGameMove
|
||||
@@ -68,6 +75,7 @@ export type MatchingGameMove =
|
||||
| MatchingGoToSetupMove
|
||||
| MatchingSetConfigMove
|
||||
| MatchingResumeGameMove
|
||||
| MatchingHoverCardMove
|
||||
|
||||
// Generic game state union
|
||||
export type GameState = MemoryPairsState // Add other game states as union later
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "2.11.0",
|
||||
"version": "2.12.0",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user