onlinecodesimulator/game.js

617 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { Robot } from "./robot.js";
import { GameWorld } from "./gameworld.js";
import { lessons } from './data/lessons.js';
let mostRecentCode = ""; // The users 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);
if (lesson.map) {
console.log("Loading map for lesson:", lesson.map);
document.getElementById('gameCanvas').style.display = 'block';
for (let i = 0; i < gameWorld.levelData.length; i++) {
if (gameWorld.levelData[i].name === lesson.map) {
gameWorld.currentLevel = i;
console.log("Setting current level to:", i);
break;
}
}
resetGameWorld();
} else {
document.getElementById('gameCanvas').style.display = 'none';
}
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") || consoleText.includes("PythonError")) {
console.log(consoleText);
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
});
console.log(gameWorld.waypointsReached);
if (result.done) {
markLessonDone(lesson.id);
}
console.log(result);
//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
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");
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
consoleText = "";
}
// ✅ 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";
}
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.");
document.getElementById('compile-button').disabled = false;
}
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.waypointsReachedChanged()) {
console.log("Waypoints reached:", gameWorld.waypointsReached);
checkLessonDone();
}
// 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;
clearConsole();
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(");
code = code.replace(
/^([ \t]*)while True:\s*$/gm,
(_, indent) => `${indent}while True:\n${indent} await time.sleep(0.0001)`
);
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;
});
const canvas = document.getElementById("gameCanvas");
window.addEventListener("resize", () => {
// console.log("RESIZE");
// gameWorld.resizeCanvas(canvas)
});
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();
showLesson(10);
});