All files / web/src/lib/arcade manifest-schema.ts

0% Statements 0/187
0% Branches 0/1
0% Functions 0/1
0% Lines 0/187

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188                                                                                                                                                                                                                                                                                                                                                                                       
/**
 * Game manifest schema validation
 * Validates game.yaml files using Zod
 */

import { z } from 'zod'

/**
 * Schema for practice break configuration.
 * Defines how a game should behave during practice session game breaks.
 */
export const PracticeBreakConfigSchema = z
  .object({
    /**
     * Suggested default configuration for practice breaks.
     * These settings are optimized for quick 2-10 minute games.
     */
    suggestedConfig: z.record(z.string(), z.unknown()).optional(),

    /**
     * Config fields that should be locked during practice breaks.
     * Prevents kids from making games too long or complex.
     */
    lockedFields: z.array(z.string()).optional(),

    /**
     * Minimum duration in minutes this game reasonably supports.
     * Games shorter than the break duration work best.
     */
    minDurationMinutes: z.number().min(1).optional(),

    /**
     * Maximum duration in minutes this game can reasonably take.
     * Helps the system choose appropriate games for break length.
     */
    maxDurationMinutes: z.number().min(1).optional(),

    /**
     * Difficulty presets for quick teacher selection.
     * Keys: 'easy', 'medium', 'hard' with partial config values.
     */
    difficultyPresets: z
      .object({
        easy: z.record(z.string(), z.unknown()).optional(),
        medium: z.record(z.string(), z.unknown()).optional(),
        hard: z.record(z.string(), z.unknown()).optional(),
      })
      .optional(),

    /**
     * Per-field UI metadata for the customize panel.
     * Overrides the generic FIELD_CONFIG defaults in GameBreakCustomConfig.
     * Games should specify this for any field that needs game-specific labels or options.
     */
    fieldConfig: z
      .record(
        z.string(),
        z.object({
          label: z.string(),
          type: z.enum(['number', 'boolean', 'select']),
          options: z.array(z.object({ value: z.unknown(), label: z.string() })).optional(),
          min: z.number().optional(),
          max: z.number().optional(),
          step: z.number().optional(),
        })
      )
      .optional(),
  })
  .describe('Configuration for practice break behavior')

/**
 * Scoreboard category for cross-game comparison
 */
export const ScoreboardCategorySchema = z.enum([
  'puzzle',
  'memory',
  'speed',
  'strategy',
  'geography',
  'discovery',
])

/**
 * 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')

/**
 * Co-play mode for observer participation during practice game breaks.
 *
 * - 'drop-in': Observer can join mid-game (e.g., KYW, Memory Quiz)
 * - 'join-at-start': Observer must be included before session creation (e.g., Matching)
 * - 'none': Single-player only, observer can only spectate
 */
export const CoPlayConfigSchema = z
  .object({
    mode: z.enum(['drop-in', 'join-at-start', 'none']),
    /** Maximum players including observers (defaults to manifest maxPlayers) */
    maxPlayers: z.number().int().min(1).max(10).optional(),
  })
  .describe('Observer co-play configuration for practice game breaks')

/**
 * Schema for game manifest (game.yaml)
 */
export const GameManifestSchema = z.object({
  name: z.string().min(1).describe('Internal game identifier (e.g., "matching")'),
  displayName: z.string().min(1).describe('Display name shown to users'),
  shortName: z
    .string()
    .optional()
    .describe('Short name for compact UI spaces (defaults to displayName)'),
  icon: z.string().min(1).describe('Emoji icon for the game'),
  description: z.string().min(1).describe('Short description'),
  longDescription: z.string().min(1).describe('Detailed description'),
  maxPlayers: z.number().int().min(1).max(10).describe('Maximum number of players'),
  difficulty: z
    .enum(['Beginner', 'Intermediate', 'Advanced', 'Expert'])
    .describe('Difficulty level'),
  chips: z.array(z.string()).describe('Feature chips displayed on game card'),
  color: z.string().min(1).describe('Color theme (e.g., "purple")'),
  gradient: z.string().min(1).describe('CSS gradient for card background'),
  borderColor: z.string().min(1).describe('Border color (e.g., "purple.200")'),
  available: z.boolean().describe('Whether game is available to play'),
  practiceBreakReady: z
    .boolean()
    .optional()
    .default(false)
    .describe(
      'Whether game is ready for practice session game breaks. ' +
        'Games must be single-player capable, work in 2-10 minute sessions, ' +
        'and not require complex setup or multiplayer coordination.'
    ),
  practiceBreakConfig: PracticeBreakConfigSchema.optional().describe(
    '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.'
  ),
  coPlay: CoPlayConfigSchema.optional().describe(
    'Observer co-play configuration. Declares whether observers can join as participants during practice game breaks.'
  ),
})

/**
 * Inferred TypeScript types from schemas
 */
export type PracticeBreakConfig = z.infer<typeof PracticeBreakConfigSchema>
export type GameResultsConfig = z.infer<typeof GameResultsConfigSchema>
export type CoPlayConfig = z.infer<typeof CoPlayConfigSchema>
export type GameManifest = z.infer<typeof GameManifestSchema>

/**
 * Validate a parsed manifest object
 */
export function validateManifest(data: unknown): GameManifest {
  return GameManifestSchema.parse(data)
}