Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Acorn BBC Micro - BBC BASIC</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;700@display=swap'); | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| background: #000000; | |
| color: #ffff00; | |
| font-family: 'Source Code Pro', monospace; | |
| font-size: 18px; | |
| line-height: 1.2; | |
| overflow: hidden; | |
| height: 100vh; | |
| width: 100vw; | |
| } | |
| #screen { | |
| width: 100vw; | |
| height: 100vh; | |
| background: #000000; | |
| color: #ffff00; | |
| font-family: 'Source Code Pro', monospace; | |
| font-size: 18px; | |
| padding: 20px; | |
| position: relative; | |
| white-space: pre; | |
| cursor: none; | |
| outline: none; | |
| border: none; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| #text-display { | |
| flex: 1; | |
| white-space: pre; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| #cursor { | |
| display: inline-block; | |
| width: 12px; | |
| height: 22px; | |
| background: #ffff00; | |
| animation: blink 1s infinite; | |
| position: absolute; | |
| } | |
| @keyframes blink { | |
| 0%, 50% { opacity: 1; } | |
| 51%, 100% { opacity: 0; } | |
| } | |
| .input-line { | |
| display: flex; | |
| } | |
| .prompt { | |
| color: #ffff00; | |
| } | |
| .error { | |
| color: #ff4444; | |
| } | |
| /* Scan lines effect for authentic CRT look */ | |
| #screen::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: repeating-linear-gradient( | |
| 0deg, | |
| transparent, | |
| transparent 2px, | |
| rgba(255, 255, 255, 0.02) 2px, | |
| rgba(255, 255, 255, 0.02) 4px | |
| ); | |
| pointer-events: none; | |
| z-index: 1; | |
| } | |
| /* Subtle glow effect */ | |
| #text-display { | |
| text-shadow: 0 0 3px #ffff00; | |
| } | |
| /* Responsive font sizing */ | |
| @media (max-width: 1200px) { | |
| body { font-size: 16px; } | |
| #screen { font-size: 16px; padding: 15px; } | |
| #cursor { width: 10px; height: 19px; } | |
| } | |
| @media (max-width: 800px) { | |
| body { font-size: 14px; } | |
| #screen { font-size: 14px; padding: 10px; } | |
| #cursor { width: 9px; height: 17px; } | |
| } | |
| @media (max-width: 600px) { | |
| body { font-size: 12px; } | |
| #screen { font-size: 12px; padding: 8px; } | |
| #cursor { width: 8px; height: 15px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="screen" tabindex="0"> | |
| <div id="text-display"></div> | |
| <div id="cursor"></div> | |
| </div> | |
| <script> | |
| class BBCMicro { | |
| constructor() { | |
| this.screen = document.getElementById('text-display'); | |
| this.cursor = document.getElementById('cursor'); | |
| this.screenElement = document.getElementById('screen'); | |
| // Calculate screen dimensions based on viewport | |
| this.calculateScreenDimensions(); | |
| // Screen buffer | |
| this.screenBuffer = Array(this.screenHeight).fill(null).map(() => | |
| Array(this.screenWidth).fill(' ') | |
| ); | |
| // Cursor position | |
| this.cursorX = 0; | |
| this.cursorY = 0; | |
| // Input handling | |
| this.inputLine = ''; | |
| this.inputMode = false; | |
| // BBC BASIC interpreter | |
| this.variables = new Map(); | |
| this.program = new Map(); | |
| this.running = false; | |
| this.stopped = false; | |
| this.currentLine = 0; | |
| this.forLoops = []; | |
| this.inputCallback = null; | |
| // Functions and Procedures | |
| this.functions = new Map(); | |
| this.procedures = new Map(); | |
| // Built-in functions | |
| this.builtInFunctions = { | |
| 'ABS': Math.abs, | |
| 'INT': Math.floor, | |
| 'SQR': Math.sqrt, | |
| 'SIN': Math.sin, | |
| 'COS': Math.cos, | |
| 'RND': () => Math.random(), | |
| 'LEN': (s) => String(s).length, | |
| 'ASC': (s) => String(s).charCodeAt(0), | |
| 'CHR$': (n) => String.fromCharCode(n), | |
| 'STR$': (n) => String(n), | |
| 'VAL': (s) => parseFloat(s) || 0 | |
| }; | |
| this.initializeKeyboard(); | |
| this.initializeScreen(); | |
| this.showBootMessage(); | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| this.calculateScreenDimensions(); | |
| this.updateDisplay(); | |
| this.updateCursor(); | |
| }); | |
| } | |
| calculateScreenDimensions() { | |
| const fontSize = parseInt(window.getComputedStyle(this.screenElement).fontSize); | |
| const padding = parseInt(window.getComputedStyle(this.screenElement).paddingLeft) * 2; | |
| this.charWidth = fontSize * 0.6; // Approximate character width | |
| this.charHeight = fontSize * 1.2; // Line height | |
| this.screenWidth = Math.floor((window.innerWidth - padding) / this.charWidth); | |
| this.screenHeight = Math.floor((window.innerHeight - padding) / this.charHeight); | |
| // Minimum dimensions | |
| this.screenWidth = Math.max(40, this.screenWidth); | |
| this.screenHeight = Math.max(20, this.screenHeight); | |
| } | |
| initializeKeyboard() { | |
| this.screenElement.addEventListener('keydown', (e) => { | |
| e.preventDefault(); | |
| this.handleKeyPress(e); | |
| }); | |
| // Focus management | |
| this.screenElement.focus(); | |
| document.addEventListener('click', () => { | |
| this.screenElement.focus(); | |
| }); | |
| } | |
| initializeScreen() { | |
| this.clearScreen(); | |
| this.updateDisplay(); | |
| this.updateCursor(); | |
| } | |
| handleKeyPress(e) { | |
| if (this.inputCallback) { | |
| if (e.key === 'Enter') { | |
| const input = this.inputLine; | |
| this.inputLine = ''; | |
| this.inputMode = false; | |
| this.newLine(); | |
| this.inputCallback(input); | |
| this.inputCallback = null; | |
| return; | |
| } | |
| } | |
| switch (e.key) { | |
| case 'Enter': | |
| this.processInputLine(); | |
| break; | |
| case 'Backspace': | |
| this.handleBackspace(); | |
| break; | |
| case 'Escape': | |
| this.handleEscape(); | |
| break; | |
| default: | |
| if (e.key.length === 1) { | |
| this.addToInputLine(e.key); | |
| } | |
| break; | |
| } | |
| } | |
| addToInputLine(char) { | |
| this.inputLine += char; | |
| this.printChar(char); | |
| } | |
| handleBackspace() { | |
| if (this.inputLine.length > 0) { | |
| this.inputLine = this.inputLine.slice(0, -1); | |
| this.backspace(); | |
| } | |
| } | |
| handleEscape() { | |
| if (this.running) { | |
| this.running = false; | |
| this.stopped = true; | |
| this.newLine(); | |
| this.printLine('Escape at line ' + this.currentLine); | |
| this.showPrompt(); | |
| } | |
| } | |
| processInputLine() { | |
| const line = this.inputLine.trim(); | |
| this.inputLine = ''; | |
| this.newLine(); | |
| if (!line) { | |
| this.showPrompt(); | |
| return; | |
| } | |
| try { | |
| this.processCommand(line); | |
| } catch (error) { | |
| this.printLine('Syntax error'); | |
| } | |
| if (!this.running && !this.inputCallback) { | |
| this.showPrompt(); | |
| } | |
| } | |
| processCommand(command) { | |
| const upperCommand = command.toUpperCase(); | |
| // Check if it's a numbered line | |
| const lineMatch = command.match(/^\s*(\d+)\s*(.*)/); | |
| if (lineMatch) { | |
| const lineNum = parseInt(lineMatch[1]); | |
| const code = lineMatch[2].trim(); | |
| if (code === '') { | |
| this.program.delete(lineNum); | |
| } else { | |
| this.program.set(lineNum, code); | |
| } | |
| return; | |
| } | |
| // Immediate commands | |
| if (upperCommand === 'RUN') { | |
| this.runProgram(); | |
| } else if (upperCommand === 'LIST') { | |
| this.listProgram(); | |
| } else if (upperCommand === 'NEW') { | |
| this.newProgram(); | |
| } else if (upperCommand === 'CLS') { | |
| this.clearScreen(); | |
| this.updateDisplay(); | |
| } else if (upperCommand.startsWith('PRINT ') || upperCommand === 'PRINT') { | |
| this.executePrint(command.substring(5).trim()); | |
| } else if (command.includes('=') && !command.includes('==') && !command.includes('<=') && !command.includes('>=') && !command.includes('<>')) { | |
| this.executeAssignment(command); | |
| } else if (upperCommand.startsWith('INPUT ')) { | |
| this.executeInput(command.substring(6).trim()); | |
| } else if (upperCommand.startsWith('DEF FN')) { | |
| this.defineFunction(command.substring(4).trim()); | |
| } else if (upperCommand.startsWith('DEF PROC')) { | |
| this.defineProcedure(command.substring(4).trim()); | |
| } else { | |
| throw new Error('Syntax error'); | |
| } | |
| } | |
| defineFunction(defLine) { | |
| // DEF FNname(variables) = expression | |
| const match = defLine.match(/^FN([A-Za-z][A-Za-z0-9]*)\s*\(([^)]*)\)\s*=\s*(.+)$/i); | |
| if (match) { | |
| const funcName = match[1]; | |
| const params = match[2].split(',').map(p => p.trim()).filter(p => p); | |
| const expression = match[3]; | |
| this.functions.set(funcName.toUpperCase(), { | |
| params: params, | |
| expression: expression, | |
| type: 'function' | |
| }); | |
| } else { | |
| throw new Error('Syntax error in function definition'); | |
| } | |
| } | |
| defineProcedure(defLine) { | |
| // DEF PROCname(variables) | |
| const match = defLine.match(/^PROC([A-Za-z][A-Za-z0-9]*)\s*\(([^)]*)\)\s*$/i); | |
| if (match) { | |
| const procName = match[1]; | |
| const params = match[2].split(',').map(p => p.trim()).filter(p => p); | |
| // Find the procedure body in the program | |
| const procLines = []; | |
| let foundProc = false; | |
| let endLine = -1; | |
| const sortedLines = Array.from(this.program.entries()).sort((a, b) => a[0] - b[0]); | |
| for (const [lineNum, code] of sortedLines) { | |
| if (code.toUpperCase().startsWith(`DEF PROC${procName.toUpperCase()}`)) { | |
| foundProc = true; | |
| continue; | |
| } | |
| if (foundProc) { | |
| if (code.toUpperCase() === 'ENDPROC') { | |
| endLine = lineNum; | |
| break; | |
| } | |
| procLines.push([lineNum, code]); | |
| } | |
| } | |
| if (foundProc && endLine !== -1) { | |
| this.procedures.set(procName.toUpperCase(), { | |
| params: params, | |
| lines: procLines | |
| }); | |
| } else { | |
| throw new Error('Procedure not properly defined'); | |
| } | |
| } else { | |
| throw new Error('Syntax error in procedure definition'); | |
| } | |
| } | |
| callFunction(funcName, args) { | |
| const func = this.functions.get(funcName.toUpperCase()); | |
| if (!func) { | |
| throw new Error('Function not defined'); | |
| } | |
| // Save current variables | |
| const savedVariables = new Map(this.variables); | |
| // Set parameters | |
| for (let i = 0; i < func.params.length; i++) { | |
| const paramName = func.params[i].toUpperCase(); | |
| const argValue = args[i] !== undefined ? args[i] : 0; | |
| this.variables.set(paramName, argValue); | |
| } | |
| // Evaluate expression | |
| const result = this.evaluateExpression(func.expression); | |
| // Restore variables | |
| this.variables = savedVariables; | |
| return result; | |
| } | |
| callProcedure(procName, args, callback) { | |
| const proc = this.procedures.get(procName.toUpperCase()); | |
| if (!proc) { | |
| throw new Error('Procedure not defined'); | |
| } | |
| // Save current variables | |
| const savedVariables = new Map(this.variables); | |
| // Set parameters | |
| for (let i = 0; i < proc.params.length; i++) { | |
| const paramName = proc.params[i].toUpperCase(); | |
| const argValue = args[i] !== undefined ? args[i] : 0; | |
| this.variables.set(paramName, argValue); | |
| } | |
| // Execute procedure lines | |
| this.executeProgramLines(proc.lines, 0, () => { | |
| // Restore variables when procedure completes | |
| this.variables = savedVariables; | |
| if (callback) callback(); | |
| }); | |
| } | |
| runProgram() { | |
| this.running = true; | |
| this.stopped = false; | |
| this.variables.clear(); | |
| this.forLoops = []; | |
| const sortedLines = Array.from(this.program.entries()).sort((a, b) => a[0] - b[0]); | |
| this.executeProgramLines(sortedLines, 0); | |
| } | |
| executeProgramLines(lines, index, callback) { | |
| if (this.stopped || !this.running || index >= lines.length) { | |
| this.running = false; | |
| if (callback) { | |
| callback(); | |
| } else if (!this.inputCallback) { | |
| this.showPrompt(); | |
| } | |
| return; | |
| } | |
| const [lineNum, code] = lines[index]; | |
| this.currentLine = lineNum; | |
| try { | |
| const result = this.executeStatement(code, () => { | |
| setTimeout(() => this.executeProgramLines(lines, index + 1, callback), 1); | |
| }); | |
| if (result && result.goto !== undefined) { | |
| const nextIndex = lines.findIndex(([num]) => num >= result.goto); | |
| if (nextIndex !== -1) { | |
| setTimeout(() => this.executeProgramLines(lines, nextIndex, callback), 1); | |
| } else { | |
| this.running = false; | |
| if (callback) { | |
| callback(); | |
| } else if (!this.inputCallback) { | |
| this.showPrompt(); | |
| } | |
| } | |
| } else if (!this.inputCallback) { | |
| setTimeout(() => this.executeProgramLines(lines, index + 1, callback), 1); | |
| } | |
| } catch (error) { | |
| this.printLine('Error in line ' + lineNum); | |
| this.running = false; | |
| if (callback) { | |
| callback(); | |
| } else if (!this.inputCallback) { | |
| this.showPrompt(); | |
| } | |
| } | |
| } | |
| executeStatement(statement, callback) { | |
| const trimmed = statement.trim().toUpperCase(); | |
| if (trimmed.startsWith('PRINT ') || trimmed === 'PRINT') { | |
| this.executePrint(statement.substring(5).trim()); | |
| } else if (trimmed.startsWith('INPUT ')) { | |
| this.executeInput(statement.substring(6).trim(), callback); | |
| return; | |
| } else if (statement.includes('=') && !statement.includes('==') && !statement.includes('<=') && !statement.includes('>=') && !statement.includes('<>')) { | |
| this.executeAssignment(statement); | |
| } else if (trimmed.startsWith('IF ')) { | |
| return this.executeIf(statement.substring(3).trim()); | |
| } else if (trimmed.startsWith('GOTO ')) { | |
| return { goto: parseInt(statement.substring(5).trim()) }; | |
| } else if (trimmed.startsWith('FOR ')) { | |
| return this.executeFor(statement.substring(4).trim()); | |
| } else if (trimmed === 'NEXT') { | |
| return this.executeNext(); | |
| } else if (trimmed === 'CLS') { | |
| this.clearScreen(); | |
| this.updateDisplay(); | |
| } else if (trimmed === 'END') { | |
| this.running = false; | |
| } else if (trimmed === 'ENDPROC') { | |
| // End of procedure | |
| if (callback) callback(); | |
| return; | |
| } else if (trimmed.startsWith('PROC')) { | |
| // Procedure call | |
| const procMatch = statement.match(/^PROC([A-Za-z][A-Za-z0-9]*)\s*\(([^)]*)\)$/i); | |
| if (procMatch) { | |
| const procName = procMatch[1]; | |
| const argsStr = procMatch[2]; | |
| const args = argsStr ? argsStr.split(',').map(arg => this.evaluateExpression(arg.trim())) : []; | |
| this.callProcedure(procName, args, callback); | |
| return; | |
| } | |
| } else { | |
| throw new Error('Syntax error'); | |
| } | |
| if (callback) callback(); | |
| } | |
| executePrint(expression) { | |
| if (!expression) { | |
| this.printLine(''); | |
| return; | |
| } | |
| try { | |
| // Handle semicolons and commas | |
| const parts = this.parsePrintExpression(expression); | |
| let output = ''; | |
| let newLine = true; | |
| for (let i = 0; i < parts.length; i++) { | |
| const part = parts[i]; | |
| if (part.type === 'expression') { | |
| const value = this.evaluateExpression(part.value); | |
| output += String(value); | |
| } else if (part.type === 'separator') { | |
| if (part.value === ',') { | |
| output += '\t'; | |
| } else if (part.value === ';') { | |
| if (i === parts.length - 1) { | |
| newLine = false; | |
| } | |
| } | |
| } | |
| } | |
| if (newLine) { | |
| this.printLine(output); | |
| } else { | |
| this.printText(output); | |
| } | |
| } catch (error) { | |
| this.printLine('Syntax error'); | |
| } | |
| } | |
| parsePrintExpression(expr) { | |
| const parts = []; | |
| let current = ''; | |
| let inString = false; | |
| let parenLevel = 0; | |
| for (let i = 0; i < expr.length; i++) { | |
| const char = expr[i]; | |
| if (char === '"' && (i === 0 || expr[i-1] !== '\\')) { | |
| inString = !inString; | |
| current += char; | |
| } else if (!inString) { | |
| if (char === '(') parenLevel++; | |
| else if (char === ')') parenLevel--; | |
| if ((char === ',' || char === ';') && parenLevel === 0) { | |
| if (current.trim()) { | |
| parts.push({ type: 'expression', value: current.trim() }); | |
| } | |
| parts.push({ type: 'separator', value: char }); | |
| current = ''; | |
| } else { | |
| current += char; | |
| } | |
| } else { | |
| current += char; | |
| } | |
| } | |
| if (current.trim()) { | |
| parts.push({ type: 'expression', value: current.trim() }); | |
| } | |
| return parts; | |
| } | |
| executeInput(varName, callback) { | |
| this.printText('? '); | |
| this.inputCallback = (input) => { | |
| const numValue = parseFloat(input); | |
| this.variables.set(varName.toUpperCase(), isNaN(numValue) ? input : numValue); | |
| if (callback) callback(); | |
| }; | |
| } | |
| executeAssignment(statement) { | |
| const match = statement.match(/^\s*([A-Za-z][A-Za-z0-9]*[$%]?)\s*=\s*(.+)/); | |
| if (match) { | |
| const varName = match[1].toUpperCase(); | |
| const value = this.evaluateExpression(match[2]); | |
| this.variables.set(varName, value); | |
| } else { | |
| throw new Error('Syntax error'); | |
| } | |
| } | |
| executeIf(statement) { | |
| const match = statement.match(/^(.+?)\s+THEN\s+(.+)$/i); | |
| if (match) { | |
| const condition = this.evaluateExpression(match[1]); | |
| if (condition) { | |
| const thenPart = match[2]; | |
| if (thenPart.match(/^\d+$/)) { | |
| return { goto: parseInt(thenPart) }; | |
| } else { | |
| return this.executeStatement(thenPart); | |
| } | |
| } | |
| } | |
| } | |
| executeFor(statement) { | |
| const match = statement.match(/^([A-Za-z][A-Za-z0-9]*)\s*=\s*(.+?)\s+TO\s+(.+?)(?:\s+STEP\s+(.+))?$/i); | |
| if (match) { | |
| const varName = match[1].toUpperCase(); | |
| const startValue = this.evaluateExpression(match[2]); | |
| const endValue = this.evaluateExpression(match[3]); | |
| const stepValue = match[4] ? this.evaluateExpression(match[4]) : 1; | |
| this.variables.set(varName, startValue); | |
| this.forLoops.push({ | |
| variable: varName, | |
| end: endValue, | |
| step: stepValue, | |
| line: this.currentLine | |
| }); | |
| } else { | |
| throw new Error('Syntax error'); | |
| } | |
| } | |
| executeNext() { | |
| if (this.forLoops.length === 0) { | |
| throw new Error('NEXT without FOR'); | |
| } | |
| const loop = this.forLoops[this.forLoops.length - 1]; | |
| const currentValue = this.variables.get(loop.variable); | |
| const nextValue = currentValue + loop.step; | |
| if ((loop.step > 0 && nextValue <= loop.end) || | |
| (loop.step < 0 && nextValue >= loop.end)) { | |
| this.variables.set(loop.variable, nextValue); | |
| return { goto: loop.line }; | |
| } else { | |
| this.forLoops.pop(); | |
| } | |
| } | |
| evaluateExpression(expr) { | |
| expr = expr.trim(); | |
| // String literal | |
| if (expr.startsWith('"') && expr.endsWith('"')) { | |
| return expr.slice(1, -1); | |
| } | |
| // Number | |
| if (/^-?\d*\.?\d+$/.test(expr)) { | |
| return parseFloat(expr); | |
| } | |
| // Variable | |
| if (/^[A-Za-z][A-Za-z0-9]*[$%]?$/.test(expr)) { | |
| const varName = expr.toUpperCase(); | |
| return this.variables.get(varName) || 0; | |
| } | |
| // Function call | |
| const funcMatch = expr.match(/^FN([A-Za-z][A-Za-z0-9]*)\s*\(([^)]*)\)$/i); | |
| if (funcMatch) { | |
| const funcName = funcMatch[1]; | |
| const argsStr = funcMatch[2]; | |
| const args = argsStr ? argsStr.split(',').map(arg => this.evaluateExpression(arg.trim())) : []; | |
| return this.callFunction(funcName, args); | |
| } | |
| // Built-in function | |
| const builtInMatch = expr.match(/^([A-Za-z]+\$?)\s*\(\s*(.+?)\s*\)$/); | |
| if (builtInMatch) { | |
| const funcName = builtInMatch[1].toUpperCase(); | |
| const argExpr = builtInMatch[2]; | |
| const argValue = this.evaluateExpression(argExpr); | |
| if (this.builtInFunctions[funcName]) { | |
| return this.builtInFunctions[funcName](argValue); | |
| } else { | |
| throw new Error('Unknown function'); | |
| } | |
| } | |
| return this.evaluateComplexExpression(expr); | |
| } | |
| evaluateComplexExpression(expr) { | |
| // Handle parentheses first | |
| while (expr.includes('(')) { | |
| const match = expr.match(/\(([^()]+)\)/); | |
| if (match) { | |
| const result = this.evaluateExpression(match[1]); | |
| expr = expr.replace(match[0], result); | |
| } else { | |
| break; | |
| } | |
| } | |
| // Comparison operators | |
| for (const op of ['<=', '>=', '<>', '=', '<', '>']) { | |
| const index = expr.lastIndexOf(op); | |
| if (index > 0) { | |
| const left = this.evaluateExpression(expr.substring(0, index)); | |
| const right = this.evaluateExpression(expr.substring(index + op.length)); | |
| switch (op) { | |
| case '=': return left == right ? -1 : 0; | |
| case '<>': return left != right ? -1 : 0; | |
| case '<': return left < right ? -1 : 0; | |
| case '>': return left > right ? -1 : 0; | |
| case '<=': return left <= right ? -1 : 0; | |
| case '>=': return left >= right ? -1 : 0; | |
| } | |
| } | |
| } | |
| // Arithmetic operators (right to left for same precedence) | |
| for (const op of ['+', '-']) { | |
| const index = expr.lastIndexOf(op); | |
| if (index > 0) { | |
| const left = this.evaluateExpression(expr.substring(0, index)); | |
| const right = this.evaluateExpression(expr.substring(index + 1)); | |
| return op === '+' ? left + right : left - right; | |
| } | |
| } | |
| for (const op of ['*', '/']) { | |
| const index = expr.lastIndexOf(op); | |
| if (index > 0) { | |
| const left = this.evaluateExpression(expr.substring(0, index)); | |
| const right = this.evaluateExpression(expr.substring(index + 1)); | |
| return op === '*' ? left * right : left / right; | |
| } | |
| } | |
| if (expr.indexOf('^') > 0) { | |
| const index = expr.indexOf('^'); | |
| const left = this.evaluateExpression(expr.substring(0, index)); | |
| const right = this.evaluateExpression(expr.substring(index + 1)); | |
| return Math.pow(left, right); | |
| } | |
| throw new Error('Syntax error'); | |
| } | |
| listProgram() { | |
| const sortedLines = Array.from(this.program.entries()).sort((a, b) => a[0] - b[0]); | |
| for (const [lineNum, code] of sortedLines) { | |
| this.printLine(` ${lineNum} ${code}`); | |
| } | |
| } | |
| newProgram() { | |
| this.program.clear(); | |
| this.variables.clear(); | |
| this.functions.clear(); | |
| this.procedures.clear(); | |
| this.printLine(''); | |
| } | |
| showBootMessage() { | |
| this.printLine(''); | |
| this.printLine('BBC Computer 32K'); | |
| this.printLine(''); | |
| this.printLine('BASIC'); | |
| this.printLine(''); | |
| this.showPrompt(); | |
| } | |
| showPrompt() { | |
| this.printText('>'); | |
| } | |
| clearScreen() { | |
| this.screenBuffer = Array(this.screenHeight).fill(null).map(() => | |
| Array(this.screenWidth).fill(' ') | |
| ); | |
| this.cursorX = 0; | |
| this.cursorY = 0; | |
| } | |
| printChar(char) { | |
| if (char === '\n' || this.cursorX >= this.screenWidth) { | |
| this.newLine(); | |
| if (char === '\n') return; | |
| } | |
| if (this.cursorY >= this.screenHeight) { | |
| this.scrollUp(); | |
| this.cursorY = this.screenHeight - 1; | |
| } | |
| this.screenBuffer[this.cursorY][this.cursorX] = char; | |
| this.cursorX++; | |
| this.updateDisplay(); | |
| this.updateCursor(); | |
| } | |
| printText(text) { | |
| for (const char of text) { | |
| this.printChar(char); | |
| } | |
| } | |
| printLine(text) { | |
| this.printText(text); | |
| this.newLine(); | |
| } | |
| newLine() { | |
| this.cursorX = 0; | |
| this.cursorY++; | |
| if (this.cursorY >= this.screenHeight) { | |
| this.scrollUp(); | |
| this.cursorY = this.screenHeight - 1; | |
| } | |
| this.updateDisplay(); | |
| this.updateCursor(); | |
| } | |
| backspace() { | |
| if (this.cursorX > 0) { | |
| this.cursorX--; | |
| this.screenBuffer[this.cursorY][this.cursorX] = ' '; | |
| this.updateDisplay(); | |
| this.updateCursor(); | |
| } | |
| } | |
| scrollUp() { | |
| for (let y = 0; y < this.screenHeight - 1; y++) { | |
| this.screenBuffer[y] = [...this.screenBuffer[y + 1]]; | |
| } | |
| this.screenBuffer[this.screenHeight - 1] = Array(this.screenWidth).fill(' '); | |
| this.updateDisplay(); | |
| } | |
| updateDisplay() { | |
| let display = ''; | |
| for (let y = 0; y < this.screenHeight; y++) { | |
| display += this.screenBuffer[y].join('').trimEnd() + '\n'; | |
| } | |
| this.screen.textContent = display; | |
| } | |
| updateCursor() { | |
| const charWidth = parseFloat(window.getComputedStyle(this.screenElement).fontSize) * 0.6; | |
| const lineHeight = parseFloat(window.getComputedStyle(this.screenElement).fontSize) * 1.2; | |
| const padding = 20; | |
| // Position cursor at the bottom of the text line | |
| this.cursor.style.left = (this.cursorX * charWidth + padding) + 'px'; | |
| this.cursor.style.top = (this.cursorY * lineHeight + padding) + 'px'; | |
| } | |
| } | |
| // Initialize BBC Micro | |
| const bbcMicro = new BBCMicro(); | |
| </script> | |
| </body> | |
| </html> |