Add progressive puzzle mode alongside the existing sandbox: - 8 levels from basic gates to 2-bit adder - Truth table verification with pass/fail feedback - Gate restrictions per level - Custom components system (save circuits as reusable chips) - Save/load circuits as JSON - Level selection sidebar with difficulty ratings - Mode toggle: Sandbox (free play) vs Puzzle (guided levels) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
298 lines
9.8 KiB
JavaScript
298 lines
9.8 KiB
JavaScript
// Level system — puzzle definitions, verification, and progression
|
|
import { state } from './state.js';
|
|
import { evaluate } from './gates.js';
|
|
|
|
// Level definitions
|
|
export const LEVELS = [
|
|
{
|
|
id: 'buffer',
|
|
category: 'Logic Basics',
|
|
title: 'Buffer',
|
|
description: 'Connect input to output directly. Pass the value through.',
|
|
availableGates: ['INPUT', 'OUTPUT'],
|
|
testCases: [
|
|
{ inputs: { 0: 0 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 1 }, outputs: { 0: 1 } }
|
|
],
|
|
hints: ['Use only one INPUT and one OUTPUT gate.'],
|
|
difficulty: 1
|
|
},
|
|
{
|
|
id: 'not_gate',
|
|
category: 'Logic Basics',
|
|
title: 'NOT Gate',
|
|
description: 'Build a NOT gate using only NAND gates. Invert the input signal.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'NAND'],
|
|
testCases: [
|
|
{ inputs: { 0: 0 }, outputs: { 0: 1 } },
|
|
{ inputs: { 0: 1 }, outputs: { 0: 0 } }
|
|
],
|
|
hints: ['Connect both inputs of a NAND gate to the same signal.'],
|
|
difficulty: 1
|
|
},
|
|
{
|
|
id: 'and_gate',
|
|
category: 'Logic Basics',
|
|
title: 'AND Gate',
|
|
description: 'Build an AND gate using only NAND gates. AND = NAND with inverted output.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'NAND'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 0, 1: 1 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 1, 1: 0 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 1, 1: 1 }, outputs: { 0: 1 } }
|
|
],
|
|
hints: ['NOT(NAND) = AND. You need 2 NAND gates.', 'Use a NAND gate as a NOT on the output.'],
|
|
difficulty: 2
|
|
},
|
|
{
|
|
id: 'or_gate',
|
|
category: 'Logic Basics',
|
|
title: 'OR Gate',
|
|
description: 'Build an OR gate using NAND gates. OR = NAND(NOT A, NOT B).',
|
|
availableGates: ['INPUT', 'OUTPUT', 'NAND'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 0, 1: 1 }, outputs: { 0: 1 } },
|
|
{ inputs: { 0: 1, 1: 0 }, outputs: { 0: 1 } },
|
|
{ inputs: { 0: 1, 1: 1 }, outputs: { 0: 1 } }
|
|
],
|
|
hints: ['Invert both inputs first, then NAND them.', 'You need 3 NAND gates total.'],
|
|
difficulty: 2
|
|
},
|
|
{
|
|
id: 'xor_gate',
|
|
category: 'Logic Basics',
|
|
title: 'XOR Gate',
|
|
description: 'Build an XOR gate using NAND gates. True when inputs differ.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'NAND'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 0, 1: 1 }, outputs: { 0: 1 } },
|
|
{ inputs: { 0: 1, 1: 0 }, outputs: { 0: 1 } },
|
|
{ inputs: { 0: 1, 1: 1 }, outputs: { 0: 0 } }
|
|
],
|
|
hints: ['XOR = (A NAND B) NAND (NOT(A NAND NOT B) NAND NOT(NOT A NAND B))', 'This requires 4-5 NAND gates.'],
|
|
difficulty: 3
|
|
},
|
|
{
|
|
id: 'half_adder',
|
|
category: 'Arithmetic',
|
|
title: 'Half Adder',
|
|
description: 'Build a half adder. Two outputs: Sum (A XOR B) and Carry (A AND B).',
|
|
availableGates: ['INPUT', 'OUTPUT', 'NAND'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0 }, outputs: { 0: 0, 1: 0 } }, // 0+0: sum=0, carry=0
|
|
{ inputs: { 0: 0, 1: 1 }, outputs: { 0: 1, 1: 0 } }, // 0+1: sum=1, carry=0
|
|
{ inputs: { 0: 1, 1: 0 }, outputs: { 0: 1, 1: 0 } }, // 1+0: sum=1, carry=0
|
|
{ inputs: { 0: 1, 1: 1 }, outputs: { 0: 0, 1: 1 } } // 1+1: sum=0, carry=1
|
|
],
|
|
hints: ['Two independent functions: Sum=XOR, Carry=AND.', 'You need 2 OUTPUT gates.'],
|
|
difficulty: 3
|
|
},
|
|
{
|
|
id: 'full_adder',
|
|
category: 'Arithmetic',
|
|
title: 'Full Adder',
|
|
description: 'Build a full adder with carry-in. Outputs: Sum (3-input XOR) and Carry.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'NAND'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0, 2: 0 }, outputs: { 0: 0, 1: 0 } }, // 0+0+0
|
|
{ inputs: { 0: 0, 1: 0, 2: 1 }, outputs: { 0: 1, 1: 0 } }, // 0+0+1
|
|
{ inputs: { 0: 0, 1: 1, 2: 0 }, outputs: { 0: 1, 1: 0 } }, // 0+1+0
|
|
{ inputs: { 0: 0, 1: 1, 2: 1 }, outputs: { 0: 0, 1: 1 } }, // 0+1+1
|
|
{ inputs: { 0: 1, 1: 0, 2: 0 }, outputs: { 0: 1, 1: 0 } }, // 1+0+0
|
|
{ inputs: { 0: 1, 1: 0, 2: 1 }, outputs: { 0: 0, 1: 1 } }, // 1+0+1
|
|
{ inputs: { 0: 1, 1: 1, 2: 0 }, outputs: { 0: 0, 1: 1 } }, // 1+1+0
|
|
{ inputs: { 0: 1, 1: 1, 2: 1 }, outputs: { 0: 1, 1: 1 } } // 1+1+1
|
|
],
|
|
hints: ['Reuse your half adder logic. A full adder = half adder + half adder + OR.'],
|
|
difficulty: 4
|
|
},
|
|
{
|
|
id: 'two_bit_adder',
|
|
category: 'Components',
|
|
title: '2-bit Adder',
|
|
description: 'Chain two full adders to add two 2-bit numbers.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'FULL_ADDER'], // FULL_ADDER is saved component
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0, 2: 0, 3: 0 }, outputs: { 0: 0, 1: 0, 2: 0 } }, // 00+00=000
|
|
{ inputs: { 0: 0, 1: 0, 2: 0, 3: 1 }, outputs: { 0: 0, 1: 0, 2: 1 } }, // 00+01=001
|
|
{ inputs: { 0: 0, 1: 1, 2: 0, 3: 1 }, outputs: { 0: 0, 1: 1, 2: 1 } }, // 01+01=010
|
|
{ inputs: { 0: 1, 1: 1, 2: 0, 3: 1 }, outputs: { 0: 1, 1: 1, 2: 1 } }, // 11+01=100
|
|
{ inputs: { 0: 1, 1: 1, 2: 1, 3: 1 }, outputs: { 0: 0, 1: 1, 2: 1 } } // 11+11=110
|
|
],
|
|
hints: ['Use two FULL_ADDER components chained together.', 'First adder has no carry-in (set to 0).'],
|
|
difficulty: 3
|
|
}
|
|
];
|
|
|
|
// Progress tracking
|
|
export const progress = {
|
|
unlockedLevels: ['buffer'],
|
|
completedLevels: [],
|
|
currentLevel: null,
|
|
customComponents: {} // { name -> component definition }
|
|
};
|
|
|
|
/**
|
|
* Get all available levels for display
|
|
*/
|
|
export function getAllLevels() {
|
|
return LEVELS;
|
|
}
|
|
|
|
/**
|
|
* Get a level by ID
|
|
*/
|
|
export function getLevel(id) {
|
|
return LEVELS.find(l => l.id === id);
|
|
}
|
|
|
|
/**
|
|
* Check if a level is unlocked
|
|
*/
|
|
export function isLevelUnlocked(levelId) {
|
|
return progress.unlockedLevels.includes(levelId);
|
|
}
|
|
|
|
/**
|
|
* Check if a level is completed
|
|
*/
|
|
export function isLevelCompleted(levelId) {
|
|
return progress.completedLevels.includes(levelId);
|
|
}
|
|
|
|
/**
|
|
* Verify if the current circuit passes all test cases for a level
|
|
*/
|
|
export function verifyLevel(levelId) {
|
|
const level = getLevel(levelId);
|
|
if (!level) return { passed: false, results: [], message: 'Level not found' };
|
|
|
|
const results = [];
|
|
let allPassed = true;
|
|
|
|
for (const testCase of level.testCases) {
|
|
const result = runTestCase(level, testCase);
|
|
results.push(result);
|
|
if (!result.passed) allPassed = false;
|
|
}
|
|
|
|
return {
|
|
passed: allPassed,
|
|
results,
|
|
message: allPassed ? 'All tests passed! ✓' : `${results.filter(r => r.passed).length}/${results.length} tests passed`
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Run a single test case
|
|
*/
|
|
function runTestCase(level, testCase) {
|
|
// Find INPUT gates
|
|
const inputGates = state.gates.filter(g => g.type === 'INPUT');
|
|
// Find OUTPUT gates
|
|
const outputGates = state.gates.filter(g => g.type === 'OUTPUT');
|
|
|
|
// Check if we have the right number of inputs and outputs
|
|
const inputIds = Object.keys(testCase.inputs).map(Number).sort((a, b) => a - b);
|
|
const outputIds = Object.keys(testCase.outputs).map(Number).sort((a, b) => a - b);
|
|
|
|
if (inputGates.length !== inputIds.length || outputGates.length !== outputIds.length) {
|
|
return {
|
|
passed: false,
|
|
inputs: testCase.inputs,
|
|
expectedOutputs: testCase.outputs,
|
|
actualOutputs: {},
|
|
error: `Expected ${inputIds.length} inputs and ${outputIds.length} outputs, got ${inputGates.length} and ${outputGates.length}`
|
|
};
|
|
}
|
|
|
|
// Set input values
|
|
for (let i = 0; i < inputIds.length; i++) {
|
|
if (inputGates[i]) {
|
|
inputGates[i].value = testCase.inputs[i];
|
|
}
|
|
}
|
|
|
|
// Evaluate circuit
|
|
state.gates.forEach(g => {
|
|
if (g.type !== 'INPUT' && g.type !== 'CLOCK') g.value = 0;
|
|
});
|
|
state.gates.forEach(g => evaluate(g));
|
|
|
|
// Check outputs
|
|
const expected = testCase.outputs;
|
|
let passed = true;
|
|
const actualOutputs = {};
|
|
|
|
for (let i = 0; i < outputIds.length; i++) {
|
|
const actualValue = outputGates[i]?.value || 0;
|
|
actualOutputs[i] = actualValue;
|
|
if (actualValue !== expected[i]) {
|
|
passed = false;
|
|
}
|
|
}
|
|
|
|
return {
|
|
passed,
|
|
inputs: testCase.inputs,
|
|
expectedOutputs: expected,
|
|
actualOutputs
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Complete a level and unlock the next one
|
|
*/
|
|
export function completeLevel(levelId) {
|
|
const levelIndex = LEVELS.findIndex(l => l.id === levelId);
|
|
if (levelIndex >= 0 && !progress.completedLevels.includes(levelId)) {
|
|
progress.completedLevels.push(levelId);
|
|
}
|
|
|
|
// Unlock next level
|
|
if (levelIndex < LEVELS.length - 1) {
|
|
const nextId = LEVELS[levelIndex + 1].id;
|
|
if (!progress.unlockedLevels.includes(nextId)) {
|
|
progress.unlockedLevels.push(nextId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a custom component (saved circuit)
|
|
*/
|
|
export function registerComponent(name, gateTypes, connections) {
|
|
progress.customComponents[name] = {
|
|
name,
|
|
gateTypes,
|
|
connections
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get a custom component
|
|
*/
|
|
export function getComponent(name) {
|
|
return progress.customComponents[name];
|
|
}
|
|
|
|
/**
|
|
* Get all custom components
|
|
*/
|
|
export function getAllComponents() {
|
|
return progress.customComponents;
|
|
}
|
|
|
|
/**
|
|
* Reset progress (dev/testing)
|
|
*/
|
|
export function resetProgress() {
|
|
progress.unlockedLevels = ['buffer'];
|
|
progress.completedLevels = [];
|
|
progress.currentLevel = null;
|
|
progress.customComponents = {};
|
|
}
|