BBC-micro / index.html
Luis-Filipe's picture
Update index.html
c3477a2 verified
<!DOCTYPE html>
<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>