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>
This commit is contained in:
383
js/puzzleUI.js
Normal file
383
js/puzzleUI.js
Normal file
@@ -0,0 +1,383 @@
|
||||
// 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');
|
||||
}
|
||||
Reference in New Issue
Block a user