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>
384 lines
12 KiB
JavaScript
384 lines
12 KiB
JavaScript
// Puzzle UI system — level selection, puzzle mode, verification
|
|
import { state } from './state.js';
|
|
import { LEVELS, getLevel, isLevelUnlocked, isLevelCompleted, verifyLevel, completeLevel } from './levels.js';
|
|
import { evaluateAll } from './gates.js';
|
|
|
|
export let puzzleMode = false;
|
|
export let currentLevel = null;
|
|
|
|
/**
|
|
* Initialize puzzle UI
|
|
*/
|
|
export function initPuzzleUI() {
|
|
createLevelSelectionPanel();
|
|
createPuzzlePanel();
|
|
initModeToggle();
|
|
}
|
|
|
|
/**
|
|
* Create level selection panel
|
|
*/
|
|
function createLevelSelectionPanel() {
|
|
const panel = document.createElement('div');
|
|
panel.id = 'level-panel';
|
|
panel.className = 'puzzle-panel';
|
|
panel.innerHTML = `
|
|
<div class="level-header">
|
|
<h2>Levels</h2>
|
|
<button id="close-levels" class="close-btn">✕</button>
|
|
</div>
|
|
<div id="levels-container" class="levels-container"></div>
|
|
`;
|
|
document.body.appendChild(panel);
|
|
|
|
// Create level cards
|
|
const container = document.getElementById('levels-container');
|
|
const categories = new Map();
|
|
|
|
LEVELS.forEach(level => {
|
|
if (!categories.has(level.category)) {
|
|
categories.set(level.category, []);
|
|
}
|
|
categories.get(level.category).push(level);
|
|
});
|
|
|
|
categories.forEach((levels, category) => {
|
|
const categoryEl = document.createElement('div');
|
|
categoryEl.className = 'level-category';
|
|
categoryEl.innerHTML = `<h3>${category}</h3>`;
|
|
|
|
levels.forEach(level => {
|
|
const unlocked = isLevelUnlocked(level.id);
|
|
const completed = isLevelCompleted(level.id);
|
|
const card = document.createElement('div');
|
|
card.className = `level-card ${completed ? 'completed' : ''} ${!unlocked ? 'locked' : ''}`;
|
|
card.innerHTML = `
|
|
<div class="level-card-header">
|
|
<div class="level-title">${level.title}</div>
|
|
<div class="level-difficulty">${'●'.repeat(level.difficulty)}${'○'.repeat(5 - level.difficulty)}</div>
|
|
</div>
|
|
<div class="level-description">${level.description}</div>
|
|
<div class="level-status">
|
|
${completed ? '✓ COMPLETED' : unlocked ? 'READY' : '🔒 LOCKED'}
|
|
</div>
|
|
`;
|
|
|
|
if (unlocked) {
|
|
card.addEventListener('click', () => selectLevel(level.id));
|
|
}
|
|
|
|
categoryEl.appendChild(card);
|
|
});
|
|
|
|
container.appendChild(categoryEl);
|
|
});
|
|
|
|
document.getElementById('close-levels').addEventListener('click', () => {
|
|
panel.classList.remove('visible');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create puzzle info panel (shown when level is selected)
|
|
*/
|
|
function createPuzzlePanel() {
|
|
const panel = document.createElement('div');
|
|
panel.id = 'puzzle-panel';
|
|
panel.className = 'puzzle-panel puzzle-info';
|
|
panel.innerHTML = `
|
|
<div class="puzzle-header">
|
|
<div>
|
|
<h2 id="puzzle-title">Level Title</h2>
|
|
<p id="puzzle-description" class="puzzle-description">Level description</p>
|
|
</div>
|
|
<button id="close-puzzle" class="close-btn">✕</button>
|
|
</div>
|
|
<div id="puzzle-content">
|
|
<div class="truth-table-section">
|
|
<h3>Truth Table</h3>
|
|
<div id="truth-table" class="truth-table"></div>
|
|
</div>
|
|
<div class="available-gates-section">
|
|
<h3>Available Gates</h3>
|
|
<div id="available-gates" class="available-gates-list"></div>
|
|
</div>
|
|
<div class="puzzle-actions">
|
|
<button id="verify-btn" class="action-btn verify-btn">Verify Solution</button>
|
|
<button id="hint-btn" class="action-btn hint-btn">Hint</button>
|
|
<button id="reset-puzzle-btn" class="action-btn reset-btn">Reset</button>
|
|
</div>
|
|
<div id="verification-result" class="verification-result"></div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(panel);
|
|
|
|
document.getElementById('close-puzzle').addEventListener('click', () => {
|
|
exitPuzzleMode();
|
|
});
|
|
|
|
document.getElementById('verify-btn').addEventListener('click', () => {
|
|
verifySolution();
|
|
});
|
|
|
|
document.getElementById('hint-btn').addEventListener('click', () => {
|
|
showHint();
|
|
});
|
|
|
|
document.getElementById('reset-puzzle-btn').addEventListener('click', () => {
|
|
resetPuzzle();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize mode toggle in toolbar
|
|
*/
|
|
function initModeToggle() {
|
|
const toolbar = document.getElementById('toolbar');
|
|
const separator = document.createElement('div');
|
|
separator.className = 'separator';
|
|
|
|
const modeToggle = document.createElement('div');
|
|
modeToggle.id = 'mode-toggle';
|
|
modeToggle.className = 'mode-toggle';
|
|
modeToggle.innerHTML = `
|
|
<button id="mode-sandbox" class="mode-btn active" data-mode="sandbox">Sandbox</button>
|
|
<button id="mode-puzzle" class="mode-btn" data-mode="puzzle">Puzzle</button>
|
|
`;
|
|
|
|
toolbar.appendChild(separator);
|
|
toolbar.appendChild(modeToggle);
|
|
|
|
document.getElementById('mode-sandbox').addEventListener('click', () => {
|
|
setSandboxMode();
|
|
});
|
|
|
|
document.getElementById('mode-puzzle').addEventListener('click', () => {
|
|
setPuzzleMode();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Set sandbox mode
|
|
*/
|
|
export function setSandboxMode() {
|
|
puzzleMode = false;
|
|
currentLevel = null;
|
|
document.getElementById('mode-sandbox').classList.add('active');
|
|
document.getElementById('mode-puzzle').classList.remove('active');
|
|
document.getElementById('level-panel').classList.remove('visible');
|
|
document.getElementById('puzzle-panel').classList.remove('visible');
|
|
document.getElementById('toolbar').classList.remove('puzzle-mode');
|
|
}
|
|
|
|
/**
|
|
* Set puzzle mode
|
|
*/
|
|
export function setPuzzleMode() {
|
|
puzzleMode = true;
|
|
document.getElementById('mode-puzzle').classList.add('active');
|
|
document.getElementById('mode-sandbox').classList.remove('active');
|
|
document.getElementById('level-panel').classList.add('visible');
|
|
}
|
|
|
|
/**
|
|
* Select a level
|
|
*/
|
|
function selectLevel(levelId) {
|
|
const level = getLevel(levelId);
|
|
if (!level || !isLevelUnlocked(levelId)) return;
|
|
|
|
currentLevel = level;
|
|
|
|
// Update puzzle panel
|
|
document.getElementById('puzzle-title').textContent = level.title;
|
|
document.getElementById('puzzle-description').textContent = level.description;
|
|
|
|
// Render truth table
|
|
renderTruthTable(level);
|
|
|
|
// Show available gates
|
|
renderAvailableGates(level);
|
|
|
|
// Clear verification result
|
|
document.getElementById('verification-result').innerHTML = '';
|
|
|
|
// Hide level panel, show puzzle panel
|
|
document.getElementById('level-panel').classList.remove('visible');
|
|
document.getElementById('puzzle-panel').classList.add('visible');
|
|
|
|
// Clear circuit and setup for puzzle
|
|
clearCircuitForPuzzle(level);
|
|
}
|
|
|
|
/**
|
|
* Render truth table
|
|
*/
|
|
function renderTruthTable(level) {
|
|
const table = document.getElementById('truth-table');
|
|
table.innerHTML = '';
|
|
|
|
// Get input/output counts
|
|
const testCase = level.testCases[0];
|
|
const inputCount = Object.keys(testCase.inputs).length;
|
|
const outputCount = Object.keys(testCase.outputs).length;
|
|
|
|
// Create header
|
|
const header = document.createElement('div');
|
|
header.className = 'truth-table-row header';
|
|
for (let i = 0; i < inputCount; i++) {
|
|
const cell = document.createElement('div');
|
|
cell.className = 'truth-cell input-cell';
|
|
cell.textContent = `A${i}`;
|
|
header.appendChild(cell);
|
|
}
|
|
for (let i = 0; i < outputCount; i++) {
|
|
const cell = document.createElement('div');
|
|
cell.className = 'truth-cell output-cell';
|
|
cell.textContent = `Q${i}`;
|
|
header.appendChild(cell);
|
|
}
|
|
table.appendChild(header);
|
|
|
|
// Create rows
|
|
level.testCases.forEach((tc, idx) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'truth-table-row';
|
|
|
|
for (let i = 0; i < inputCount; i++) {
|
|
const cell = document.createElement('div');
|
|
cell.className = 'truth-cell input-cell';
|
|
cell.textContent = tc.inputs[i];
|
|
row.appendChild(cell);
|
|
}
|
|
|
|
for (let i = 0; i < outputCount; i++) {
|
|
const cell = document.createElement('div');
|
|
cell.className = 'truth-cell output-cell';
|
|
cell.textContent = tc.outputs[i];
|
|
row.appendChild(cell);
|
|
}
|
|
|
|
table.appendChild(row);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render available gates
|
|
*/
|
|
function renderAvailableGates(level) {
|
|
const container = document.getElementById('available-gates');
|
|
container.innerHTML = '';
|
|
|
|
level.availableGates.forEach(gateName => {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'available-gate-btn';
|
|
btn.textContent = gateName;
|
|
btn.addEventListener('click', () => {
|
|
state.placingGate = gateName;
|
|
});
|
|
container.appendChild(btn);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clear circuit for puzzle mode
|
|
*/
|
|
function clearCircuitForPuzzle(level) {
|
|
state.gates = [];
|
|
state.connections = [];
|
|
state.placingGate = null;
|
|
state.connecting = null;
|
|
evaluateAll();
|
|
}
|
|
|
|
/**
|
|
* Verify solution
|
|
*/
|
|
function verifySolution() {
|
|
if (!currentLevel) return;
|
|
|
|
const result = verifyLevel(currentLevel.id);
|
|
const resultEl = document.getElementById('verification-result');
|
|
|
|
if (result.passed) {
|
|
resultEl.className = 'verification-result success';
|
|
resultEl.innerHTML = `
|
|
<div class="result-message">✓ ${result.message}</div>
|
|
<button id="next-level-btn" class="action-btn">Next Level →</button>
|
|
`;
|
|
|
|
completeLevel(currentLevel.id);
|
|
|
|
document.getElementById('next-level-btn').addEventListener('click', () => {
|
|
const currentIdx = LEVELS.findIndex(l => l.id === currentLevel.id);
|
|
if (currentIdx < LEVELS.length - 1) {
|
|
const nextLevel = LEVELS[currentIdx + 1];
|
|
selectLevel(nextLevel.id);
|
|
} else {
|
|
resultEl.innerHTML = '<div class="result-message">🎉 All levels completed!</div>';
|
|
}
|
|
});
|
|
} else {
|
|
resultEl.className = 'verification-result failure';
|
|
resultEl.innerHTML = `<div class="result-message">✗ ${result.message}</div>`;
|
|
|
|
// Show which tests failed
|
|
const failedTests = result.results.filter(r => !r.passed);
|
|
if (failedTests.length > 0 && failedTests.length <= 3) {
|
|
resultEl.innerHTML += '<div class="failed-tests">';
|
|
failedTests.forEach(test => {
|
|
resultEl.innerHTML += `
|
|
<div class="failed-test">
|
|
Input: ${JSON.stringify(test.inputs)} →
|
|
Expected: ${JSON.stringify(test.expectedOutputs)},
|
|
Got: ${JSON.stringify(test.actualOutputs)}
|
|
</div>
|
|
`;
|
|
});
|
|
resultEl.innerHTML += '</div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show hint
|
|
*/
|
|
function showHint() {
|
|
if (!currentLevel || !currentLevel.hints) return;
|
|
|
|
const hints = currentLevel.hints;
|
|
let hintIndex = parseInt(sessionStorage.getItem(`hint_${currentLevel.id}`) || '0');
|
|
|
|
if (hintIndex >= hints.length) {
|
|
alert('No more hints available!');
|
|
return;
|
|
}
|
|
|
|
const hint = hints[hintIndex];
|
|
alert(`Hint: ${hint}`);
|
|
sessionStorage.setItem(`hint_${currentLevel.id}`, String(hintIndex + 1));
|
|
}
|
|
|
|
/**
|
|
* Reset puzzle
|
|
*/
|
|
function resetPuzzle() {
|
|
if (!currentLevel) return;
|
|
clearCircuitForPuzzle(currentLevel);
|
|
document.getElementById('verification-result').innerHTML = '';
|
|
}
|
|
|
|
/**
|
|
* Exit puzzle mode
|
|
*/
|
|
export function exitPuzzleMode() {
|
|
setSandboxMode();
|
|
}
|
|
|
|
/**
|
|
* Show level panel
|
|
*/
|
|
export function showLevelPanel() {
|
|
document.getElementById('level-panel').classList.add('visible');
|
|
}
|