529 lines
15 KiB
JavaScript
529 lines
15 KiB
JavaScript
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);
|
||
|
||
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;
|
||
}
|
||
|
||
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
|
||
});
|
||
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(6);
|
||
|
||
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(`<span style="color:red;">${event.data.message}</span>`);
|
||
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");
|
||
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("<b>🔥 Gun Fired! 🔥</b>");
|
||
}
|
||
|
||
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";
|
||
}
|
||
|
||
// ✅ 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;
|
||
|
||
// 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();
|
||
});
|
||
|
||
|
||
|
||
|