zkt25/index.html

850 lines
32 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Karel Simulator</title>
<style>
:root {
--bg: #121212;
--surface: #1e1e1e;
--border: #2a2a2a;
--text: #e0e0e0;
--accent: #0f0;
--wall: #d32f2f;
--hover: rgba(255,255,255,0.05);
--radius: 4px;
}
*,*::before,*::after{box-sizing:border-box;}
body {
margin:0;padding:20px;
background:var(--bg);
color:var(--text);
font-family:monospace;
line-height:1.4;
}
h1,h2,p,label{color:var(--text);}
#container,#mapEditorContainer {
display:flex;gap:20px;
}
#leftPane,#mapEditorControls{flex:1;max-width:600px;}
#rightPane,#editorMap{flex:1;}
#editor,#console,#editorMap,#map {
width:100%;
background:var(--surface);
border:1px solid var(--border);
border-radius:var(--radius);
padding:12px;
margin-bottom:20px;
white-space:pre;
color:var(--text);
font-family:monospace;
overflow:auto;
}
#editor{height:200px;}
#console{height:150px;color:var(--accent);white-space:pre-wrap;}
button,input[type="number"],select {
background:var(--surface);
border:1px solid var(--border);
border-radius:var(--radius);
color:var(--text);
padding:8px 16px;
font:inherit;
cursor:pointer;
transition:background 0.2s;
margin-right:10px;
}
button:hover,input[type="number"]:hover,select:hover {
background:var(--hover);
}
button:active{background:var(--border);}
input[type="file"]{
background:none;border:none;color:var(--text);
}
#map{
background:var(--surface);
border:1px solid var(--border);
color:var(--text);
}
#status{display:flex;gap:20px;margin-top:10px;}
.status-card{
background:var(--surface);
border:1px solid var(--border);
border-radius:var(--radius);
padding:8px 12px;
display:flex;align-items:center;
}
.status-card .label{color:var(--accent);margin-right:6px;}
.map-border{color:var(--accent);}
.cell-content{color:var(--accent);}
.wall-border{color:var(--wall);}
/* Editor spacing */
#mapEditorControls{display:flex;flex-direction:column;gap:16px;}
#mapEditorControls h2{margin-bottom:0;}
#mapEditorControls > label,
#mapEditorControls > input[type="number"],
#mapEditorControls > select,
#mapEditorControls > button {
margin:4px 0;
}
.edit-mode-group {
display:flex;
flex-direction:column;
gap:8px;
}
.clickable-wall{
display:inline-block;
transition:background 0.2s;
cursor:pointer;
}
.clickable-wall:hover{
background:var(--hover);
}
</style>
</head>
<body>
<h1>Karel Simulator</h1>
<!-- Main Simulator View -->
<div id="container">
<div id="leftPane">
<p>
Write your Karel program below. Parentheses &amp; semicolons mandatory.
If youre ever unsure how to use the Simulator,
<a href="manual.html" target="_blank" style="color:var(--accent);text-decoration:underline;">
consult the manual
</a>.
</p>
<textarea id="editor">// Example:
if (frontIsBlocked()) {
turnLeft();
} else {
move();
}
</textarea>
<button id="runButton">Compile &amp; Run</button>
<button id="stopButton">Stop</button>
<button id="editMapButton">Edit Map</button>
<h2>Console Output:</h2>
<div id="console"></div>
<h2>Upload Map File:</h2>
<input type="file" id="mapFileInput" accept=".txt,.kw"/>
<button id="uploadMapButton">Upload Map</button>
</div>
<div id="rightPane">
<h2>Karel Map:</h2>
<div id="map"></div>
<div id="status"></div>
</div>
</div>
<!-- Manual Map Editor -->
<div id="mapEditorContainer" style="display:none;">
<div id="mapEditorControls">
<h2>Manual Map Editor</h2>
<label>
Width:
<input type="number" id="editWidth" min="3" value="10"/>
</label>
<label>
Height:
<input type="number" id="editHeight" min="3" value="10"/>
</label>
<button id="generateMapButton">Generate Map</button>
<div class="edit-mode-group">
<label><input type="radio" name="editMode" value="wall" checked/> Toggle Wall</label>
<label><input type="radio" name="editMode" value="beeper"/> Beeper (click to add, Ctrl+click to remove)</label>
<label><input type="radio" name="editMode" value="karel"/> Set Karel</label>
</div>
<label>
Karel Dir:
<select id="karelDirSelect">
<option value="north">North</option>
<option value="east">East</option>
<option value="south">South</option>
<option value="west">West</option>
</select>
</label>
<!-- NEW: Beeper Bag Count -->
<label>
Beeper Bag:
<input type="number" id="beeperBagInput" min="0" max="99" value="0"/>
</label>
<button id="importEditorMapButton">Import to Simulator</button>
<button id="discardEditorMapButton">Discard Changes</button>
</div>
<div id="editorMap"></div>
</div>
<script>
// ───────── Globals & State ─────────
let uploadedMapData = null,
currentWorld, currentKarel,
editorMapData = null,
STEP_DELAY = 300,
stopRequested = false,
editMode = 'wall';
function log(msg){
document.getElementById('console').textContent += msg + '\n';
}
function sleep(ms){
return new Promise(r=>setTimeout(r,ms));
}
/**********************************************************************
* 1. LEXER
**********************************************************************/
class Token { constructor(type,value){ this.type=type; this.value=value; } }
const TokenType={ IDENTIFIER:'IDENTIFIER',KEYWORD:'KEYWORD',SYMBOL:'SYMBOL',EOF:'EOF' };
const keywords=new Set(['if','else','while']);
class Lexer {
constructor(input){ this.input=input; this.pos=0; }
skipWhitespace(){
while(this.pos<this.input.length && /\s/.test(this.input[this.pos]))
this.pos++;
}
nextToken(){
this.skipWhitespace();
if(this.pos>=this.input.length) return new Token(TokenType.EOF,'');
const ch=this.input[this.pos];
if(/[A-Za-z_]/.test(ch)){
let start=this.pos;
while(this.pos<this.input.length && /[A-Za-z0-9_]/.test(this.input[this.pos]))
this.pos++;
const w=this.input.slice(start,this.pos);
return new Token(keywords.has(w)?TokenType.KEYWORD:TokenType.IDENTIFIER,w);
}
if('(){};'.includes(ch)){
this.pos++;
return new Token(TokenType.SYMBOL,ch);
}
this.pos++;
return this.nextToken();
}
tokenize(){
const tokens=[];
let tok;
do{ tok=this.nextToken(); tokens.push(tok); }
while(tok.type!==TokenType.EOF);
return tokens;
}
}
/**********************************************************************
* 2. PARSER
**********************************************************************/
class ProgramNode{ constructor(stmts){ this.statements=stmts; } }
class ExpressionStatement{ constructor(expr){ this.expr=expr; } }
class IfStatement{ constructor(cond,thenB,elseB){
this.condition=cond; this.thenBlock=thenB; this.elseBlock=elseB;
}}
class WhileStatement{ constructor(cond,blk){ this.condition=cond; this.block=blk; } }
class BlockStatement{ constructor(stmts){ this.statements=stmts; } }
class FunctionCall{ constructor(name){ this.name=name; } }
class Parser {
constructor(tokens){ this.tokens=tokens; this.pos=0; }
cur(){ return this.tokens[this.pos]; }
eat(type,val=null){
const t=this.cur();
if(t.type!==type||(val&&t.value!==val))
throw new Error(`Unexpected token: ${t.value}`);
this.pos++;
return t;
}
parseProgram(){
const stmts=[];
while(this.cur().type!==TokenType.EOF){
stmts.push(this.parseStatement());
}
return new ProgramNode(stmts);
}
parseStatement(){
const t=this.cur();
if(t.type===TokenType.KEYWORD&&t.value==='if') return this.parseIf();
if(t.type===TokenType.KEYWORD&&t.value==='while') return this.parseWhile();
if(t.type===TokenType.SYMBOL&&t.value==='{') return this.parseBlock();
const expr=this.parseExpression();
this.eat(TokenType.SYMBOL,';');
return new ExpressionStatement(expr);
}
parseIf(){
this.eat(TokenType.KEYWORD,'if');
this.eat(TokenType.SYMBOL,'(');
const cond=this.parseExpression();
this.eat(TokenType.SYMBOL,')');
const thenB=this.parseStatement();
let elseB=null;
if(this.cur().type===TokenType.KEYWORD&&this.cur().value==='else'){
this.eat(TokenType.KEYWORD,'else');
elseB=this.parseStatement();
}
return new IfStatement(cond,thenB,elseB);
}
parseWhile(){
this.eat(TokenType.KEYWORD,'while');
this.eat(TokenType.SYMBOL,'(');
const cond=this.parseExpression();
this.eat(TokenType.SYMBOL,')');
const blk=this.parseStatement();
return new WhileStatement(cond,blk);
}
parseBlock(){
this.eat(TokenType.SYMBOL,'{');
const stmts=[];
while(!(this.cur().type===TokenType.SYMBOL&&this.cur().value==='}')){
stmts.push(this.parseStatement());
}
this.eat(TokenType.SYMBOL,'}');
return new BlockStatement(stmts);
}
parseExpression(){ return this.parseFunctionCall(); }
parseFunctionCall(){
const name=this.eat(TokenType.IDENTIFIER).value;
this.eat(TokenType.SYMBOL,'(');
this.eat(TokenType.SYMBOL,')');
return new FunctionCall(name);
}
}
/**********************************************************************
* 3. INTERPRETER (async + stop)
**********************************************************************/
async function executeProgramAsync(prog,ctx){
for(const stmt of prog.statements){
if(stopRequested) throw new Error("Execution stopped by user");
await executeStatementAsync(stmt,ctx);
}
}
async function executeStatementAsync(stmt,ctx){
if(stopRequested) throw new Error("Execution stopped by user");
if(stmt instanceof ExpressionStatement){
await evaluateExpressionAsync(stmt.expr,ctx);
} else if(stmt instanceof IfStatement){
if(await evaluateExpressionAsync(stmt.condition,ctx)){
await executeStatementAsync(stmt.thenBlock,ctx);
} else if(stmt.elseBlock){
await executeStatementAsync(stmt.elseBlock,ctx);
}
} else if(stmt instanceof WhileStatement){
while(await evaluateExpressionAsync(stmt.condition,ctx)){
if(stopRequested) throw new Error("Execution stopped by user");
await executeStatementAsync(stmt.block,ctx);
}
} else if(stmt instanceof BlockStatement){
for(const s of stmt.statements){
if(stopRequested) throw new Error("Execution stopped by user");
await executeStatementAsync(s,ctx);
}
}
}
async function evaluateExpressionAsync(expr,ctx){
if(stopRequested) throw new Error("Execution stopped by user");
if(expr instanceof FunctionCall){
const res = executeFunction(expr.name,ctx.karel);
if(['move','turnLeft','pickBeeper','putBeeper'].includes(expr.name)){
renderWorld(currentWorld,currentKarel);
await sleep(STEP_DELAY);
}
return res;
}
throw new Error('Unknown expression');
}
/**********************************************************************
* 4. WORLD & KAREL
**********************************************************************/
const DIRECTIONS={NORTH:0,EAST:1,SOUTH:2,WEST:3},
DIRECTION_SYMBOLS=['↑','→','↓','←'];
class World {
constructor(w,h){
this.width=w; this.height=h;
this.grid=[]; this.walls=[];
for(let x=0;x<w;x++){
this.grid[x]=[]; this.walls[x]=[];
for(let y=0;y<h;y++){
this.grid[x][y]=0;
this.walls[x][y]={north:false,east:false,south:false,west:false};
}
}
}
isWithinBounds(x,y){return x>=0&&x<this.width&&y>=0&&y<this.height;}
getBeeperCount(x,y){return this.isWithinBounds(x,y)?this.grid[x][y]:0;}
addBeeper(x,y){if(this.isWithinBounds(x,y))this.grid[x][y]++;}
removeBeeper(x,y){
if(this.isWithinBounds(x,y)&&this.grid[x][y]>0){this.grid[x][y]--;return true;}
return false;
}
}
class Karel {
constructor(world,x,y,dir){
this.world=world;this.x=x;this.y=y;this.direction=dir;this.beeperBag=0;
}
frontIsClear(){
const side=['north','east','south','west'][this.direction];
if(this.world.walls[this.x][this.y][side])return false;
const [nx,ny]=this.nextPosition(this.direction);
return this.world.isWithinBounds(nx,ny);
}
frontIsBlocked(){return !this.frontIsClear();}
leftIsClear(){
const d=(this.direction+3)%4,side=['north','east','south','west'][d];
if(this.world.walls[this.x][this.y][side])return false;
const [nx,ny]=this.nextPosition(d);
return this.world.isWithinBounds(nx,ny);
}
leftIsBlocked(){return !this.leftIsClear();}
rightIsClear(){
const d=(this.direction+1)%4,side=['north','east','south','west'][d];
if(this.world.walls[this.x][this.y][side])return false;
const [nx,ny]=this.nextPosition(d);
return this.world.isWithinBounds(nx,ny);
}
rightIsBlocked(){return !this.rightIsClear();}
nextToABeeper(){return this.world.getBeeperCount(this.x,this.y)>0;}
notNextToABeeper(){return !this.nextToABeeper();}
facingNorth(){return this.direction===DIRECTIONS.NORTH;}
notFacingNorth(){return !this.facingNorth();}
facingSouth(){return this.direction===DIRECTIONS.SOUTH;}
notFacingSouth(){return !this.facingSouth();}
facingEast(){return this.direction===DIRECTIONS.EAST;}
notFacingEast(){return !this.facingEast();}
facingWest(){return this.direction===DIRECTIONS.WEST;}
notFacingWest(){return !this.facingWest();}
anyBeepersInBeeperBag(){return this.beeperBag>0;}
noBeepersInBeeperBag(){return this.beeperBag===0;}
nextPosition(dir){
let nx=this.x,ny=this.y;
if(dir===DIRECTIONS.NORTH)ny++;
if(dir===DIRECTIONS.EAST) nx++;
if(dir===DIRECTIONS.SOUTH)ny--;
if(dir===DIRECTIONS.WEST) nx--;
return [nx,ny];
}
move(){
if(this.frontIsClear()){
[this.x,this.y]=this.nextPosition(this.direction);
log(`Moved to (${this.x}, ${this.y})`);
} else {
log("Cannot move; wall ahead.");
}
}
turnLeft(){
this.direction=(this.direction+3)%4;
log("Turned left. Now facing "+DIRECTION_SYMBOLS[this.direction]);
}
pickBeeper(){
if(this.world.getBeeperCount(this.x,this.y)>0){
this.world.removeBeeper(this.x,this.y);
this.beeperBag++;
log(`Picked up a beeper. Bag: ${this.beeperBag}`);
} else {
log("No beeper to pick up.");
}
}
putBeeper(){
if(this.beeperBag>0){
this.world.addBeeper(this.x,this.y);
this.beeperBag--;
log(`Placed a beeper. Bag: ${this.beeperBag}`);
} else {
log("No beeper in bag.");
}
}
}
/**********************************************************************
* 5. FUNCTION MAPPING
**********************************************************************/
function executeFunction(name,karel){
switch(name){
case 'move':return karel.move();
case 'turnLeft':return karel.turnLeft();
case 'pickBeeper':return karel.pickBeeper();
case 'putBeeper':return karel.putBeeper();
case 'frontIsClear':return karel.frontIsClear();
case 'frontIsBlocked':return karel.frontIsBlocked();
case 'leftIsClear':return karel.leftIsClear();
case 'leftIsBlocked':return karel.leftIsBlocked();
case 'rightIsClear':return karel.rightIsClear();
case 'rightIsBlocked':return karel.rightIsBlocked();
case 'nextToABeeper':return karel.nextToABeeper();
case 'notNextToABeeper':return karel.notNextToABeeper();
case 'facingNorth':return karel.facingNorth();
case 'notFacingNorth':return karel.notFacingNorth();
case 'facingSouth':return karel.facingSouth();
case 'notFacingSouth':return karel.notFacingSouth();
case 'facingEast':return karel.facingEast();
case 'notFacingEast':return karel.notFacingEast();
case 'facingWest':return karel.facingWest();
case 'notFacingWest':return karel.notFacingWest();
case 'anyBeepersInBeeperBag':return karel.anyBeepersInBeeperBag();
case 'noBeepersInBeeperBag':return karel.noBeepersInBeeperBag();
}
throw new Error(`Unknown function: ${name}`);
}
const context={karel:null,callFunction:n=>executeFunction(n,context.karel)};
/**********************************************************************
* 6. WORLD RENDERING
**********************************************************************/
function renderWorld(world,karel){
currentWorld=world;currentKarel=karel;
const w=world.width,h=world.height,total=w*4+1;
let out='';
// top
for(let i=0;i<total;i++){
out+= (i===0||i===total-1)
? '<span class="map-border">+</span>'
: '<span class="map-border">-</span>';
}
out+='<br>';
// rows
for(let y=h-1;y>=0;y--){
let row='';
for(let x=0;x<w;x++){
if(x===0) row+='<span class="map-border">|</span>';
else if(world.walls[x-1][y].east) row+='<span class="wall-border">|</span>';
else row+=' ';
let c='.';
if(karel.x===x&&karel.y===y) c=DIRECTION_SYMBOLS[karel.direction];
else {
const bc=world.getBeeperCount(x,y);
if(bc>0) c=bc<10?''+bc:'?';
}
row+=`<span class="cell-content"> ${c} </span>`;
}
row+='<span class="map-border">|</span><br>';
out+=row;
if(y>0){
let div='';
for(let x=0;x<w;x++){
if(x===0) div+='<span class="map-border">+</span>';
else div+=' ';
div+= world.walls[x][y].south
? '<span class="wall-border">---</span>'
: '<span class="map-border"> </span>';
}
div+='<span class="map-border">+</span><br>';
out+=div;
}
}
// bottom
let bot='';
for(let i=0;i<total;i++){
bot+= (i===0||i===total-1)
? '<span class="map-border">+</span>'
: '<span class="map-border">-</span>';
}
bot+='<br>';
out+=bot;
document.getElementById('map').innerHTML=out;
document.getElementById('status').innerHTML=
`<div class="status-card"><span class="label">Position:</span><span class="value">(${karel.x}, ${karel.y})</span></div>`+
`<div class="status-card"><span class="label">Beepers in Bag:</span><span class="value">${karel.beeperBag}</span></div>`;
}
/**********************************************************************
* 7. MANUAL MAP EDITOR
**********************************************************************/
function showMain(){
document.getElementById('container').style.display='flex';
document.getElementById('mapEditorContainer').style.display='none';
}
function showEditor(){
document.getElementById('container').style.display='none';
document.getElementById('mapEditorContainer').style.display='flex';
// sync mode
document.querySelectorAll('input[name="editMode"]').forEach(radio=>{
radio.addEventListener('change',()=>{editMode=radio.value;});
});
const md = uploadedMapData || {
width:3,height:3,
walls:[],beepers:[],
karelX:0,karelY:0,
karelDir:'east',karelBag:0
};
document.getElementById('editWidth').value=md.width;
document.getElementById('editHeight').value=md.height;
document.getElementById('karelDirSelect').value=md.karelDir;
// beeper bag input
const bagInput=document.getElementById('beeperBagInput');
bagInput.value=md.karelBag;
bagInput.onchange=()=>{
let v=parseInt(bagInput.value,10);
if(isNaN(v)||v<0)v=0;
if(v>99)v=99;
bagInput.value=v;
editorMapData.karelBag=v;
};
editorMapData=JSON.parse(JSON.stringify(md));
renderEditorMap();
}
function renderEditorMap(){
const w=editorMapData.width,h=editorMapData.height,total=w*4+1;
let out='';
// top border
for(let i=0;i<total;i++){
const ch=(i===0||i===total-1)?'+':'-';
out+=`<span class="map-border editor-border">${ch}</span>`;
}
out+='<br>';
// rows & toggles
for(let y=h-1;y>=0;y--){
let row='';
for(let x=0;x<w;x++){
if(x===0){
row+='<span class="map-border">|</span>';
} else {
const hasV=editorMapData.walls.some(wl=>wl.x===x-1&&wl.y===y&&wl.direction==='east');
row+=`<span class="clickable-wall ${hasV?'wall-border':'map-border'}"`
+` data-x="${x-1}" data-y="${y}" data-dir="east">`
+`${hasV?'|':' '}</span>`;
}
let c='.';
const bp=editorMapData.beepers.find(b=>b.x===x&&b.y===y);
if(bp)c=bp.count.toString();
if(editorMapData.karelX===x&&editorMapData.karelY===y){
c={'north':'↑','east':'→','south':'↓','west':'←'}[editorMapData.karelDir];
}
row+=`<span class="cell-content" data-x="${x}" data-y="${y}"> ${c} </span>`;
}
row+='<span class="map-border">|</span><br>';
out+=row;
if(y>0){
let div='';
for(let x=0;x<w;x++){
if(x===0)div+='<span class="map-border">+</span>';
else div+=' ';
const hasH=editorMapData.walls.some(wl=>wl.x===x&&wl.y===y&&wl.direction==='south');
div+=`<span class="clickable-wall ${hasH?'wall-border':'map-border'}"`
+` data-x="${x}" data-y="${y}" data-dir="south">`
+`${hasH?'---':' '}</span>`;
}
div+='<span class="map-border">+</span><br>';
out+=div;
}
}
// bottom border
for(let i=0;i<total;i++){
const ch=(i===0||i===total-1)?'+':'-';
out+=`<span class="map-border editor-border">${ch}</span>`;
}
out+='<br>';
document.getElementById('editorMap').innerHTML=out;
// bind wall toggles
document.querySelectorAll('#editorMap .clickable-wall').forEach(el=>{
el.onclick=()=>{
if(editMode!=='wall')return;
const x=+el.dataset.x,y=+el.dataset.y,d=el.dataset.dir;
const idx=editorMapData.walls.findIndex(wl=>wl.x===x&&wl.y===y&&wl.direction===d);
if(idx>=0)editorMapData.walls.splice(idx,1);
else editorMapData.walls.push({x,y,direction:d});
renderEditorMap();
};
});
// bind cell clicks
document.querySelectorAll('#editorMap .cell-content').forEach(el=>{
el.onclick=ev=>{
const x=+el.dataset.x,y=+el.dataset.y;
if(editMode==='wall'){
const d=document.getElementById('karelDirSelect').value;
const idx=editorMapData.walls.findIndex(wl=>wl.x===x&&wl.y===y&&wl.direction===d);
if(idx>=0)editorMapData.walls.splice(idx,1);
else editorMapData.walls.push({x,y,direction:d});
} else if(editMode==='beeper'){
let b=editorMapData.beepers.find(b=>b.x===x&&b.y===y);
if(ev.ctrlKey){
if(b){b.count--; if(b.count<=0)editorMapData.beepers=editorMapData.beepers.filter(z=>z!==b);}
} else {
if(!b)editorMapData.beepers.push({x,y,count:1});
else b.count++;
}
} else if(editMode==='karel'){
editorMapData.karelX=x;
editorMapData.karelY=y;
editorMapData.karelDir=document.getElementById('karelDirSelect').value;
}
renderEditorMap();
};
});
}
// Editor buttons
document.getElementById('editMapButton').onclick=showEditor;
document.getElementById('discardEditorMapButton').onclick=showMain;
document.getElementById('generateMapButton').onclick=()=>{
let w=parseInt(document.getElementById('editWidth').value,10),
h=parseInt(document.getElementById('editHeight').value,10);
if(isNaN(w)||w<3)w=3;
if(isNaN(h)||h<3)h=3;
document.getElementById('editWidth').value=w;
document.getElementById('editHeight').value=h;
editorMapData={
width:w,height:h,
walls:[],beepers:[],
karelX:0,karelY:0,
karelDir:document.getElementById('karelDirSelect').value,
karelBag:0
};
// reset bag input too
document.getElementById('beeperBagInput').value=0;
renderEditorMap();
};
// Import from editor
document.getElementById('importEditorMapButton').onclick=()=>{
uploadedMapData=JSON.parse(JSON.stringify(editorMapData));
const md=uploadedMapData, world=new World(md.width,md.height);
md.walls.forEach(w=>{
world.walls[w.x][w.y][w.direction]=true;
if(w.direction==='east'&&w.x+1<world.width)world.walls[w.x+1][w.y].west=true;
if(w.direction==='south'&&w.y-1>=0)world.walls[w.x][w.y-1].north=true;
});
md.beepers.forEach(b=>{for(let i=0;i<b.count;i++)world.addBeeper(b.x,b.y);});
const dirNum={north:1,east:2,south:3,west:4}[md.karelDir]-1;
const karel=new Karel(world,md.karelX,md.karelY,dirNum);
karel.beeperBag=md.karelBag;
currentWorld=world;currentKarel=karel;context.karel=karel;
renderWorld(world,karel);
showMain();
};
// Upload map file
document.getElementById('uploadMapButton').onclick=()=>{
const inp=document.getElementById('mapFileInput');
if(!inp.files.length){alert('Select a file.');return;}
const file=inp.files[0],ext=file.name.slice(file.name.lastIndexOf('.')).toLowerCase();
if(!['.txt','.kw'].includes(ext)){alert('Only .txt/.kw allowed.');return;}
const reader=new FileReader();
reader.onload=e=>{
try{
const md=parseMapFile(e.target.result);
uploadedMapData=md;
const world=new World(md.width,md.height);
md.walls.forEach(w=>{
world.walls[w.x][w.y][w.direction]=true;
if(w.direction==='east'&&w.x+1<world.width)world.walls[w.x+1][w.y].west=true;
if(w.direction==='south'&&w.y-1>=0)world.walls[w.x][w.y-1].north=true;
});
md.beepers.forEach(b=>{for(let i=0;i<b.count;i++)world.addBeeper(b.x,b.y);});
const dirNum={north:1,east:2,south:3,west:4}[md.karelDir]-1;
const karel=new Karel(world,md.karelX,md.karelY,dirNum);
karel.beeperBag=md.karelBag;
currentWorld=world;currentKarel=karel;context.karel=karel;
renderWorld(world,karel);
log('Map uploaded.');
}catch(err){
alert('Error parsing map: '+err.message);
}
};
reader.readAsText(file);
};
// Stop button
document.getElementById('stopButton').onclick=()=>{
stopRequested=true;
log('⏹ Execution stopped by user.');
};
/**********************************************************************
* 8. COMPILE & RUN
**********************************************************************/
document.getElementById('runButton').onclick=async()=>{
stopRequested=false;
document.getElementById('console').textContent='';
// build world & karel
let world,karel;
if(uploadedMapData){
const md=uploadedMapData;
world=new World(md.width,md.height);
md.walls.forEach(w=>{
world.walls[w.x][w.y][w.direction]=true;
if(w.direction==='east'&&w.x+1<world.width)world.walls[w.x+1][w.y].west=true;
if(w.direction==='south'&&w.y-1>=0)world.walls[w.x][w.y-1].north=true;
});
md.beepers.forEach(b=>{for(let i=0;i<b.count;i++)world.addBeeper(b.x,b.y);});
const dirNum={north:1,east:2,south:3,west:4}[md.karelDir]-1;
karel=new Karel(world,md.karelX,md.karelY,dirNum);
karel.beeperBag=md.karelBag;
} else {
world=new World(10,10);
karel=new Karel(world,0,0,DIRECTIONS.EAST);
}
currentWorld=world;currentKarel=karel;context.karel=karel;
renderWorld(world,karel);
// compile
log('=== Compilation ===');
let tokens,prog;
try{
tokens=new Lexer(document.getElementById('editor').value).tokenize();
prog=new Parser(tokens).parseProgram();
log('✅ Compilation successful.');
}catch(e){
return log(`⛔ Compilation error: ${e.message}`);
}
// execute
log('=== Execution ===');
try{
await executeProgramAsync(prog,context);
if(!stopRequested) log('✅ Execution complete.');
}catch(e){
if(e.message!=='Execution stopped by user'){
log(`⛔ Runtime error: ${e.message}`);
}
}
};
// Map-file parser (unchanged) ...
function parseMapFile(txt){
const lines=txt.split(/\r?\n/).map(l=>l.trim()).filter(l=>l);
if(!lines.length)throw new Error('File is empty.');
const hdr=lines[0].split(/\s+/);
if(hdr.length!==6)throw new Error('Header must have 6 tokens.');
const w=parseInt(hdr[0],10),h=parseInt(hdr[1],10),
x=parseInt(hdr[2],10)-1,y=parseInt(hdr[3],10)-1,
dir=hdr[4].toUpperCase(),bag=parseInt(hdr[5],10);
if([w,h,x+1,y+1,bag].some(n=>isNaN(n)))throw new Error('Invalid header.');
if(!['N','E','S','W'].includes(dir))throw new Error('Dir must be N/E/S/W.');
const mapDir={'N':'north','E':'east','S':'south','W':'west'};
const md={width:w,height:h,karelX:x,karelY:y,
karelDir:mapDir[dir],karelBag:bag,
walls:[],beepers:[]};
lines.slice(1).forEach((l,i)=>{
const p=l.split(/\s+/),t=p[0].toUpperCase();
if(t==='W'){
if(p.length!==4)throw new Error(`Line ${i+2}: W needs 4 tokens.`);
let xx=parseInt(p[1],10)-1,yy=parseInt(p[2],10)-1,d=p[3].toUpperCase();
if(isNaN(xx)||isNaN(yy)||!['N','E','S','W'].includes(d))
throw new Error(`Line ${i+2}: invalid wall.`);
if(d==='N'&&yy+1<h) md.walls.push({x:xx,y:yy+1,direction:'south'});
else if(d==='S') md.walls.push({x:xx,y:yy,direction:'south'});
else if(d==='E') md.walls.push({x:xx,y:yy,direction:'east'});
else if(d==='W'&&xx-1>=0)md.walls.push({x:xx-1,y:yy,direction:'east'});
}
else if(t==='B'){
if(p.length!==4)throw new Error(`Line ${i+2}: B needs 4 tokens.`);
let xx=parseInt(p[1],10)-1,yy=parseInt(p[2],10)-1,
c =parseInt(p[3],10);
if(isNaN(xx)||isNaN(yy)||isNaN(c))
throw new Error(`Line ${i+2}: invalid beeper.`);
md.beepers.push({x:xx,y:yy,count:c});
}
else throw new Error(`Line ${i+2}: must start W or B.`);
});
return md;
}
</script>
</body>
</html>