- Custom dark-themed scrollbar (WebKit + Firefox) matching the cyberpunk UI - Added 12 new levels across new categories: - Combinational Logic: MUX, DEMUX, 3-input AND, Majority, Parity - Arithmetic: Half Subtractor, 1-bit Comparator - Decoders & Encoders: 2-to-4 Decoder, 4-to-2 Encoder, 7-Segment - Components: 1-bit ALU - Logic Basics: XNOR Gate Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
528 lines
21 KiB
JavaScript
528 lines
21 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: 'xnor_gate',
|
|
category: 'Logic Basics',
|
|
title: 'XNOR Gate',
|
|
description: 'Build an XNOR gate. True when both inputs are equal.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'NAND'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0 }, outputs: { 0: 1 } },
|
|
{ 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: ['XNOR is the inverse of XOR.', 'Build XOR first, then invert the output with another NAND-as-NOT.'],
|
|
difficulty: 3
|
|
},
|
|
// ============ Combinational Logic ============
|
|
{
|
|
id: 'mux_2to1',
|
|
category: 'Combinational Logic',
|
|
title: '2:1 Multiplexer',
|
|
description: 'Build a MUX. When SEL=0, output A. When SEL=1, output B. Three inputs: A (0), B (1), SEL (2).',
|
|
availableGates: ['INPUT', 'OUTPUT', 'AND', 'OR', 'NOT'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0, 2: 0 }, outputs: { 0: 0 } }, // SEL=0 → A=0
|
|
{ inputs: { 0: 1, 1: 0, 2: 0 }, outputs: { 0: 1 } }, // SEL=0 → A=1
|
|
{ inputs: { 0: 0, 1: 1, 2: 0 }, outputs: { 0: 0 } }, // SEL=0 → A=0
|
|
{ inputs: { 0: 1, 1: 1, 2: 0 }, outputs: { 0: 1 } }, // SEL=0 → A=1
|
|
{ inputs: { 0: 0, 1: 0, 2: 1 }, outputs: { 0: 0 } }, // SEL=1 → B=0
|
|
{ inputs: { 0: 0, 1: 1, 2: 1 }, outputs: { 0: 1 } }, // SEL=1 → B=1
|
|
{ inputs: { 0: 1, 1: 0, 2: 1 }, outputs: { 0: 0 } }, // SEL=1 → B=0
|
|
{ inputs: { 0: 1, 1: 1, 2: 1 }, outputs: { 0: 1 } } // SEL=1 → B=1
|
|
],
|
|
hints: ['MUX = (A AND NOT SEL) OR (B AND SEL).', 'You need 1 NOT, 2 AND, and 1 OR gate.'],
|
|
difficulty: 2
|
|
},
|
|
{
|
|
id: 'demux_1to2',
|
|
category: 'Combinational Logic',
|
|
title: '1:2 Demultiplexer',
|
|
description: 'Route input to one of two outputs based on SEL. Inputs: DATA (0), SEL (1). Outputs: Q0, Q1.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'AND', 'NOT'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0 }, outputs: { 0: 0, 1: 0 } }, // DATA=0, SEL=0 → Q0=0, Q1=0
|
|
{ inputs: { 0: 1, 1: 0 }, outputs: { 0: 1, 1: 0 } }, // DATA=1, SEL=0 → Q0=1, Q1=0
|
|
{ inputs: { 0: 0, 1: 1 }, outputs: { 0: 0, 1: 0 } }, // DATA=0, SEL=1 → Q0=0, Q1=0
|
|
{ inputs: { 0: 1, 1: 1 }, outputs: { 0: 0, 1: 1 } } // DATA=1, SEL=1 → Q0=0, Q1=1
|
|
],
|
|
hints: ['Q0 = DATA AND NOT SEL. Q1 = DATA AND SEL.', 'You need 1 NOT and 2 AND gates.'],
|
|
difficulty: 2
|
|
},
|
|
{
|
|
id: 'and3',
|
|
category: 'Combinational Logic',
|
|
title: '3-input AND',
|
|
description: 'Build a 3-input AND gate. Output is 1 only when all three inputs are 1.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'AND'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0, 2: 0 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 1, 1: 0, 2: 0 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 1, 1: 1, 2: 0 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 0, 1: 1, 2: 1 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 1, 1: 1, 2: 1 }, outputs: { 0: 1 } }
|
|
],
|
|
hints: ['Chain two AND gates: (A AND B) AND C.'],
|
|
difficulty: 1
|
|
},
|
|
{
|
|
id: 'majority',
|
|
category: 'Combinational Logic',
|
|
title: 'Majority Gate',
|
|
description: 'Output 1 if at least two of the three inputs are 1.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'AND', 'OR'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0, 2: 0 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 1, 1: 0, 2: 0 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 0, 1: 1, 2: 0 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 0, 1: 0, 2: 1 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 1, 1: 1, 2: 0 }, outputs: { 0: 1 } },
|
|
{ inputs: { 0: 1, 1: 0, 2: 1 }, outputs: { 0: 1 } },
|
|
{ inputs: { 0: 0, 1: 1, 2: 1 }, outputs: { 0: 1 } },
|
|
{ inputs: { 0: 1, 1: 1, 2: 1 }, outputs: { 0: 1 } }
|
|
],
|
|
hints: ['Majority = (A AND B) OR (A AND C) OR (B AND C).', 'You need 3 AND gates and 2 OR gates.'],
|
|
difficulty: 2
|
|
},
|
|
{
|
|
id: 'parity',
|
|
category: 'Combinational Logic',
|
|
title: 'Even Parity',
|
|
description: 'Output 1 if an even number of inputs are 1 (0 counts as even). Three inputs.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'XOR', 'NOT'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0, 2: 0 }, outputs: { 0: 1 } }, // 0 ones = even
|
|
{ inputs: { 0: 1, 1: 0, 2: 0 }, outputs: { 0: 0 } }, // 1 one = odd
|
|
{ inputs: { 0: 0, 1: 1, 2: 0 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 0, 1: 0, 2: 1 }, outputs: { 0: 0 } },
|
|
{ inputs: { 0: 1, 1: 1, 2: 0 }, outputs: { 0: 1 } }, // 2 ones = even
|
|
{ inputs: { 0: 1, 1: 0, 2: 1 }, outputs: { 0: 1 } },
|
|
{ inputs: { 0: 0, 1: 1, 2: 1 }, outputs: { 0: 1 } },
|
|
{ inputs: { 0: 1, 1: 1, 2: 1 }, outputs: { 0: 0 } } // 3 ones = odd
|
|
],
|
|
hints: ['XOR gives odd parity. Invert it for even parity.', 'Chain: NOT(A XOR B XOR C).'],
|
|
difficulty: 2
|
|
},
|
|
// ============ Arithmetic ============
|
|
{
|
|
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 } },
|
|
{ inputs: { 0: 0, 1: 1 }, outputs: { 0: 1, 1: 0 } },
|
|
{ inputs: { 0: 1, 1: 0 }, outputs: { 0: 1, 1: 0 } },
|
|
{ inputs: { 0: 1, 1: 1 }, outputs: { 0: 0, 1: 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 and Carry-out.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'NAND'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0, 2: 0 }, outputs: { 0: 0, 1: 0 } },
|
|
{ inputs: { 0: 0, 1: 0, 2: 1 }, outputs: { 0: 1, 1: 0 } },
|
|
{ inputs: { 0: 0, 1: 1, 2: 0 }, outputs: { 0: 1, 1: 0 } },
|
|
{ inputs: { 0: 0, 1: 1, 2: 1 }, outputs: { 0: 0, 1: 1 } },
|
|
{ inputs: { 0: 1, 1: 0, 2: 0 }, outputs: { 0: 1, 1: 0 } },
|
|
{ inputs: { 0: 1, 1: 0, 2: 1 }, outputs: { 0: 0, 1: 1 } },
|
|
{ inputs: { 0: 1, 1: 1, 2: 0 }, outputs: { 0: 0, 1: 1 } },
|
|
{ inputs: { 0: 1, 1: 1, 2: 1 }, outputs: { 0: 1, 1: 1 } }
|
|
],
|
|
hints: ['A full adder = two half adders + OR for the carry.', 'Sum = A XOR B XOR Cin. Cout = (A AND B) OR (Cin AND (A XOR B)).'],
|
|
difficulty: 4
|
|
},
|
|
{
|
|
id: 'half_subtractor',
|
|
category: 'Arithmetic',
|
|
title: 'Half Subtractor',
|
|
description: 'Build A minus B. Outputs: Difference (A XOR B) and Borrow (NOT A AND B).',
|
|
availableGates: ['INPUT', 'OUTPUT', 'AND', 'XOR', 'NOT'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0 }, outputs: { 0: 0, 1: 0 } }, // 0-0: diff=0, borrow=0
|
|
{ inputs: { 0: 0, 1: 1 }, outputs: { 0: 1, 1: 1 } }, // 0-1: diff=1, borrow=1
|
|
{ inputs: { 0: 1, 1: 0 }, outputs: { 0: 1, 1: 0 } }, // 1-0: diff=1, borrow=0
|
|
{ inputs: { 0: 1, 1: 1 }, outputs: { 0: 0, 1: 0 } } // 1-1: diff=0, borrow=0
|
|
],
|
|
hints: ['Difference = A XOR B (same as addition!).', 'Borrow = (NOT A) AND B.'],
|
|
difficulty: 2
|
|
},
|
|
{
|
|
id: 'comparator',
|
|
category: 'Arithmetic',
|
|
title: '1-bit Comparator',
|
|
description: 'Compare A and B. Three outputs: A>B, A=B, A<B.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'AND', 'OR', 'NOT', 'XOR'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0 }, outputs: { 0: 0, 1: 1, 2: 0 } }, // 0=0: GT=0, EQ=1, LT=0
|
|
{ inputs: { 0: 0, 1: 1 }, outputs: { 0: 0, 1: 0, 2: 1 } }, // 0<1: GT=0, EQ=0, LT=1
|
|
{ inputs: { 0: 1, 1: 0 }, outputs: { 0: 1, 1: 0, 2: 0 } }, // 1>0: GT=1, EQ=0, LT=0
|
|
{ inputs: { 0: 1, 1: 1 }, outputs: { 0: 0, 1: 1, 2: 0 } } // 1=1: GT=0, EQ=1, LT=0
|
|
],
|
|
hints: ['A>B = A AND NOT B.', 'A=B = NOT(A XOR B) = XNOR.', 'A<B = NOT A AND B.'],
|
|
difficulty: 3
|
|
},
|
|
// ============ Decoders & Encoders ============
|
|
{
|
|
id: 'decoder_2to4',
|
|
category: 'Decoders & Encoders',
|
|
title: '2-to-4 Decoder',
|
|
description: 'Decode 2 input bits into 4 output lines. Only one output is 1 at a time.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'AND', 'NOT'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0 }, outputs: { 0: 1, 1: 0, 2: 0, 3: 0 } }, // 00 → line 0
|
|
{ inputs: { 0: 0, 1: 1 }, outputs: { 0: 0, 1: 1, 2: 0, 3: 0 } }, // 01 → line 1
|
|
{ inputs: { 0: 1, 1: 0 }, outputs: { 0: 0, 1: 0, 2: 1, 3: 0 } }, // 10 → line 2
|
|
{ inputs: { 0: 1, 1: 1 }, outputs: { 0: 0, 1: 0, 2: 0, 3: 1 } } // 11 → line 3
|
|
],
|
|
hints: ['Q0 = NOT A AND NOT B.', 'Q1 = NOT A AND B. Q2 = A AND NOT B. Q3 = A AND B.', 'You need 2 NOT gates and 4 AND gates.'],
|
|
difficulty: 3
|
|
},
|
|
{
|
|
id: 'encoder_4to2',
|
|
category: 'Decoders & Encoders',
|
|
title: '4-to-2 Encoder',
|
|
description: 'Encode 4 input lines into 2-bit binary. Assume only one input is active at a time.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'OR'],
|
|
testCases: [
|
|
{ inputs: { 0: 1, 1: 0, 2: 0, 3: 0 }, outputs: { 0: 0, 1: 0 } }, // line 0 → 00
|
|
{ inputs: { 0: 0, 1: 1, 2: 0, 3: 0 }, outputs: { 0: 0, 1: 1 } }, // line 1 → 01
|
|
{ inputs: { 0: 0, 1: 0, 2: 1, 3: 0 }, outputs: { 0: 1, 1: 0 } }, // line 2 → 10
|
|
{ inputs: { 0: 0, 1: 0, 2: 0, 3: 1 }, outputs: { 0: 1, 1: 1 } } // line 3 → 11
|
|
],
|
|
hints: ['Bit 1 (MSB) = Input2 OR Input3.', 'Bit 0 (LSB) = Input1 OR Input3.', 'Just 2 OR gates!'],
|
|
difficulty: 2
|
|
},
|
|
{
|
|
id: 'seven_seg_a',
|
|
category: 'Decoders & Encoders',
|
|
title: '7-Segment: Segment A',
|
|
description: 'Drive segment A of a 7-segment display for digits 0-3. Segment A is ON for 0, 2, 3.',
|
|
availableGates: ['INPUT', 'OUTPUT', 'AND', 'OR', 'NOT'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0 }, outputs: { 0: 1 } }, // 0 → A on
|
|
{ inputs: { 0: 0, 1: 1 }, outputs: { 0: 0 } }, // 1 → A off
|
|
{ inputs: { 0: 1, 1: 0 }, outputs: { 0: 1 } }, // 2 → A on
|
|
{ inputs: { 0: 1, 1: 1 }, outputs: { 0: 1 } } // 3 → A on
|
|
],
|
|
hints: ['A = NOT(NOT A1 AND A0). Or equivalently: A = A1 OR NOT A0.'],
|
|
difficulty: 2
|
|
},
|
|
// ============ Components ============
|
|
{
|
|
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'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0, 2: 0, 3: 0 }, outputs: { 0: 0, 1: 0, 2: 0 } },
|
|
{ inputs: { 0: 0, 1: 0, 2: 0, 3: 1 }, outputs: { 0: 0, 1: 0, 2: 1 } },
|
|
{ inputs: { 0: 0, 1: 1, 2: 0, 3: 1 }, outputs: { 0: 0, 1: 1, 2: 1 } },
|
|
{ inputs: { 0: 1, 1: 1, 2: 0, 3: 1 }, outputs: { 0: 1, 1: 1, 2: 1 } },
|
|
{ inputs: { 0: 1, 1: 1, 2: 1, 3: 1 }, outputs: { 0: 0, 1: 1, 2: 1 } }
|
|
],
|
|
hints: ['Use two FULL_ADDER components chained together.', 'First adder has no carry-in (set to 0).'],
|
|
difficulty: 3
|
|
},
|
|
{
|
|
id: 'alu_1bit',
|
|
category: 'Components',
|
|
title: '1-bit ALU',
|
|
description: 'Build a 1-bit ALU. OP=0 → AND, OP=1 → OR. Inputs: A (0), B (1), OP (2).',
|
|
availableGates: ['INPUT', 'OUTPUT', 'AND', 'OR', 'NOT'],
|
|
testCases: [
|
|
{ inputs: { 0: 0, 1: 0, 2: 0 }, outputs: { 0: 0 } }, // 0 AND 0 = 0
|
|
{ inputs: { 0: 1, 1: 0, 2: 0 }, outputs: { 0: 0 } }, // 1 AND 0 = 0
|
|
{ inputs: { 0: 1, 1: 1, 2: 0 }, outputs: { 0: 1 } }, // 1 AND 1 = 1
|
|
{ inputs: { 0: 0, 1: 0, 2: 1 }, outputs: { 0: 0 } }, // 0 OR 0 = 0
|
|
{ inputs: { 0: 1, 1: 0, 2: 1 }, outputs: { 0: 1 } }, // 1 OR 0 = 1
|
|
{ inputs: { 0: 0, 1: 1, 2: 1 }, outputs: { 0: 1 } }, // 0 OR 1 = 1
|
|
{ inputs: { 0: 1, 1: 1, 2: 1 }, outputs: { 0: 1 } } // 1 OR 1 = 1
|
|
],
|
|
hints: ['Compute both A AND B and A OR B, then use a MUX to select based on OP.', 'MUX = (result0 AND NOT OP) OR (result1 AND OP).'],
|
|
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();
|
|
}
|