Files
logic-gates/js/levels.js
Jose Luis 920a30ffa8 fix: puzzle sidebar integrates into layout instead of overlapping waveform
- Puzzle panel now shifts canvas and waveform viewer right (340px) instead of
  overlapping them, using body class toggle and CSS transitions
- Canvas resize accounts for sidebar width
- Progress (completed/unlocked levels, custom components) persists in localStorage
- Level cards refresh on each panel open to reflect current progress

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 02:32:35 +01:00

328 lines
11 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 — load from localStorage if available
function loadProgress() {
try {
const saved = localStorage.getItem('logiclab_progress');
if (saved) {
const parsed = JSON.parse(saved);
return {
unlockedLevels: parsed.unlockedLevels || ['buffer'],
completedLevels: parsed.completedLevels || [],
currentLevel: null,
customComponents: parsed.customComponents || {}
};
}
} catch (e) { /* ignore parse errors */ }
return {
unlockedLevels: ['buffer'],
completedLevels: [],
currentLevel: null,
customComponents: {}
};
}
function saveProgress() {
try {
localStorage.setItem('logiclab_progress', JSON.stringify({
unlockedLevels: progress.unlockedLevels,
completedLevels: progress.completedLevels,
customComponents: progress.customComponents
}));
} catch (e) { /* ignore storage errors */ }
}
export const progress = loadProgress();
/**
* 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);
}
}
saveProgress();
}
/**
* Register a custom component (saved circuit)
*/
export function registerComponent(name, gateTypes, connections) {
progress.customComponents[name] = {
name,
gateTypes,
connections
};
saveProgress();
}
/**
* 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 = {};
saveProgress();
}