Please hold on for a moment while the content loads.
Please hold on for a moment while the content loads.
AI-Powered Gamified Learning Platform
Gamified micro-learning platform where students learn technical topics by playing interactive games. AI generates structured JSON content per engine the app never receives JSX, HTML, or React from the model. 19 game engines ship as frozen React templates, all driven by a shared Zod schema contract.
A gamified micro-learning platform where students learn technical topics (web dev, DSA, programming) by playing interactive games. The architecture separates game engines (fixed React templates) from game content (AI-generated JSON). A shared schemas package consumed by both the Express backend and React frontend validates every piece of content at both seed time and render time, eliminating the "wrong-shape JSON to broken game" failure mode. 19 engines ship through an extensible 6-seam registry; adding a new engine touches exactly 6 files, never shared code.
Designed the 6-seam engine registry, universal GameEnvelope, and shared Zod schema package consumed by both backend and frontend for validation at seed and render time.
Built the multi-provider AI content pipeline with validated-retry loop, supporting Anthropic, OpenAI, Gemini, and OpenRouter through a common provider interface.
Implemented server-side score derivation: clients submit raw results only, the server re-validates stored content and derives correctness, XP, badges, and leaderboard updates.
Built the Express API, React frontend with 19 game engine components, MongoDB storage, Redis leaderboard, and Docker/Turborepo monorepo configuration.
Four-layer architecture: frontend React engine templates, shared Zod schemas, Express API with AI pipeline, and MongoDB/Redis storage
End-to-end play sequence: topic selection to AI generation to gameplay to scoring to leaderboard update
Layout Match, Code Assembler, Bug Hunter, Memory Match, Match Connect, Sequence Builder, Sort Categorize, Sequence Sort, Swap Pairs, Fill the Blank, Jigsaw Assembly, Path Connect, Treasure Hunt, Maze Explorer, Hidden Object Challenge, Pit Stop Order, Circuit Wire, Banana Split Race, Lane Racer. Each is a frozen React template driven entirely by JSON adding content means inserting a new JSON document, never touching engine code.
A discriminated union wrapper (GameEnvelope) lifts cross-cutting concerns out of individual engines: schema versioning, meta/title, multi-level support, rewards (XP/coins/achievements), timers, assets, audio, accessibility (reduced motion, high contrast, captions), localization, and an extensible ext bag for future signals all shared, all optional, all forward-compatible.
Supports Anthropic Claude, OpenAI, Gemini, and OpenRouter through a common provider interface. The validated-retry loop: model outputs inner content for one engine, Zod validates it against the engine's schema, and on failure the system re-prompts with exact validation errors until the shape is correct. Offline authored fixtures support local seeding, but the current personalized play route requires an AI provider at runtime.
Clients submit raw results only never a score. The server re-validates the stored content against the engine's Zod schema, re-derives correctness from the raw result, and returns a server-computed score. No client-provided score field is ever trusted. XP, badges, and leaderboard updates all go through the same server-side validation.
XP and levels are awarded from server-computed game results. Milestone badges unlock on first win and topic completion. Global leaderboard is backed by Redis sorted sets (ZSET) for fast rank queries, with MongoDB fallback when Redis is unavailable.
A category taxonomy sits above engines: puzzle, action, card, memory, logic. User preferences pick categories; the selectEngine function maps those to a concrete engine. New engines join an existing category with zero UI change. Onboarding collects preferences for personalized first-play experiences.
AI-generated UI (JSX, React components from the model) is unreliable, hard to validate, and impossible to guarantee correctness for. Every attempt produces different markup.
Each engine is a frozen React template with a typed GameProps interface. The AI emits only structured JSON content matching a per-engine Zod schema. Adding a new engine means writing a React component and a Zod schema the AI never generates UI. The system is data-driven, not model-driven.
// Frozen engine contract every engine is exactly this:
interface GameProps<T> {
content: T // typed per-engine content
onComplete: (result: unknown) => void // raw result, never score
}
// Registry: engine → Zod schema. Single source of truth.
const ENGINE_SCHEMAS = {
"layout-match": LayoutMatchSchema,
"code-assembler": CodeAssemblerSchema,
"memory-match": MemoryMatchSchema,
"match-connect": MatchConnectSchema,
// ... 15 more engines
} as const;
// AI emits inner content only server wraps it:
const inner = await generateContent(prompt)
const validated = ENGINE_SCHEMAS[engine].parse(inner)
const envelope = makeEnvelope(engine, validated)Cross-cutting features (XP, timers, localization, accessibility, rewards, audio) were initially handled inside individual engines, leading to duplicated code and inconsistent behavior across the platform.
A universal GameEnvelope wraps every game. Cross-cutting fields are defined once: rewards, timer, assets, audio, accessibility, localization. Each is optional and forward-compatible. Engines remain focused on their specific mechanic; adding a new envelope feature benefits all 19 engines simultaneously.
// Universal envelope all but 3 fields optional:
interface GameEnvelope {
schemaVersion: string // "2.0" gates migrations
engine: Engine // discriminator
content: EngineContent // per-engine AI payload
// Cross-cutting (shared, optional, forward-compatible):
meta?: { title?; prompt?; objective?; instructions? }
levels?: Level[]
rewards?: { xp?; coins?; achievements?: string[] }
timer?: { seconds: number }
assets?: { id: string; kind: string; src: string }[]
audio?: { music?; sfx?: Record<string, string> }
accessibility?: { reducedMotion?; highContrast?; captions? }
localization?: { language: string; strings?: ... }
ext?: Record<string, unknown> // experimental features
}
// AI never emits the envelope it emits inner content only.
// Server wraps it deterministically:
const envelope = makeEnvelope(engine, content, { rewards: { xp: 10 } })AI-generated content with wrong shape breaks games silently. Validating only at seed time misses runtime corruption. Validating only at render time catches problems too late for graceful handling.
The same Zod schema package is imported by both the backend seed script and the frontend renderer. Before insert into MongoDB, the seed script validates AI output and rejects/retries on failure. Before render, the frontend validates content and shows an error state instead of a broken game. Single source of truth for validation logic.
// Shared schemas package consumed by both apps:
// packages/schemas/src/engine.ts
export const ENGINE_SCHEMAS = {
"code-assembler": CodeAssemblerSchema,
// ...
} as const
// Backend seed script validate before insert:
import { ENGINE_SCHEMAS } from "@eduplay/schemas"
try {
const content = ENGINE_SCHEMAS["code-assembler"].parse(aiOutput)
await db.collection("gameContents").insertOne({ engine, content })
} catch (err) {
// ZodError with exact field-level issues
await retryGenerate(prompt, err.issues) // re-prompt with errors
}
// Frontend renderer validate before render:
import { ENGINE_SCHEMAS } from "@eduplay/schemas"
try {
const content = ENGINE_SCHEMAS[engine].parse(raw.content)
return <CodeAssembler content={content} onComplete={...} />
} catch {
return <ErrorState message="Content validation failed" />
}Trusting client-provided scores enables cheating. Computing scores on the client is unreliable (network issues, bugs) and non-authoritative. But re-validating game content on every score request adds complexity.
Clients submit only a raw result (e.g., the order the player assembled). The server re-validates the stored GameSession envelope, derives correctness from the raw result against the engine's Zod schema, and returns a server-computed ScoreOutcome. XP, badge grants, and leaderboard updates all flow through this single authoritative path. No client-provided score field is ever read.
// Client submits raw result only never a score:
fetch("/api/score", {
method: "POST",
body: JSON.stringify({
playId: "abc123",
result: ["l3", "l1", "l2"] // player's line order
})
})
// Server re-derives everything:
export function scoreResult(
engine: Engine,
content: unknown, // re-validated from stored GameSession
result: unknown, // raw client result
): ScoreOutcome {
const validated = ENGINE_SCHEMAS[engine].parse(content)
const correct = deriveCorrectness(engine, validated, result)
return {
valid: true,
correct,
score: correct ? computeXp(engine, validated) : 0,
userAnswer: formatResult(engine, result),
correctAnswer: formatCorrectAnswer(engine, validated),
}
}Cross-cutting features (rewards, timers, localization, accessibility) benefit all 19 engines without duplication. Adding a new envelope feature requires zero engine changes.
Every game includes optional fields most don't use. The envelope adds indirection: content is nested inside, not flat. Schema evolution requires versioning on the envelope level.
Zero runtime dependency on LLM availability. Content is deterministic and testable. Faster play-start since no generation latency. All content verified at seed time.
Content is static until re-seeded. Cannot personalize per player in real-time. Requires a seed script run as a deployment step. New topics need re-seeding before playable.
Predictable rendering, testable components, deterministic gameplay. Zod schemas have a known shape to validate against. Six-seam checklist makes adding new engines mechanical.
Game mechanic is limited to what templates support. Adding a new engine requires writing a React component plus a Zod schema. AI-generated UI could theoretically support more diverse interactions.
Wrong-shape AI JSON
Game content fails validation. No game can be generated for the subtopic.
Per-engine Zod schemas catch shape mismatches at seed time. Validated-retry loop re-prompts the model with exact ZodError issues until the content fits the schema. If retries exhaust, the seed script logs the failure and skips that subtopic no broken content enters the database.
LLM unavailable at runtime
Personalized play start fails because the current route generates five questions through the configured provider.
The repository includes an AI-free authored fixture seed for local development and demos. A production fallback from /api/play/start to published fixture content is not currently implemented.
Score cheating via modified client
Users submit inflated scores to manipulate leaderboard rankings.
Server-authoritative scoring: the server re-validates stored game content, re-derives correctness from the raw result, and computes the score. No client-provided score field is ever read. XP, badges, and leaderboard updates all flow through this single server path.
Engine/content coupling drift
A schema change in one engine breaks content validation for existing seeded games.
GameEnvelope.schemaVersion gates migrations. Per-engine Zod schemas are versioned independently. The seed script re-validates all existing content on version bumps. The ext bag in the envelope absorbs experimental fields without schema version bumps.
Shared Zod schemas between backend and frontend eliminated an entire class of bugs. If content validated at seed time, it renders correctly at game time. No more 'works in seed, breaks in render' issues.
Mapping engine strings to schemas, components, prompts, and scoring functions through registries (not if/else chains) makes adding a new engine a mechanical 6-seam checklist instead of a risky cross-file refactor.
AI models produce wrong-shaped JSON regularly. Passing Zod validation errors back as a re-prompt signal turns an unreliable model output into a reliable data pipeline. The system never stores unvalidated content.
Three distinct stores with different lifecycles: searchable metadata (cheap queries), heavy content (lazy-loaded), per-user progress (write-heavy). Mixing them makes lists slow, definitions hard to cache, and progress tracking convoluted.
This is a public open-source hackathon MVP. The details above reflect actual development work: