function BefungeBoard(source, constraints) {
constraints = constraints || {
width: 80,
height: 25
};
this.constraints = constraints;
this.grid = source.split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/).map(function (line) {
return (line + String.repeat(' ', constraints.width - line.length)).split('');
});
for (var i = this.grid.length; i < constraints.height; i++) {
this.grid[i] = String.repeat(' ', constraints.width).split('');
}
this.pointer = {
x: 0,
y: 0
};
this.direction = Direction.RIGHT;
}
BefungeBoard.prototype.nextPosition = function () {
var vector = this.direction.toVector(),
nextPosition = {
x: this.pointer.x + vector[0],
y: this.pointer.y + vector[1]
};
nextPosition.x = nextPosition.x < 0 ? this.constraints.width - 1 : nextPosition.x;
nextPosition.y = nextPosition.y < 0 ? this.constraints.height - 1 : nextPosition.y;
nextPosition.x = nextPosition.x >= this.constraints.width ? 0 : nextPosition.x;
nextPosition.y = nextPosition.y >= this.constraints.height ? 0 : nextPosition.y;
return nextPosition;
};
BefungeBoard.prototype.advance = function () {
this.pointer = this.nextPosition();
if (this.onAdvance) {
this.onAdvance.call(null, this.pointer);
}
};
BefungeBoard.prototype.currentToken = function () {
return this.grid[this.pointer.y][this.pointer.x];
};
BefungeBoard.prototype.nextToken = function () {
var nextPosition = this.nextPosition();
return this.grid[nextPosition.y][nextPosition.x];
};
var Direction = (function () {
var vectors = [
[1, 0],
[-1, 0],
[0, -1],
[0, 1]
];
function Direction(value) {
this.value = value;
}
Direction.prototype.toVector = function () {
return vectors[this.value];
};
return {
UP: new Direction(2),
DOWN: new Direction(3),
RIGHT: new Direction(0),
LEFT: new Direction(1)
};
})();
function BefungeStack() {
this.stack = [];
}
BefungeStack.prototype.pushAscii = function (item) {
this.pushNumber(item.charCodeAt());
};
BefungeStack.prototype.pushNumber = function (item) {
if (isNaN(+item)) {
throw new Error(typeof item + " | " + item + " is not a number");
}
this.stack.push(+item);
};
BefungeStack.prototype.popAscii = function () {
return String.fromCharCode(this.popNumber());
};
BefungeStack.prototype.popNumber = function () {
return this.stack.length === 0 ? 0 : this.stack.pop();
};
function Befunge(source, constraints) {
this.board = new BefungeBoard(source, constraints);
this.stack = new BefungeStack();
this.stringMode = false;
this.terminated = false;
this.digits = "0123456789".split('');
}
Befunge.prototype.run = function () {
for (var i = 1; i <= (this.stepsPerTick || 10); i++) {
this.step();
if (this.terminated) {
return;
}
}
requestAnimationFrame(this.run.bind(this));
};
Befunge.prototype.step = function () {
this.processCurrentToken();
this.board.advance();
};
Befunge.prototype.processCurrentToken = function () {
var token = this.board.currentToken();
if (this.stringMode && token !== '"') {
return this.stack.pushAscii(token);
}
if (this.digits.indexOf(token) !== -1) {
return this.stack.pushNumber(token);
}
switch (token) {
case ' ':
while ((token = this.board.nextToken()) == ' ') {
this.board.advance();
}
return;
case '+':
return this.stack.pushNumber(this.stack.popNumber() + this.stack.popNumber());
case '-':
return this.stack.pushNumber(-this.stack.popNumber() + this.stack.popNumber());
case '*':
return this.stack.pushNumber(this.stack.popNumber() * this.stack.popNumber());
case '/':
var denominator = this.stack.popNumber(),
numerator = this.stack.popNumber(),
result;
if (denominator === 0) {
result = +prompt("Illegal division by zero. Please enter the result to use:");
} else {
result = Math.floor(numerator / denominator);
}
return this.stack.pushNumber(result);
case '%':
var modulus = this.stack.popNumber(),
numerator = this.stack.popNumber(),
result;
if (modulus === 0) {
result = +prompt("Illegal division by zero. Please enter the result to use:");
} else {
result = Math.floor(numerator / modulus);
}
return this.stack.pushNumber(result);
case '!':
return this.stack.pushNumber(this.stack.popNumber() === 0 ? 1 : 0);
case '`':
return this.stack.pushNumber(this.stack.popNumber() < this.stack.popNumber() ? 1 : 0);
case '>':
this.board.direction = Direction.RIGHT;
return;
case '<':
this.board.direction = Direction.LEFT;
return;
case '^':
this.board.direction = Direction.UP;
return;
case 'v':
this.board.direction = Direction.DOWN;
return;
case '?':
this.board.direction = [Direction.RIGHT, Direction.UP, Direction.LEFT, Direction.DOWN][Math.floor(4 * Math.random())];
return;
case '_':
this.board.direction = this.stack.popNumber() === 0 ? Direction.RIGHT : Direction.LEFT;
return;
case '|':
this.board.direction = this.stack.popNumber() === 0 ? Direction.DOWN : Direction.UP;
return;
case '"':
this.stringMode = !this.stringMode;
return;
case ':':
var top = this.stack.popNumber();
this.stack.pushNumber(top);
return this.stack.pushNumber(top);
case '\\':
var first = this.stack.popNumber(),
second = this.stack.popNumber();
this.stack.pushNumber(first);
return this.stack.pushNumber(second);
case '$':
return this.stack.popNumber();
case '#':
return this.board.advance();
case 'p':
return this.board.grid[this.stack.popNumber()][this.stack.popNumber()] = this.stack.popAscii();
case 'g':
return this.stack.pushAscii(this.board.grid[this.stack.popNumber()][this.stack.popNumber()]);
case '&':
return this.stack.pushNumber(+prompt("Please enter a number:"));
case '~':
return this.stack.pushAscii(prompt("Please enter a character:")[0]);
case '.':
return this.print(this.stack.popNumber());
case ',':
return this.print(this.stack.popAscii());
case '@':
this.terminated = true;
return;
}
};
Befunge.prototype.withStdout = function (printer) {
this.print = printer;
return this;
};
Befunge.prototype.withOnAdvance = function (onAdvance) {
this.board.onAdvance = onAdvance;
return this;
};
String.repeat = function (str, count) {
var repeated = "";
for (var i = 1; i <= count; i++) {
repeated += str;
}
return repeated;
};
window['requestAnimationFrame'] = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) {
window.setTimeout(callback, 1000 / 60);
};
(function () {
var currentInstance = null;
function resetInstance() {
currentInstance = null;
}
function getOrCreateInstance() {
if (currentInstance !== null && currentInstance.terminated) {
resetInstance();
}
if (currentInstance === null) {
var boardSize = Editor.getBoardSize();
currentInstance = new Befunge(Editor.getSource(), {
width: boardSize.width,
height: boardSize.height
});
currentInstance.stepsPerTick = Editor.getStepsPerTick();
currentInstance.withStdout(Editor.append);
currentInstance.withOnAdvance(function (position) {
Editor.highlight(currentInstance.board.grid, position.x, position.y);
});
}
return currentInstance;
}
var Editor = (function (onExecute, onStep, onReset) {
var source = document.getElementById('source'),
sourceDisplay = document.getElementById('source-display'),
sourceDisplayWrapper = document.getElementById('source-display-wrapper'),
stdout = document.getElementById('stdout');
var execute = document.getElementById('execute'),
step = document.getElementById('step'),
reset = document.getElementById('reset');
var boardWidth = document.getElementById('board-width'),
boardHeight = document.getElementById('board-height'),
stepsPerTick = document.getElementById('steps-per-tick');
function showEditor() {
source.style.display = "block";
sourceDisplayWrapper.style.display = "none";
source.focus();
}
function hideEditor() {
source.style.display = "none";
sourceDisplayWrapper.style.display = "block";
var computedHeight = getComputedStyle(source).height;
sourceDisplayWrapper.style.minHeight = computedHeight;
sourceDisplayWrapper.style.maxHeight = computedHeight;
sourceDisplay.textContent = source.value;
}
function resetOutput() {
stdout.value = null;
}
function escapeEntities(input) {
return input.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
sourceDisplayWrapper.onclick = function () {
resetOutput();
showEditor();
onReset && onReset.call(null);
};
execute.onclick = function () {
resetOutput();
hideEditor();
onExecute && onExecute.call(null);
};
step.onclick = function () {
hideEditor();
onStep && onStep.call(null);
};
reset.onclick = function () {
resetOutput();
showEditor();
onReset && onReset.call(null);
};
return {
getSource: function () {
return source.value;
},
append: function (content) {
stdout.value = stdout.value + content;
},
highlight: function (grid, x, y) {
var highlighted = [];
for (var row = 0; row < grid.length; row++) {
highlighted[row] = [];
for (var column = 0; column < grid[row].length; column++) {
highlighted[row][column] = escapeEntities(grid[row][column]);
}
}
highlighted[y][x] = '<span class="activeToken">' + highlighted[y][x] + '</span>';
sourceDisplay.innerHTML = highlighted.map(function (lineTokens) {
return lineTokens.join('');
}).join('\n');
},
getBoardSize: function () {
return {
width: +boardWidth.innerHTML,
height: +boardHeight.innerHTML
};
},
getStepsPerTick: function () {
return +stepsPerTick.innerHTML;
}
};
})(function () {
getOrCreateInstance().run();
}, function () {
getOrCreateInstance().step();
}, resetInstance);
})();
.container {
width: 100%;
}
.so-box {
font-family:'Helvetica Neue', Arial, sans-serif;
font-weight: bold;
color: #fff;
text-align: center;
padding: .3em .7em;
font-size: 1em;
line-height: 1.1;
border: 1px solid #c47b07;
-webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3), 0 2px 0 rgba(255, 255, 255, 0.15) inset;
text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
background: #f88912;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3), 0 2px 0 rgba(255, 255, 255, 0.15) inset;
}
.control {
display: inline-block;
border-radius: 6px;
float: left;
margin-right: 25px;
cursor: pointer;
}
.option {
padding: 10px 20px;
margin-right: 25px;
float: left;
}
input, textarea {
box-sizing: border-box;
}
textarea {
display: block;
white-space: pre;
overflow: auto;
height: 75px;
width: 100%;
max-width: 100%;
min-height: 25px;
}
span[contenteditable] {
padding: 2px 6px;
background: #cc7801;
color: #fff;
}
#controls-container, #options-container {
height: auto;
padding: 6px 0;
}
#stdout {
height: 50px;
}
#reset {
float: right;
}
#source-display-wrapper {
display: none;
width: 100%;
height: 100%;
overflow: auto;
border: 1px solid black;
box-sizing: border-box;
}
#source-display {
font-family: monospace;
white-space: pre;
padding: 2px;
}
.activeToken {
background: #f88912;
}
.clearfix:after {
content:".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.clearfix {
display: inline-block;
}
* html .clearfix {
height: 1%;
}
.clearfix {
display: block;
}
<div class="container">
<textarea id="source" placeholder="Enter your Befunge-93 program here" wrap="off">&> :#v_,>:#,_@
^-1:<</textarea>
<div id="source-display-wrapper">
<div id="source-display"></div>
</div>
</div>
<div id="controls-container" class="container clearfix">
<input type="button" id="execute" class="control so-box" value="► Execute" />
<input type="button" id="step" class="control so-box" value="Step" />
<input type="button" id="reset" class="control so-box" value="Reset" />
</div>
<div id="stdout-container" class="container">
<textarea id="stdout" placeholder="Output" wrap="off" readonly></textarea>
</div>
<div id="options-container" class="container">
<div class="option so-box">Steps per Tick: <span id="steps-per-tick" contenteditable>500</span>
</div>
<div class="option so-box">Board Size: <span id="board-width" contenteditable>80</span> x <span id="board-height" contenteditable>25</span>
</div>
</div>