import { Robot } from "./robot.js"; import { GameWorld } from "./gameworld.js"; import { lessons } from './data/lessons.js'; let mostRecentCode = ""; // The user’s code let codeRanGood = false; // Your own function that returns true/false let consoleText = ""; // What was printed let currentLesson = 0; let lessonComplete = false; async function loadLessons() { const response = await fetch('data/lessons.json'); lessons = await response.json(); showLesson(0); } function showLesson(index) { if (index < 0 || index >= lessons.length) return; lessonComplete = false; currentLesson = index; const lesson = lessons[index]; loadLessonContent(lesson); updateTabs(lessons, index); document.getElementById('prev-lesson').disabled = index === 0; document.getElementById('next-lesson').disabled = index === lessons.length - 1; //console.log(isLessonDone(lesson.id)); updateLessonStatus(); PopulateObjectives(lesson.objectives); // Populate objectives for the lesson } function loadLessonContent(lesson) { document.getElementById('lesson-title').textContent = lesson.title; document.getElementById('lesson-content').innerHTML = lesson.content; } function updateTabs(lessons, currentIndex) { let select = document.getElementById('lesson-select'); let select_robot = document.getElementById('robot-select'); select.innerHTML = ''; // Clear old options select_robot.innerHTML = ''; // Clear old options lessons.forEach((lesson, index) => { const option = document.createElement('option'); option.value = index; option.textContent = lesson.title || `Lesson ${index + 1}`; if (lesson.level == "basics") { select.appendChild(option); } else { select_robot.appendChild(option); } }); // Set current selected lesson select.value = currentIndex; select_robot.value = currentIndex; // Handle user selection select.addEventListener('change', () => { const selectedIndex = Number(select.value); console.log("Selected lesson index:", selectedIndex); showLesson(selectedIndex); }); select_robot.addEventListener('change', () => { const selectedIndex = Number(select_robot.value); console.log("Selected lesson index:", selectedIndex); showLesson(selectedIndex); }); } document.getElementById('prev-lesson').addEventListener('click', () => { showLesson(currentLesson - 1); }); document.getElementById('next-lesson').addEventListener('click', () => { showLesson(currentLesson + 1); }); function checkLessonDone() { //consoleText = outputText; // Update console text if (consoleText.includes("Error") || consoleText.includes("Exception")) { codeRanGood = false; } else { codeRanGood = true; } console.log("codeRanGood ", codeRanGood); //checkCurrentStep(outputText); const lesson = lessons[currentLesson]; const result = lesson.doneCondition({ code: mostRecentCode, consoleText: consoleText, codeRanGood: codeRanGood, gameWorld: gameWorld }); if (result.done) { markLessonDone(lesson.id); } if (result.progressArray) { console.log("Progress: ", result.progressArray); for (let i = 0; i < result.progressArray.length; i++) { const objective = result.progressArray[i]; if (objective) { toggleObjective(i, true); // Mark as completed } else { toggleObjective(i, false); // Mark as not completed } } } if (result.hint) { logToConsole("Hint: " + result.hint, false); //console.log("Hint:", result.hint); // Or show it in your console UI } } function markLessonDone(lessonId) { if (lessonComplete) return; // Prevent marking multiple times lessonComplete = true; // Set flag to prevent further marking // Your logic here: e.g., store in localStorage or update UI console.log(`Lesson ${lessonId} marked as done!`); // For example: localStorage.setItem(`lessonDone_${lessonId}`, 'true'); logToConsole("✅ Task Completed! ✅", false); updateLessonStatus(); } function updateLessonStatus() { const statusEl = document.getElementById('lesson-status'); const lesson = lessons[currentLesson]; const done = isLessonDone(lesson.id); // your persistent check if (done) { statusEl.textContent = '✅'; } else { statusEl.textContent = ''; } document.getElementById('next-lesson').disabled = !done || currentLesson === lessons.length - 1; } function isLessonDone(lessonId) { return localStorage.getItem(`lessonDone_${lessonId}`) === 'true'; } let currentStepIndex = 0; function checkCurrentStep(text) { const lesson = lessons[currentLesson]; const step = lesson.steps[currentStepIndex]; const result = step.doneCondition(text); if (result) { currentStepIndex++; renderSteps(); // Re-render to show the new step } } function renderSteps() { const lesson = lessons[currentLesson]; const container = document.getElementById("lesson-content"); container.innerHTML = ""; loadLessonContent(lesson); // Load the lesson content first for (let i = 0; i <= currentStepIndex && i < lesson.steps.length; i++) { const stepEl = document.createElement("div"); stepEl.innerHTML = lesson.steps[i].content; container.appendChild(stepEl); } } function clearLessonProgress() { Object.keys(localStorage).forEach(key => { if (key.startsWith('lessonDone_')) { localStorage.removeItem(key); } }); // Optionally update the UI after clearing updateLessonStatus(); } let objectiveElements = []; // store checkbox + item div const listContainer = document.getElementById("objectives-list"); function PopulateObjectives(objectives) { listContainer.innerHTML = ""; // Clear the DOM objectiveElements.length = 0; // Clear the stored references objectives.forEach((text, index) => { const item = document.createElement("div"); item.className = "objective"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.id = "obj-" + index; checkbox.disabled = true; // make it non-interactive const label = document.createElement("label"); label.htmlFor = checkbox.id; label.textContent = text; item.appendChild(checkbox); item.appendChild(label); listContainer.appendChild(item); objectiveElements.push({ checkbox, item }); // Store reference }); } function toggleObjective(index, completed = true) { const obj = objectiveElements[index]; if (obj) { obj.checkbox.checked = completed; obj.item.classList.toggle("completed", completed); } } //clearLessonProgress(); // Clear progress on load for testing showLesson(9); const consoleElement = document.getElementById("console"); const gameCanvas = document.getElementById("gameCanvas"); const ctx = gameCanvas.getContext("2d"); const gameWorld = new GameWorld(); let monacoEditor; let pyodideWorker = startPyodideWorker(); let robots = null;//createInitialRobots(); //gameWorld.addRobot(robots["player"]); let paused = false; let scale = 1; // Zoom level let offsetX = 0; // Pan X let offsetY = 0; // Pan Y let isPanning = false; let startX, startY; // Mouse start positions // ✅ Function to create the Pyodide Worker function startPyodideWorker() { const worker = new Worker("pyodide-worker.js?v=" + Date.now()); // ✅ Reattach the event listener when a new worker is created worker.onmessage = (event) => { if (paused) return; switch (event.data.type) { case "console": logToConsole(event.data.data); break; case "error": logToConsole(`${event.data.message}`); break; case "fire": fire(); break; case "turn": turn(event.data.data); break; case "move": move(event.data.data); break; case "execution_done": console.log("Execution done"); document.getElementById('compile-button').disabled = false; checkLessonDone(); break; } }; return worker; } // ✅ Function to create initial robots function createInitialRobots() { return { "player": new Robot("player", 50, 50, "blue") //"enemy1": new Robot("enemy1", 200, 150, "red"), //"enemy2": new Robot("enemy2", 400, 250, "red") }; } // ✅ Function to log messages to console const maxLines = 64; const logLines = []; function logToConsole(text, checkLesson = true) { if (text.includes("Pyodide not initialized yet.")) return; const newLines = text.split('\n').map(line => line.trim()).filter(line => line !== ""); // Add new lines to the array logLines.push(...newLines); // Keep only the last maxLines entries if (logLines.length > maxLines) { logLines.splice(0, logLines.length - maxLines); } // Clear console consoleElement.innerHTML = ""; // Create and append separate divs for each line logLines.forEach(lineText => { const lineDiv = document.createElement('div'); lineDiv.innerHTML = lineText; // <-- render HTML tags here lineDiv.style.maxWidth = "100%"; lineDiv.style.whiteSpace = "pre-wrap"; lineDiv.style.overflowWrap = "break-word"; lineDiv.style.wordWrap = "break-word"; consoleElement.appendChild(lineDiv); }); // Scroll to bottom consoleElement.scrollTop = consoleElement.scrollHeight; if (checkLesson && !text.includes("Welcome")) { // Don't check lesson completion for welcome message consoleText = text; // Update console text //checkLessonDone(text); } } function clearConsole() { logLines.length = 0; // Empty the logLines array consoleElement.innerHTML = ""; // Clear the console DOM element } // ✅ Game Control Functions function fire() { logToConsole("🔥 Gun Fired! 🔥"); } function turn(deg) { robots["player"].turn(deg); } function move(distance) { robots["player"].move(distance); } // ✅ Pause/Resume Function function togglePause() { paused = !paused; document.getElementById("pause-button").innerText = paused ? "Resume" : "Pause"; } function resetGameWorld() { robots = createInitialRobots(); gameWorld.reset(robots["player"]); pyodideWorker.postMessage({ type: "interrupt" }); } // ✅ Reset Function (Fixed) function resetGame() { // Terminate the worker pyodideWorker.terminate(); clearConsole(); // Restart the worker and rebind event listener pyodideWorker = startPyodideWorker(); // Reset the robots to their initial state robots = createInitialRobots(); gameWorld.reset(robots["player"]); //gameWorld.addRobot(robots["player"]); // Clear the console consoleElement.innerHTML = ""; // Unpause the game if it was paused paused = false; document.getElementById("pause-button").innerText = "Pause"; logToConsole("Welcome to the game! Type your Python code in the editor and click 'Compile' to execute it."); } const targetFPS = 30; const targetInterval = 1000 / targetFPS; // Time in milliseconds per frame let lastFrameTime = 0; // ✅ Game Loop function gameLoop(timestamp) { const deltaTime = timestamp - lastFrameTime; // If enough time has passed since the last frame, update and draw if (deltaTime >= targetInterval) { lastFrameTime = timestamp; if (!paused) { ctx.resetTransform(); // Fill the entire visible canvas to remove artifacts ctx.fillStyle = "#DDD"; ctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height); ctx.translate(offsetX, offsetY); // Apply panning ctx.scale(scale, scale); // Apply zooming gameWorld.update(); gameWorld.draw(ctx); // if (gameWorld.checkPlayerCompletedTask()) { // logToConsole("✅ Task Completed! ✅"); // togglePause(); // gameWorld.currentLevel++; // } pyodideWorker.postMessage({ type: "game_state", state: gameWorld }); } } requestAnimationFrame(gameLoop); } // ✅ Update "distance" and "speed" every 2 seconds with random values function updateSensorData() { const distance = Math.random() * 100; // Random distance (0-100) const speed = Math.random() * 10; // Random speed (0-10) console.log(`Distance: ${distance.toFixed(2)}, Speed: ${speed.toFixed(2)}`); pyodideWorker.postMessage({ type: "sensor_update", data: { distance, speed } }); //logToConsole(`📡 Sensor Update - Distance: ${distance.toFixed(2)}, Speed: ${speed.toFixed(2)}`); } //setInterval(updateSensorData, 2000); // Call every 2 seconds document.getElementById("compile-button").addEventListener("click", () => { if (paused) return; document.getElementById('compile-button').disabled = true; resetGameWorld(); // Use the Monaco Editor instance to get the code let code = monacoEditor.getValue(); // Get text from the editor //console.log(code); code = code.replace(/time\.sleep\(/g, "await time.sleep("); //console.log(code); consoleElement.innerHTML = ""; pyodideWorker.postMessage({ type: "execute", code: code }); mostRecentCode = code; logToConsole("Compiling your code...", false); }); document.getElementById("pause-button").addEventListener("click", togglePause); document.getElementById("reset-button").addEventListener("click", resetGame); gameCanvas.addEventListener("wheel", (event) => { event.preventDefault(); const scaleFactor = 1.1; const mouseX = event.offsetX; const mouseY = event.offsetY; // Convert mouse coordinates to world coordinates (before zoom) const worldX = (mouseX - offsetX) / scale; const worldY = (mouseY - offsetY) / scale; // Apply zoom if (event.deltaY < 0) { scale *= scaleFactor; // Zoom in } else { scale /= scaleFactor; // Zoom out } // Keep zoom within limits scale = Math.max(0.5, Math.min(3, scale)); // Adjust offset so zooming is centered at mouse position offsetX = mouseX - worldX * scale; offsetY = mouseY - worldY * scale; }); gameCanvas.addEventListener("mousedown", (event) => { isPanning = true; startX = event.clientX - offsetX; startY = event.clientY - offsetY; }); gameCanvas.addEventListener("mousemove", (event) => { if (!isPanning) return; offsetX = event.clientX - startX; offsetY = event.clientY - startY; }); gameCanvas.addEventListener("mouseup", () => { isPanning = false; }); const textarea = document.getElementById('python-code'); // textarea.addEventListener('keydown', function (event) { // if (event.key === 'Tab') { // event.preventDefault(); // const start = this.selectionStart; // const end = this.selectionEnd; // this.value = this.value.substring(0, start) + ' ' + this.value.substring(end); // this.selectionStart = this.selectionEnd = start + 1; // } // }); // game.js export function initializeMonaco() { require.config({ paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/min/vs" } }); require(["vs/editor/editor.main"], function () { // Store the editor instance in the global variable monacoEditor = monaco.editor.create(document.getElementById("monaco-editor"), { value: "# Type your Python code here\n", language: "python", theme: "vs-dark", automaticLayout: true }); }); } function setupCanvas() { const canvas = document.getElementById("gameCanvas"); const context = canvas.getContext("2d"); // Get the device pixel ratio const dpr = window.devicePixelRatio || 1; // Adjust the canvas size for high-DPI displays const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; // Scale the drawing context context.scale(dpr, dpr); // Optional: Clear the canvas for better visuals context.clearRect(0, 0, canvas.width, canvas.height); } // Call this function when the page loads fetch('/data/levels.json') .then(response => response.json()) .then(data => { gameWorld.levelData = data; setupCanvas(); resetGame(); // Initialize the game and robots // Start game loop gameLoop(); });