The Campaign System is CodeJam’s structured learning pathway. Progress through tiers (HTML, CSS, JavaScript, Python, C++), unlock challenges, and defeat bosses to advance your coding journey.
The campaign replaces traditional linear tutorials with a game-like progression system that adapts to your skill level and learning pace.
Campaign Architecture
The campaign is organized into tiers containing nodes . Each node represents a learning module, challenge, or boss battle.
Briefing Nodes Educational content and interactive guides teaching core concepts.
Challenge Nodes Timed coding challenges testing your skills in game modes.
Boss Nodes Epic debugging scenarios in Linux sandboxes. Requires boss keys.
Schema: Campaign Nodes
// From convex/schema.ts:110-123
campaign_nodes : defineTable ({
slug: v . string (), // e.g. "node-1-html-basics"
title: v . string (),
type: v . union (
v . literal ( "briefing" ),
v . literal ( "challenge" ),
v . literal ( "boss" )
),
tier: v . string (), // "html", "css", "js", "python", "cpp", "tailwindcss"
requires: v . optional ( v . array ( v . string ())), // slugs of parent nodes
position: v . object ({ x: v . number (), y: v . number () }), // Visual layout
data: v . object ({
description: v . optional ( v . string ()),
guideId: v . optional ( v . string ()), // For briefing nodes
gameId: v . optional ( v . string ()), // For challenge/boss nodes
bossScenario: v . optional ( v . string ()), // For boss nodes
}),
})
Node Type Details
Purpose: Teach concepts through interactive guidesData Fields:
guideId - Links to a guide in the guides table
description - Brief overview of the learning topic
Example: {
slug : "node-1-html-basics" ,
title : "HTML: The Skeleton" ,
type : "briefing" ,
tier : "html" ,
data : {
description : "Learn the structure of the web." ,
guideId : "html-basics"
}
}
Purpose: Test skills through game modesData Fields:
gameId - Links to a game definition (e.g., “function-fury”)
description - Challenge objective
Example: {
slug : "node-2-function-fury" ,
title : "Function Fury" ,
type : "challenge" ,
tier : "js" ,
requires : [ "node-1-html-basics" ],
data : {
description : "Debug the faulty function." ,
gameId : "function-fury"
}
}
Purpose: Epic debugging scenarios in sandboxesData Fields:
bossScenario - Scenario type (e.g., “memory-leak”)
gameId - Optional game mode for the boss
description - Boss challenge description
Example: {
slug : "node-4-boss-html" ,
title : "BOSS: DOM Destroyer" ,
type : "boss" ,
tier : "html" ,
requires : [ "node-3-css-intro" ],
data : {
description : "Fix the memory leak in the layout engine." ,
bossScenario : "memory-leak"
}
}
Schema: User Progress
Each user’s campaign progress is tracked:
// From convex/schema.ts:125-131
user_campaign_progress : defineTable ({
userId: v . id ( "users" ),
unlockedNodes: v . array ( v . string ()), // slugs
completedNodes: v . array ( v . string ()), // slugs
currentTier: v . string (),
bossKeys: v . array ( v . string ()), // keys earned from challenges
}). index ( "by_user" , [ "userId" ])
Progress Fields
unlockedNodes - Array of node slugs the user can access
completedNodes - Array of node slugs the user has finished
currentTier - Active learning track (“html”, “python”, etc.)
bossKeys - Keys earned from completing challenges, required for boss battles
Getting Campaign Data
Fetch All Nodes
// From convex/campaign.ts:12-18
export const getCampaignTree = query ({
args: {},
handler : async ( ctx ) => {
return await ctx . db . query ( "campaign_nodes" ). collect ();
},
});
Fetch User Progress
// From convex/campaign.ts:20-43
export const getUserProgress = query ({
args: {},
handler : async ( ctx ) => {
const userId = await getAuthUserId ( ctx );
if ( ! userId ) return null ;
const progress = await ctx . db
. query ( "user_campaign_progress" )
. withIndex ( "by_user" , ( q ) => q . eq ( "userId" , userId ))
. unique ();
if ( ! progress ) {
// Return default starter state
return {
unlockedNodes: [ "node-1-html-basics" ],
completedNodes: [],
currentTier: "html" ,
bossKeys: [],
};
}
return progress ;
},
});
New users automatically start with "node-1-html-basics" unlocked. This is the entry point for the HTML tier.
Selecting a Learning Track
Users can switch between language tiers:
// From convex/campaign.ts:45-99
export const setTrack = mutation ({
args: { trackId: v . string () }, // "html", "python", "cpp", etc.
handler : async ( ctx , args ) => {
const userId = await getAuthUserId ( ctx );
if ( ! userId ) throw new Error ( "Unauthorized" );
let progress = await ctx . db
. query ( "user_campaign_progress" )
. withIndex ( "by_user" , q => q . eq ( "userId" , userId ))
. unique ();
const startNodes : Record < string , string > = {
"html" : "node-1-html-basics" ,
"python" : "node-1-python-basics" ,
"cpp" : "node-1-cpp-basics" ,
"js" : "node-5-js-logic" ,
"tailwindcss" : "node-1-tailwind-basics"
};
const newStartNode = startNodes [ args . trackId ];
if ( ! newStartNode ) throw new Error ( "Invalid track" );
// Sync preferredLanguage for better UX
const langMap : Record < string , string > = {
"html" : "HTML" ,
"python" : "Python" ,
"cpp" : "C++" ,
"js" : "JavaScript" ,
"tailwindcss" : "CSS"
};
if ( langMap [ args . trackId ]) {
await ctx . db . patch ( userId , {
preferredLanguage: langMap [ args . trackId ]
});
}
if ( ! progress ) {
await ctx . db . insert ( "user_campaign_progress" , {
userId ,
unlockedNodes: [ newStartNode ],
completedNodes: [],
currentTier: args . trackId ,
bossKeys: []
});
} else {
// Add to unlocked nodes
const newUnlocked = [ ... progress . unlockedNodes ];
if ( ! newUnlocked . includes ( newStartNode )) {
newUnlocked . push ( newStartNode );
}
await ctx . db . patch ( progress . _id , {
unlockedNodes: newUnlocked ,
currentTier: args . trackId
});
}
}
});
Available Tiers
HTML Start here. Learn DOM structure and semantic markup.
Python Data-focused track with logic and algorithms.
JavaScript Advanced tier unlocked after HTML boss.
C++ Expert-level low-level programming.
Tailwind CSS Utility-first styling framework.
Completing Nodes
When a user completes a node (finishes a guide or wins a challenge):
// From convex/campaign.ts:101-141
export const internalCompleteNode = internalMutation ({
args: { nodeSlug: v . string (), userId: v . id ( "users" ) },
handler : async ( ctx , args ) => {
let progress = await ctx . db
. query ( "user_campaign_progress" )
. withIndex ( "by_user" , q => q . eq ( "userId" , args . userId ))
. unique ();
if ( ! progress ) return ;
if ( progress . completedNodes . includes ( args . nodeSlug )) {
return ; // Already completed
}
// Add to completed
const newCompleted = [ ... progress . completedNodes , args . nodeSlug ];
// Unlock next nodes
const allNodes = await ctx . db . query ( "campaign_nodes" ). collect ();
const newlyUnlocked : string [] = [];
for ( const node of allNodes ) {
if ( node . requires && node . requires . includes ( args . nodeSlug )) {
const allMet = node . requires . every ( req =>
newCompleted . includes ( req )
);
if ( allMet && ! progress . unlockedNodes . includes ( node . slug )) {
newlyUnlocked . push ( node . slug );
}
}
}
await ctx . db . patch ( progress . _id , {
completedNodes: newCompleted ,
unlockedNodes: [ ... progress . unlockedNodes , ... newlyUnlocked ]
});
}
});
Unlocking Logic
Node Completed
User finishes a briefing, challenge, or boss node.
Add to Completed
The node slug is added to completedNodes array.
Check Dependencies
System scans all nodes with requires field containing the completed node.
Validate Prerequisites
For each dependent node, verify ALL required nodes are completed.
Unlock New Nodes
Nodes with satisfied dependencies are added to unlockedNodes.
This dependency system allows complex node graphs, not just linear progression. You could have nodes requiring multiple prerequisites from different tiers.
Campaign Seed Data
The campaign is initialized with seed data:
// From convex/campaign.ts:153-284 (excerpt)
export const seedCampaign = mutation ({
args: {},
handler : async ( ctx ) => {
const nodes = [
// HTML Track
{
slug: "node-1-html-basics" ,
title: "HTML: The Skeleton" ,
type: "briefing" ,
tier: "html" ,
position: { x: 100 , y: 300 },
data: {
description: "Learn the structure of the web." ,
guideId: "html-basics"
}
},
{
slug: "node-2-function-fury" ,
title: "Function Fury" ,
type: "challenge" ,
tier: "js" ,
requires: [ "node-1-html-basics" ],
position: { x: 300 , y: 300 },
data: {
description: "Debug the faulty function." ,
gameId: "function-fury"
}
},
{
slug: "node-4-boss-html" ,
title: "BOSS: DOM Destroyer" ,
type: "boss" ,
tier: "html" ,
requires: [ "node-3-css-intro" ],
position: { x: 700 , y: 300 },
data: {
description: "Fix the memory leak." ,
bossScenario: "memory-leak"
}
},
// Additional tiers...
];
for ( const node of nodes ) {
await ctx . db . insert ( "campaign_nodes" , node );
}
}
});
The seed mutation deletes existing nodes before inserting new ones. This is for development only - production systems should use upsert logic.
Visual Roadmap
The campaign system includes position data for visual rendering:
position : { x : v . number (), y : v . number () }
This enables a 2D roadmap UI:
[HTML Basics] --> [Function Fury] --> [CSS Intro] --> [BOSS: DOM Destroyer] --> [JS Logic]
|
v
[Python Basics] --> [Logic Labyrinth]
|
v
[C++ Basics]
Position coordinates can be used to render an interactive node graph using libraries like React Flow or D3.js.
Boss Keys
Boss nodes require boss keys earned from completing challenges:
bossKeys : v . array ( v . string ())
When a user completes a challenge node, they earn a boss key. Boss nodes check if the user has the required key before allowing entry.
Boss Key System Earned From: Challenge nodes
Used For: Unlocking boss battles
Tracked In: user_campaign_progress.bossKeysExample: Completing 3 challenges in the HTML tier earns an “HTML Boss Key” required to enter the DOM Destroyer boss battle.
Integration with Guides
Briefing nodes link to the Learning Paths system:
// From convex/guides.ts:88-103
// When a guide is completed, check if it's part of a campaign node
const guide = await ctx . db . get ( args . guideId );
if ( guide ) {
const allNodes = await ctx . db . query ( "campaign_nodes" ). collect ();
const node = allNodes . find ( n => n . data ?. guideId === guide . slug );
if ( node ) {
await ctx . runMutation ( internal . campaign . internalCompleteNode , {
userId ,
nodeSlug: node . slug
});
}
}
Completing a guide automatically marks the corresponding campaign node as completed, unlocking the next nodes in the chain.
Dashboard Integration
The campaign system powers the main dashboard view:
Roadmap Widget - Visual node graph showing locked/unlocked/completed states
Current Tier Display - Shows active learning track
Progress Stats - Percentage completion per tier
Next Challenge - Suggests the next unlocked node
Example Campaign Flow
User Starts Campaign
New user automatically has "node-1-html-basics" unlocked.
Complete HTML Basics
User finishes the HTML guide. Node marked complete, unlocks "node-2-function-fury".
Win Function Fury
User completes the challenge. Earns a boss key, unlocks "node-3-css-intro".
Complete CSS Intro
User finishes CSS guide. Unlocks "node-4-boss-html".
Defeat DOM Destroyer
User beats the boss battle. Unlocks JavaScript tier: "node-5-js-logic".
Learning Paths Explore the guide system powering briefing nodes
Boss Battles Learn about boss node implementation
Game Modes See all challenge types available in the campaign