feat(know-your-world): add turn-based restrictions for letter typing

In turn-based learning mode:
- Show current player's emoji next to typing instruction
- Only allow the current player to type letters
- Show "Waiting for [player] to type..." when it's not your turn
- Display "Not your turn!" notice when attempting to type during another player's turn

This makes it clear whose turn it is and prevents confusion in multiplayer games.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-30 20:34:51 -06:00
parent f5ce53efc0
commit 45730bb4db
1 changed files with 101 additions and 16 deletions

View File

@ -119,7 +119,13 @@ export function GameInfoPanel({
} = controlsState
// Get game state values
const { giveUpVotes = [], activeUserIds = [], gameMode } = state
const {
giveUpVotes = [],
activeUserIds = [],
gameMode,
currentPlayer,
playerMetadata = {},
} = state
// Get viewer ID for vote checking
const { data: viewerId } = useViewerId()
@ -127,6 +133,23 @@ export function GameInfoPanel({
// Touch device detection for virtual keyboard
const isTouchDevice = useIsTouchDevice()
// Track "not your turn" notification
const [showNotYourTurn, setShowNotYourTurn] = useState(false)
// Check if it's the local viewer's turn (for turn-based mode)
const isMyTurn = useMemo(() => {
if (gameMode !== 'turn-based') return true // Always "your turn" in non-turn-based modes
if (!currentPlayer || !viewerId) return false
const currentPlayerMeta = playerMetadata[currentPlayer]
return currentPlayerMeta?.userId === viewerId
}, [gameMode, currentPlayer, viewerId, playerMetadata])
// Get current player's emoji for display
const currentPlayerEmoji = useMemo(() => {
if (!currentPlayer) return null
return playerMetadata[currentPlayer]?.emoji || null
}, [currentPlayer, playerMetadata])
// Music context and modal state
const music = useMusic()
const [isMusicModalOpen, setIsMusicModalOpen] = useState(false)
@ -334,6 +357,20 @@ export function GameInfoPanel({
return
}
// Only accept single character keys (letters only)
const pressedLetter = e.key.toLowerCase()
if (pressedLetter.length !== 1 || !/[a-z]/i.test(pressedLetter)) {
return
}
// In turn-based mode, only allow the current player to type
if (gameMode === 'turn-based' && !isMyTurn) {
setShowNotYourTurn(true)
// Auto-hide the notice after 2 seconds
setTimeout(() => setShowNotYourTurn(false), 2000)
return
}
// Use optimistic count to prevent race conditions when typing fast
const nextLetterIndex = optimisticLetterCountRef.current
if (nextLetterIndex >= requiresNameConfirmation) {
@ -350,23 +387,26 @@ export function GameInfoPanel({
// Normalize accented letters to base ASCII (e.g., 'é' → 'e', 'ñ' → 'n')
// so users can type region names like "Côte d'Ivoire" or "São Tomé" with a regular keyboard
const expectedLetter = normalizeToBaseLetter(letterInfo.char)
const pressedLetter = e.key.toLowerCase()
// Only accept single character keys (letters only, no space needed since we skip spaces)
if (pressedLetter.length === 1 && /[a-z]/i.test(pressedLetter)) {
if (pressedLetter === expectedLetter) {
// Optimistically advance count before server responds
optimisticLetterCountRef.current = nextLetterIndex + 1
// Dispatch to shared state - server validates and broadcasts to all sessions
confirmLetter(pressedLetter, nextLetterIndex)
}
// Ignore wrong characters silently (no feedback, no backspace needed)
if (pressedLetter === expectedLetter) {
// Optimistically advance count before server responds
optimisticLetterCountRef.current = nextLetterIndex + 1
// Dispatch to shared state - server validates and broadcasts to all sessions
confirmLetter(pressedLetter, nextLetterIndex)
}
// Ignore wrong characters silently (no feedback, no backspace needed)
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [requiresNameConfirmation, nameConfirmed, currentRegionName, confirmLetter])
}, [
requiresNameConfirmation,
nameConfirmed,
currentRegionName,
confirmLetter,
gameMode,
isMyTurn,
])
// Check if animation is in progress based on timestamp
useEffect(() => {
@ -653,8 +693,37 @@ export function GameInfoPanel({
gap: '1.5',
})}
>
<span></span>
<span>Type the underlined letter{requiresNameConfirmation > 1 ? 's' : ''}</span>
{/* In turn-based mode, show current player's emoji to indicate whose turn it is */}
{gameMode === 'turn-based' && currentPlayerEmoji ? (
<span>{currentPlayerEmoji}</span>
) : (
<span></span>
)}
<span>
{gameMode === 'turn-based' && !isMyTurn
? `Waiting for ${playerMetadata[currentPlayer]?.name || 'player'} to type...`
: `Type the underlined letter${requiresNameConfirmation > 1 ? 's' : ''}`}
</span>
</div>
)}
{/* "Not your turn" notice */}
{showNotYourTurn && (
<div
data-element="not-your-turn-notice"
className={css({
marginTop: '3',
padding: '2 4',
bg: isDark ? 'red.900/80' : 'red.100',
color: isDark ? 'red.200' : 'red.800',
rounded: 'lg',
fontSize: 'sm',
fontWeight: 'medium',
textAlign: 'center',
})}
>
Not your turn! Wait for {playerMetadata[currentPlayer]?.name || 'the other player'}
.
</div>
)}
</animated.div>
@ -688,6 +757,13 @@ export function GameInfoPanel({
})()}
isDark={isDark}
onKeyPress={(letter) => {
// In turn-based mode, only allow the current player to type
if (gameMode === 'turn-based' && !isMyTurn) {
setShowNotYourTurn(true)
setTimeout(() => setShowNotYourTurn(false), 2000)
return
}
const nextLetterIndex = optimisticLetterCountRef.current
if (nextLetterIndex >= requiresNameConfirmation) return
@ -1218,8 +1294,17 @@ export function GameInfoPanel({
gap: '1.5',
})}
>
<span></span>
<span>Type the underlined letter{requiresNameConfirmation > 1 ? 's' : ''}</span>
{/* In turn-based mode, show current player's emoji to indicate whose turn it is */}
{gameMode === 'turn-based' && currentPlayerEmoji ? (
<span>{currentPlayerEmoji}</span>
) : (
<span></span>
)}
<span>
{gameMode === 'turn-based' && !isMyTurn
? `Waiting for ${playerMetadata[currentPlayer]?.name || 'player'} to type...`
: `Type the underlined letter${requiresNameConfirmation > 1 ? 's' : ''}`}
</span>
</div>
)}
</div>