feat(arcade): update matching game and manifest schema
Updates to game break configuration and manifest validation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b197d8f035
commit
4f812d4ad0
|
|
@ -483,7 +483,13 @@ function InteractiveMatchingGame({ theme = 'light' }: { theme?: 'light' | 'dark'
|
|||
</div>
|
||||
|
||||
{/* Timer simulation */}
|
||||
<div className={css({ display: 'flex', flexDirection: 'column', gap: '0.25rem' })}>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.25rem',
|
||||
})}
|
||||
>
|
||||
<span className={css({ fontSize: '0.625rem', color: 'gray.400' })}>
|
||||
Timer: {Math.floor(elapsedSeconds / 60)}:
|
||||
{(elapsedSeconds % 60).toString().padStart(2, '0')} elapsed
|
||||
|
|
@ -611,7 +617,13 @@ export const Documentation: Story = {
|
|||
layout issues.
|
||||
</p>
|
||||
|
||||
<h2 className={css({ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '0.5rem' })}>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0.5rem',
|
||||
})}
|
||||
>
|
||||
Full Page Context Stories
|
||||
</h2>
|
||||
<p className={css({ marginBottom: '1rem', lineHeight: 1.6 })}>
|
||||
|
|
@ -637,7 +649,13 @@ export const Documentation: Story = {
|
|||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 className={css({ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '0.5rem' })}>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0.5rem',
|
||||
})}
|
||||
>
|
||||
Isolated Stories
|
||||
</h2>
|
||||
<p className={css({ marginBottom: '1rem', lineHeight: 1.6 })}>
|
||||
|
|
@ -645,7 +663,13 @@ export const Documentation: Story = {
|
|||
Compare these with Full Context stories to debug layout issues caused by nav positioning.
|
||||
</p>
|
||||
|
||||
<h2 className={css({ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '0.5rem' })}>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0.5rem',
|
||||
})}
|
||||
>
|
||||
Timer States
|
||||
</h2>
|
||||
<p className={css({ marginBottom: '1rem', lineHeight: 1.6 })}>
|
||||
|
|
@ -672,7 +696,13 @@ export const Documentation: Story = {
|
|||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 className={css({ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '0.5rem' })}>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0.5rem',
|
||||
})}
|
||||
>
|
||||
Architecture
|
||||
</h2>
|
||||
<ul
|
||||
|
|
@ -700,7 +730,13 @@ export const Documentation: Story = {
|
|||
</li>
|
||||
</ul>
|
||||
|
||||
<h2 className={css({ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '0.5rem' })}>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0.5rem',
|
||||
})}
|
||||
>
|
||||
Game Phases
|
||||
</h2>
|
||||
<ul
|
||||
|
|
|
|||
|
|
@ -418,6 +418,15 @@ export function MatchingProvider({ children }: { children: ReactNode }) {
|
|||
return false
|
||||
}
|
||||
|
||||
// Can't flip if session state hasn't been received yet (currentPlayer not set)
|
||||
// This prevents race condition where user clicks before server state arrives
|
||||
if (!state.currentPlayer) {
|
||||
console.log(
|
||||
'[RoomProvider][canFlipCard] Blocked: waiting for session state (currentPlayer empty)'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const card = gameCards.find((c) => c.id === cardId)
|
||||
if (!card || card.matched) {
|
||||
console.log('[RoomProvider][canFlipCard] Blocked: card not found or already matched')
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -405,7 +405,9 @@ export function SetupPhase() {
|
|||
>
|
||||
<span>🧮</span>
|
||||
<span
|
||||
className={css({ fontSize: isCompact ? '16px' : { base: '16px', md: '20px' } })}
|
||||
className={css({
|
||||
fontSize: isCompact ? '16px' : { base: '16px', md: '20px' },
|
||||
})}
|
||||
>
|
||||
↔️
|
||||
</span>
|
||||
|
|
@ -464,7 +466,9 @@ export function SetupPhase() {
|
|||
>
|
||||
<span>🤝</span>
|
||||
<span
|
||||
className={css({ fontSize: isCompact ? '16px' : { base: '16px', md: '20px' } })}
|
||||
className={css({
|
||||
fontSize: isCompact ? '16px' : { base: '16px', md: '20px' },
|
||||
})}
|
||||
>
|
||||
➕
|
||||
</span>
|
||||
|
|
@ -599,7 +603,10 @@ export function SetupPhase() {
|
|||
<>
|
||||
<div
|
||||
data-element="button-level"
|
||||
className={css({ fontSize: '14px', fontWeight: 'bold' })}
|
||||
className={css({
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
})}
|
||||
>
|
||||
{difficultyInfo[difficulty].label}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ export interface MatchingConfig extends GameConfig {
|
|||
gameType: GameType
|
||||
difficulty: Difficulty
|
||||
turnTimer: number
|
||||
/**
|
||||
* Skip the setup phase and start directly in playing phase.
|
||||
* When true, getInitialState() will generate cards immediately
|
||||
* and set gamePhase to 'playing' instead of 'setup'.
|
||||
*/
|
||||
skipSetupPhase?: boolean
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -37,9 +37,21 @@ const manifest: GameManifest = {
|
|||
minDurationMinutes: 1,
|
||||
maxDurationMinutes: 5,
|
||||
difficultyPresets: {
|
||||
easy: { selectedCount: 5, displayTime: 2.5, selectedDifficulty: 'beginner' },
|
||||
medium: { selectedCount: 5, displayTime: 2.0, selectedDifficulty: 'easy' },
|
||||
hard: { selectedCount: 8, displayTime: 1.5, selectedDifficulty: 'medium' },
|
||||
easy: {
|
||||
selectedCount: 5,
|
||||
displayTime: 2.5,
|
||||
selectedDifficulty: 'beginner',
|
||||
},
|
||||
medium: {
|
||||
selectedCount: 5,
|
||||
displayTime: 2.0,
|
||||
selectedDifficulty: 'easy',
|
||||
},
|
||||
hard: {
|
||||
selectedCount: 8,
|
||||
displayTime: 1.5,
|
||||
selectedDifficulty: 'medium',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,49 @@ export const PracticeBreakConfigSchema = z
|
|||
})
|
||||
.describe('Configuration for practice break behavior')
|
||||
|
||||
/**
|
||||
* Scoreboard category for cross-game comparison
|
||||
*/
|
||||
export const ScoreboardCategorySchema = z.enum([
|
||||
'puzzle',
|
||||
'memory',
|
||||
'speed',
|
||||
'strategy',
|
||||
'geography',
|
||||
])
|
||||
|
||||
/**
|
||||
* Schema for game results configuration.
|
||||
* Defines how a game reports results for display and scoreboard tracking.
|
||||
*/
|
||||
export const GameResultsConfigSchema = z
|
||||
.object({
|
||||
/**
|
||||
* Whether this game supports results reporting.
|
||||
* Games that support this should implement getResultsReport() in their validator.
|
||||
*/
|
||||
supportsResults: z.boolean(),
|
||||
|
||||
/**
|
||||
* How long to show results screen (ms).
|
||||
* Default is 5000ms (5 seconds).
|
||||
*/
|
||||
resultsDisplayDurationMs: z.number().min(1000).optional().default(5000),
|
||||
|
||||
/**
|
||||
* Custom component name for results display.
|
||||
* If not specified, the default GameBreakResultsScreen is used.
|
||||
*/
|
||||
customResultsComponent: z.string().optional(),
|
||||
|
||||
/**
|
||||
* Category for universal scoreboard.
|
||||
* Used for grouping and comparing scores across games.
|
||||
*/
|
||||
scoreboardCategory: ScoreboardCategorySchema.optional(),
|
||||
})
|
||||
.describe('Configuration for game results reporting')
|
||||
|
||||
/**
|
||||
* Schema for game manifest (game.yaml)
|
||||
*/
|
||||
|
|
@ -84,12 +127,17 @@ export const GameManifestSchema = z.object({
|
|||
'Configuration for practice break behavior including suggested defaults, ' +
|
||||
'locked fields, duration constraints, and difficulty presets.'
|
||||
),
|
||||
resultsConfig: GameResultsConfigSchema.optional().describe(
|
||||
'Configuration for game results reporting including display duration, ' +
|
||||
'scoreboard category, and custom component options.'
|
||||
),
|
||||
})
|
||||
|
||||
/**
|
||||
* Inferred TypeScript types from schemas
|
||||
*/
|
||||
export type PracticeBreakConfig = z.infer<typeof PracticeBreakConfigSchema>
|
||||
export type GameResultsConfig = z.infer<typeof GameResultsConfigSchema>
|
||||
export type GameManifest = z.infer<typeof GameManifestSchema>
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue