Skip to main content

Overview

The level system provides a framework for creating interactive git challenges. Each level defines a starting state, goal state, and solution validation logic. Location: src/js/level/

Architecture

Level Flow

Level Start

Load Start Tree

Show Goal Visualization

User Commands

Validate After Each Command

Tree Comparison

Level Complete / Continue

Level Class

Location: src/js/level/index.js

Structure

class Level extends Sandbox {
  initialize(options) {
    options = options || {};
    this.level = options.level || {};
    
    this.gitCommandsIssued = [];
    this.solved = false;
    this.wasResetAfterSolved = false;
    
    this.initGoalData(options);
    this.initName(options);
    this.isGoalExpanded = false;
    
    // Call super.initialize() - creates mainVis
    super.initialize(options);
    
    // Bind events
    this.on('minimizeCanvas', this.minimizeGoal, this);
    this.on('resizeCanvas', this.resizeGoal, this);
    
    this.startOffCommand();
    this.handleOpen(options.deferred);
    
    LevelActions.setIsSolvingLevel(true);
  }
}

Key Properties

PropertyTypeDescription
levelObjectLevel definition with goals and solutions
mainVisVisualizationUser’s working visualization
goalVisVisualizationGoal state visualization
goalCanvasHolderCanvasTerminalHolderContainer for goal visualization
gitCommandsIssuedArrayCommands executed (for scoring)
solvedBooleanWhether level is complete
wasResetAfterSolvedBooleanTrack resets after solving

Level Definition

Basic Structure

var level = {
  // Metadata
  id: 'intro1',
  name: {
    'en_US': 'Introduction to Git Commits',
    'es_AR': 'Introducción a los Commits de Git'
  },
  
  // Starting state
  startTree: {
    branches: {
      main: {id: 'main', target: 'C1', type: 'branch'}
    },
    commits: {
      C0: {id: 'C0', parents: [], rootCommit: true},
      C1: {id: 'C1', parents: ['C0']}
    },
    HEAD: {id: 'HEAD', target: 'main', type: 'general ref'}
  },
  
  // Goal state
  goalTreeString: '%7B%22branches%22%3A%7B%22main%22%3A%7B...%7D%7D',
  
  // Solution
  solutionCommand: 'git commit; git commit',
  
  // Dialog shown at start
  startDialog: {
    childViews: [
      {
        type: 'ModalAlert',
        options: {
          markdowns: [
            '## Git Commits',
            'A commit in a git repository records a snapshot...'
          ]
        }
      }
    ]
  },
  
  // Hint
  hint: {
    'en_US': 'Just type in \'git commit\' twice!',
    'es_AR': '¡Sólo tipea \'git commit\' dos veces!'
  },
  
  // Disabled commands (optional)
  disabledMap: {
    'git rebase': true
  }
};

Tree String Format

Goal trees are stored as URL-encoded JSON:
// Original tree object
var goalTree = {
  branches: {main: {id: 'main', target: 'C3'}},
  commits: {
    C0: {id: 'C0', parents: []},
    C1: {id: 'C1', parents: ['C0']},
    C2: {id: 'C2', parents: ['C1']},
    C3: {id: 'C3', parents: ['C2']}
  },
  HEAD: {id: 'HEAD', target: 'main'}
};

// Convert to tree string
var treeString = gitEngine.printTree(goalTree);
// Result: URL-encoded JSON string

Initialization

initGoalData(options)

Validates level has required fields:
initGoalData(options) {
  if (!this.level.goalTreeString || !this.level.solutionCommand) {
    throw new Error('need goal tree and solution');
  }
}

initName(options)

Sets up level toolbar:
initName() {
  var name = intl.getName(this.level);
  this.levelToolbar = React.createElement(LevelToolbarView, {
    name: name,
    onGoalClick: this.toggleGoal.bind(this),
    onObjectiveClick: this.toggleObjective.bind(this),
    parent: this
  });
  
  ReactDOM.render(
    this.levelToolbar,
    document.getElementById('levelToolbarMount')
  );
}

initVisualization(options)

Creates main working visualization:
initVisualization(options) {
  this.mainVis = new Visualization({
    el: options.el || this.getDefaultVisEl(),
    treeString: options.level.startTree
  });
}

initGoalVisualization()

Creates goal visualization:
initGoalVisualization() {
  var onlyMain = TreeCompare.onlyMainCompared(this.level);
  
  this.goalCanvasHolder = new CanvasTerminalHolder({
    text: (onlyMain) ? intl.str('goal-only-main') : undefined,
    parent: this
  });
  
  this.goalVis = new Visualization({
    el: this.goalCanvasHolder.getCanvasLocation(),
    containerElement: this.goalCanvasHolder.getCanvasLocation(),
    treeString: this.level.goalTreeString,
    noKeyboardInput: true,
    smallCanvas: true,
    isGoalVis: true,
    levelBlob: this.level,
    noClick: true
  });
  
  // Handle goal window dragging
  this.goalDragHandler = function(event, ui) {
    if (Math.abs(ui.position.left) < 0.4 * $(window).width()) {
      if (!$('#goalPlaceholder').is(':visible')) {
        $('#goalPlaceholder').show();
        this.mainVis.myResize();
      }
    } else {
      if ($('#goalPlaceholder').is(':visible')) {
        $('#goalPlaceholder').hide();
        this.mainVis.myResize();
      }
    }
  }.bind(this);
  
  this.goalVis.customEvents.on('drag', this.goalDragHandler);
  
  return this.goalCanvasHolder;
}

Goal Visualization Control

showGoal(command, defer)

showGoal(command, defer) {
  this.isGoalExpanded = true;
  this.trigger('goalToggled');
  
  this.showSideVis(command, defer, this.goalCanvasHolder, 
    this.initGoalVisualization);
  
  // Show placeholder if on right side
  if ($(this.goalVis.el).offset().left > 0.5 * $(window).width()) {
    $('#goalPlaceholder').show();
    this.mainVis.myResize();
  }
}

hideGoal(command, defer)

hideGoal(command, defer) {
  this.isGoalExpanded = false;
  this.trigger('goalToggled');
  this.hideSideVis(command, defer, this.goalCanvasHolder);
}

minimizeGoal(position, size)

minimizeGoal(position, size) {
  this.isGoalExpanded = false;
  this.trigger('goalToggled');
  this.goalVis.hide();
  this.goalWindowPos = position;
  this.goalWindowSize = size;
  
  if ($('#goalPlaceholder').is(':visible')) {
    $('#goalPlaceholder').hide();
    this.mainVis.myResize();
  }
}

Command Tracking

beforeCommandCB(command)

Called before command execution:
beforeCommandCB(command) {
  // Save state for undo
  this._treeBeforeCommand = this.mainVis.gitEngine.printTree();
}

afterCommandCB(command)

Called after command execution:
afterCommandCB(command) {
  if (this.doesCommandCountTowardsTotal(command)) {
    this.gitCommandsIssued.push(command.get('rawStr'));
    this.undoStack.push(this._treeBeforeCommand);
  }
}

doesCommandCountTowardsTotal(command)

Determines if command counts for scoring:
doesCommandCountTowardsTotal(command) {
  if (command.get('error')) {
    return false;
  }
  
  var matched = false;
  var commandsThatCount = Commands.commands.getCommandsThatCount();
  
  Object.values(commandsThatCount).forEach(function(map) {
    Object.values(map).forEach(function(regex) {
      matched = matched || regex.test(command.get('rawStr'));
    });
  });
  
  return matched;
}

Solution Validation

afterCommandDefer(defer, command)

Validates solution after each command:
afterCommandDefer(defer, command) {
  if (this.solved) {
    command.addWarning(intl.str('already-solved'));
    defer.resolve();
    return;
  }
  
  var current = this.mainVis.gitEngine.printTree();
  var solved = TreeCompare.dispatchFromLevel(this.level, current);
  
  if (!solved) {
    defer.resolve();
    return;
  }
  
  // Level solved!
  this.levelSolved(defer);
}

levelSolved(defer)

Handles level completion:
levelSolved(defer) {
  this.solved = true;
  
  if (!this.isShowingSolution) {
    var numCommands = this.gitCommandsIssued.length;
    var best = this.getNumSolutionCommands();
    var isBest = numCommands <= best;
    
    LevelActions.setLevelSolved(this.level.id, isBest);
    log.levelSolved(this.getEnglishName());
  }
  
  this.hideGoal();
  
  var nextLevel = LevelStore.getNextLevel(this.level.id);
  var numCommands = this.gitCommandsIssued.length;
  var best = this.getNumSolutionCommands();
  
  var skipFinishDialog = this.testOption('noFinishDialog') || 
    this.wasResetAfterSolved;
  var skipFinishAnimation = this.wasResetAfterSolved;
  
  if (!skipFinishAnimation) {
    GlobalStateActions.levelSolved();
  }
  
  // Speed up animation on repeated solves
  var speed = 1.0;
  switch (GlobalStateStore.getNumLevelsSolved()) {
    case 2: speed = 1.5; break;
    case 3: speed = 1.8; break;
    case 4: speed = 2.1; break;
    case 5: speed = 2.4; break;
  }
  if (GlobalStateStore.getNumLevelsSolved() > 5) {
    speed = 2.5;
  }
  
  // Play finish animation
  var finishAnimationChain = null;
  if (skipFinishAnimation) {
    finishAnimationChain = Promise.resolve();
  } else {
    GlobalStateActions.changeIsAnimating(true);
    finishAnimationChain = this.mainVis.gitVisuals.finishAnimation(speed);
    
    if (this.mainVis.originVis) {
      finishAnimationChain = finishAnimationChain.then(
        this.mainVis.originVis.gitVisuals.finishAnimation(speed)
      );
    }
  }
  
  // Show completion dialog
  if (!skipFinishDialog) {
    finishAnimationChain = finishAnimationChain.then(function() {
      var nextDialog = new NextLevelConfirm({
        nextLevel: nextLevel,
        numCommands: numCommands,
        best: best
      });
      return nextDialog.getPromise();
    });
  }
  
  finishAnimationChain
    .then(function() {
      if (!skipFinishDialog && nextLevel) {
        log.choseNextLevel(nextLevel.id);
        Main.getEventBaton().trigger(
          'commandSubmitted',
          'level ' + nextLevel.id
        );
      }
    })
    .catch(function() {
      // User declined to continue
    })
    .then(function() {
      GlobalStateActions.changeIsAnimating(false);
      defer.resolve();
    });
}

Tree Comparison

Location: src/js/graph/treeCompare.js

dispatchFromLevel(level, currentTree)

Validates current tree matches goal:
TreeCompare.dispatchFromLevel = function(level, currentTree) {
  var goalTree = JSON.parse(unescape(level.goalTreeString));
  
  // Reduce trees to comparable form
  TreeCompare.reduceTreeFields([goalTree, currentTree]);
  
  // Compare branch positions
  if (!TreeCompare.compareBranches(goalTree, currentTree)) {
    return false;
  }
  
  // Compare commit structure
  if (!TreeCompare.compareCommitStructure(goalTree, currentTree)) {
    return false;
  }
  
  // Compare tags if present
  if (goalTree.tags && !TreeCompare.compareTags(goalTree, currentTree)) {
    return false;
  }
  
  return true;
};

onlyMainCompared(level)

Checks if only main branch is validated:
TreeCompare.onlyMainCompared = function(level) {
  var goalTree = JSON.parse(unescape(level.goalTreeString));
  var branchNames = Object.keys(goalTree.branches);
  
  // Filter out remote branches
  branchNames = branchNames.filter(function(name) {
    return name.indexOf('o/') !== 0;
  });
  
  return branchNames.length === 1 && branchNames[0] === 'main';
};

compareCommitStructure(tree1, tree2)

Compares commit graph structure:
TreeCompare.compareCommitStructure = function(tree1, tree2) {
  var commits1 = tree1.commits;
  var commits2 = tree2.commits;
  
  // Check commit count
  if (Object.keys(commits1).length !== Object.keys(commits2).length) {
    return false;
  }
  
  // Check each commit's parents
  for (var id in commits1) {
    if (!commits2[id]) return false;
    
    var parents1 = commits1[id].parents || [];
    var parents2 = commits2[id].parents || [];
    
    if (parents1.length !== parents2.length) return false;
    
    for (var i = 0; i < parents1.length; i++) {
      if (parents1[i] !== parents2[i]) return false;
    }
  }
  
  return true;
};

Solution Display

showSolution(command, deferred)

Displays solution animation:
showSolution(command, deferred) {
  var toIssue = this.level.solutionCommand;
  
  var issueFunc = function() {
    this.isShowingSolution = true;
    this.wasResetAfterSolved = true;
    
    Main.getEventBaton().trigger('commandSubmitted', toIssue);
    log.showLevelSolution(this.getEnglishName());
  }.bind(this);
  
  var commandStr = command.get('rawStr');
  if (!this.testOptionOnString(commandStr, 'noReset')) {
    toIssue = 'reset --forSolution; ' + toIssue;
  }
  
  if (this.testOptionOnString(commandStr, 'force')) {
    issueFunc();
    command.finishWith(deferred);
    return;
  }
  
  // Confirm if level not already solved
  if (this.level.id && !LevelStore.isLevelSolved(this.level.id)) {
    var confirmDefer = createDeferred();
    var dialog = intl.getDialog(require('../dialogs/confirmShowSolution'))[0];
    var confirmView = new ConfirmCancelTerminal({
      markdowns: dialog.options.markdowns,
      deferred: confirmDefer
    });
    
    confirmDefer.promise
      .then(issueFunc)
      .catch(function() {
        command.setResult("");
      })
      .then(function() {
        setTimeout(function() {
          command.finishWith(deferred);
        }, confirmView.getAnimationTime());
      });
  } else {
    issueFunc();
    command.finishWith(deferred);
  }
}

Level Commands

Available Commands

var regexMap = {
  'help level': /^help level$/,
  'start dialog': /^start dialog$/,
  'show goal': /^(show goal|goal|help goal)$/,
  'hide goal': /^hide goal$/,
  'show solution': /^show solution($|\s)/,
  'objective': /^(objective|assignment)$/
};

getInstantCommands()

Provides level-specific instant commands:
getInstantCommands() {
  var getHint = function() {
    var hint = intl.getHint(this.level);
    if (!hint || !hint.length) {
      return intl.str('no-hint');
    }
    return hint;
  }.bind(this);
  
  return [
    [/^help$|^\?$/, function() {
      throw new Errors.CommandResult({
        msg: intl.str('help-vague-level')
      });
    }],
    [/^hint$/, function() {
      throw new Errors.CommandResult({
        msg: getHint()
      });
    }]
  ];
}

Disabled Commands

Levels can disable specific commands:
initParseWaterfall(options) {
  super.initParseWaterfall(options);
  
  if (options.level.disabledMap) {
    this.parseWaterfall.addFirst(
      'instantWaterfall',
      new DisabledMap({
        disabledMap: options.level.disabledMap
      }).getInstantCommands()
    );
  }
}
Location: src/js/level/disabledMap.js
class DisabledMap {
  constructor(options) {
    this.disabledMap = options.disabledMap;
  }
  
  getInstantCommands() {
    var instants = [];
    
    Object.keys(this.disabledMap).forEach(function(command) {
      instants.push([
        new RegExp('^' + command),
        function() {
          throw new CommandResult({
            msg: intl.str('disabled-command', {command: command})
          });
        }
      ]);
    });
    
    return instants;
  }
}

Level Sequences

Levels are organized into sequences: Location: src/levels/index.js
var sequenceInfo = [
  {
    displayName: 'Introduction Sequence',
    ids: ['intro1', 'intro2', 'intro3', 'intro4']
  },
  {
    displayName: 'Ramping Up',
    ids: ['rampup1', 'rampup2', 'rampup3', 'rampup4']
  },
  {
    displayName: 'Moving Work Around',
    ids: ['move1', 'move2']
  },
  {
    displayName: 'A Mixed Bag',
    ids: ['mixed1', 'mixed2', 'mixed3']
  },
  {
    displayName: 'Advanced Topics',
    ids: ['advanced1', 'advanced2', 'advanced3']
  },
  {
    displayName: 'Push & Pull -- Git Remotes!',
    ids: ['remote1', 'remote2', 'remote3', 'remote4', 'remote5', 'remote6']
  },
  {
    displayName: 'To Origin And Beyond',
    ids: ['remoteAdvanced1', 'remoteAdvanced2', 'remoteAdvanced3']
  }
];

Build docs developers (and LLMs) love