Files
logic-gates/js/puzzleUI.js
Jose Luis b2e367817c feat: add Turing Complete-style puzzle system
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>
2026-03-20 02:06:57 +01:00

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');
}