Cat's Eye Technologies avatar Cat's Eye Technologies committed d2c4f21 Draft

Initial, incomplete implementation in Javascript with yoob.js.

Comments (0)

Files changed (9)

impl/wunnel.js/demo/wunnel.html

+<!DOCTYPE html>
+<head>
+  <meta charset="utf-8">
+  <title>Wunnel</title>
+  <style>
+#program { display: none; }
+#load { display: none; }
+#display {
+    color: white;
+    background: blue;
+    border: 3px solid blue;
+    font-family: monospace;
+}
+#output {
+    color: white;
+    background: #008000;
+    border: 3px solid #008000;
+    font-family: monospace;
+}
+#canvas { border: 1px solid blue; display: none; }
+.example_program {
+    display: none;
+}
+  </style>
+</head>
+<body>
+
+<h1>Wunnel</h1>
+
+<button id="load">Load</button>
+<button id="edit">Edit</button>
+<button id="start">Start</button>
+<button id="stop">Stop</button>
+<button id="step">Step</button>
+Speed: <input id="speed" type="range" min="0" max="200" value="0" />
+
+<div>
+  example source: <select id="select_source"></select>
+</div>
+
+<div id="display_container">
+  <pre id="display"></pre>
+</div>
+
+<textarea id="program" rows="25" cols="40"></textarea>
+
+<div id="bitwise-cat" class="example_program"
+>          o   ooo  o
+
+
+o
+o
+o
+o         o
+o         o
+o         o
+o         o
+o
+o        o     o
+o         o
+o
+o        o
+o              o
+o        o     o
+o              o
+
+         o
+o oooooooo     o
+         o
+         o
+         o
+
+         o    oooo o
+</div>
+
+</body>
+<script src="../src/yoob/playfield.js"></script>
+<script src="../src/yoob/playfield-html-view.js"></script>
+<script src="../src/yoob/cursor.js"></script>
+<script src="../src/yoob/tape.js"></script>
+<script src="../src/yoob/controller.js"></script>
+<script src="../src/yoob/example-manager.js"></script>
+<script src="../src/wunnel-controller.js"></script>
+<script>
+  var v = new yoob.PlayfieldHTMLView().init(
+    null, document.getElementById('display')
+  );
+  var c = (new WunnelController()).init(v);
+  c.connect({
+    'start': 'start',
+    'stop': 'stop',
+    'step': 'step',
+    'load': 'load',
+    'edit': 'edit',
+    'speed': 'speed',
+    'select': 'select_source',
+    'source': 'program',
+    'display': 'display_container'
+  });
+  c.click_load();
+  e = (new yoob.ExampleManager()).init({
+    selectElem: document.getElementById('select_source'),
+    controller: c
+  }).populateFromClass('example_program');
+</script>

impl/wunnel.js/src/wunnel-controller.js

+/*
+ * requires yoob.Controller
+ * requires yoob.Playfield
+ * requires yoob.Cursor
+ * ...VERY INCOMPLETE.
+ */
+function WunnelPlayfield() {
+    this.setDefault(' ');
+};
+WunnelPlayfield.prototype = new yoob.Playfield();
+
+
+function WunnelCursor() {
+    this.getX = function() { return this.x; }
+    this.getY = function() { return this.y; }
+    this.setX = function(x) { this.x = x; }
+    this.setY = function(y) { this.y = y; }
+};
+WunnelCursor.prototype = new yoob.Cursor();
+
+
+function WunnelController() {
+    var pf;
+    var ip;
+    var opp;
+    var tape;
+    var head;
+
+    var halted;
+    var needsInput;
+
+    this.init = function(view) {
+        pf = new WunnelPlayfield();
+
+        ip = new WunnelCursor(0, 0, 1, 1);
+        opp = new WunnelCursor(0, 0, 1, 1);
+
+        view.pf = pf;
+        this.view = view.setCursors([ip]);
+
+        return this;
+    };
+
+    this.positiveGenus = "0689@%&QROPADBqeopadb";
+
+    this.genusMoreThanZero = function(c) {
+        for (var i = 0; i < this.positiveGenus.length; i++) {
+            if (this.positiveGenus.charAt(i) === c)
+                return true;
+        }
+        return false;
+    };
+
+    this.step = function() {
+
+        //BasicCursor<CharacterElement> ip = playfield.getCursor(0);
+        //BasicCursor<Operation> opp = opTable.getCursor(0);
+        //BasicHead<IntegerElement> h = tape.getHead(0);
+        var instruction = pf.get(ip.x, ip.y);
+        var k = 'BLA'; // operations.get(opp.x, opp.y);
+
+        if (this.genusMoreThanZero(instruction)) {
+            if (k === 'END') {
+                halted = true;
+                return errors;
+            } else if (k === 'NOP') {
+            } else if (k === 'SHU') {
+                if (ip.isHeaded(-1, 0)) {
+                    ip.setY(ip.getY() - h.read());
+                } else if (ip.isHeaded(1, 0)) {
+                    ip.setY(ip.getY() + h.read());
+                } else if (ip.isHeaded(0, -1)) {
+                    ip.setX(ip.getX() + h.read());
+                } else if (ip.isHeaded(0, 1)) {
+                    ip.setX(ip.getX() - h.read());
+                }
+            } else if (k === 'ROT') {
+                ip.rotateCounterclockwise();
+                ip.rotateCounterclockwise();
+                opp.rotateCounterclockwise();
+                opp.rotateCounterclockwise();
+            } else if (k === 'LEF') {
+                head--;
+            } else if (k === 'RIG') {
+                head++;
+            } else if (k === 'NEG') {
+                tape.put(head, -1);
+            } else if (k === 'BLA') {
+                tape.put(head, 0);
+            } else if (k === 'PLU') {
+                tape.put(head, 1);
+            } else if (k === 'OUT') {
+                var i = tape.get(head);
+                if (i === 0) {
+                    //world.output(new CharacterElement('0'));
+                } else {
+                    //world.output(new CharacterElement('1'));
+                }
+            } else if (k === 'INP') {
+                var c = '0'; // world.inputCharacter();
+                if (c == null) {
+                    needsInput = true;
+                    return errors;
+                }
+                if (c === '1') {
+                    tape.put(head, 1);
+                } else {
+                    tape.put(head, 0);
+                }
+            }
+        } else {
+            opp.advance();
+        }
+
+        ip.advance();
+        /*
+        if (playfield.hasFallenOffEdge(ip)) {
+            halted = true;
+        }
+        */
+
+        this.view.draw();
+
+        needsInput = false;
+    }
+
+    this.load = function(text) {
+        pf.clear();
+        pf.load(0, 0, text);
+        ip.x = 0;
+        ip.y = 0;
+        ip.dx = 0;
+        ip.dy = 1;
+        tape = new yoob.Tape();
+        head = 0;
+        this.view.draw();
+    };
+};
+WunnelController.prototype = new yoob.Controller();
Add a comment to this file

impl/wunnel.js/src/wunnel.js

Empty file added.

impl/wunnel.js/src/yoob/controller.js

+/*
+ * This file is part of yoob.js version 0.6-PRE
+ * Available from https://github.com/catseye/yoob.js/
+ * This file is in the public domain.  See http://unlicense.org/ for details.
+ */
+if (window.yoob === undefined) yoob = {};
+
+/*
+ * A controller for executing(/animating/evolving) states
+ * (such as esolang program states or cellular automaton
+ * configurations.)
+ *
+ * Can be connected to a UI in the DOM.
+ *
+ * Subclass this and override the following methods:
+ * - make it evolve the state by one tick in the step() method
+ * - make it load the state from a multiline string in the load() method
+ *
+ * You may wish to store the state in the controller's .state attribute,
+ * but you needn't (and arguably shouldn't.)  Likewise, the controller
+ * does not concern itself with depicting the state.  You should use
+ * something like yoob.PlayfieldCanvasView for that, instead.
+ */
+yoob.Controller = function() {
+    this.intervalId = undefined;
+    this.delay = 100;
+    this.source = undefined;
+    this.speed = undefined;
+    this.controls = {};
+
+    this.makeEventHandler = function(control, key) {
+        if (this['click_' + key] !== undefined) {
+            key = 'click_' + key;
+        }
+        var $this = this;
+        return function(e) {
+            $this[key](control);
+        };
+    };
+
+    /*
+     * Single argument is a dictionary (object) where the keys
+     * are the actions a controller can undertake, and the values
+     * are either DOM elements or strings; if strings, DOM elements
+     * with those ids will be obtained from the document and used.
+     */
+    this.connect = function(dict) {
+        var keys = ["start", "stop", "step", "load", "edit"];
+        for (var i in keys) {
+            var key = keys[i];
+            var value = dict[key];
+            if (typeof value === 'string') {
+                value = document.getElementById(value);
+            }
+            if (value) {
+                value.onclick = this.makeEventHandler(value, key);
+                this.controls[key] = value;
+            }
+        }
+        if (this.controls.stop) {
+            this.controls.stop.disabled = true;
+        }
+
+        var keys = ["source", "display"];
+        for (var i in keys) {
+            var key = keys[i];
+            var value = dict[key];
+            if (typeof value === 'string') {
+                value = document.getElementById(value);
+            }
+            if (value) {
+                this[key] = value;
+            }
+        }
+
+        var speed = dict.speed;
+        if (typeof speed === 'string') {
+            speed = document.getElementById(speed);
+        }
+        if (speed) {
+            this.speed = speed;
+            speed.value = this.delay;
+            var $this = this;
+            speed.onchange = function(e) {
+                $this.delay = speed.value;
+                if ($this.intervalId !== undefined) {
+                    $this.stop();
+                    $this.start();
+                }
+            }
+        }        
+    };
+
+    this.click_step = function(e) {
+        this.stop();
+        this.step();
+    };
+
+    this.step = function() {
+        alert("step() NotImplementedError");
+    };
+
+    this.click_load = function(e) {
+        this.click_stop();
+        this.load(this.source.value);
+        if (this.controls.edit) this.controls.edit.style.display = "inline";
+        if (this.controls.load) this.controls.load.style.display = "none";
+        if (this.controls.start) this.controls.start.disabled = false;
+        if (this.controls.step) this.controls.step.disabled = false;
+        if (this.controls.stop) this.controls.stop.disabled = true;
+        if (this.display) this.display.style.display = "block";
+        if (this.source) this.source.style.display = "none";
+    };
+
+    this.load = function(text) {
+        alert("load() NotImplementedError");
+    };
+
+    /*
+     * Loads a source text into the source element.
+     */
+    this.loadSource = function(text) {
+        if (this.source) this.source.value = text;
+        this.load(text);
+    };
+
+    /*
+     * Loads a source text into the source element.
+     * Assumes it comes from an element in the document, so it translates
+     * the basic HTML escapes (but no others) to plain text.
+     */
+    this.loadSourceFromHTML = function(html) {
+        var text = html;
+        text = text.replace(/\&lt;/g, '<');
+        text = text.replace(/\&gt;/g, '>');
+        text = text.replace(/\&amp;/g, '&');
+        this.loadSource(text);
+    };
+
+    this.click_edit = function(e) {
+        this.click_stop();
+        if (this.controls.edit) this.controls.edit.style.display = "none";
+        if (this.controls.load) this.controls.load.style.display = "inline";
+        if (this.controls.start) this.controls.start.disabled = true;
+        if (this.controls.step) this.controls.step.disabled = true;
+        if (this.controls.stop) this.controls.stop.disabled = true;
+        if (this.display) this.display.style.display = "none";
+        if (this.source) this.source.style.display = "block";
+    };
+
+    this.click_start = function(e) {
+        this.start();
+        if (this.controls.start) this.controls.start.disabled = true;
+        if (this.controls.step) this.controls.step.disabled = false;
+        if (this.controls.stop) this.controls.stop.disabled = false;
+    };
+
+    this.start = function() {
+        if (this.intervalId !== undefined)
+            return;
+        this.step();
+        var $this = this;
+        this.intervalId = setInterval(function() { $this.step(); }, this.delay);
+    };
+
+    this.click_stop = function(e) {
+        this.stop();
+        if (this.controls.stop && this.controls.stop.disabled) {
+            return;
+        }
+        if (this.controls.start) this.controls.start.disabled = false;
+        if (this.controls.step) this.controls.step.disabled = false;
+        if (this.controls.stop) this.controls.stop.disabled = true;
+    };
+
+    this.stop = function() {
+        if (this.intervalId === undefined)
+            return;
+        clearInterval(this.intervalId);
+        this.intervalId = undefined;
+    };
+};

impl/wunnel.js/src/yoob/cursor.js

+/*
+ * This file is part of yoob.js version 0.6-PRE
+ * Available from https://github.com/catseye/yoob.js/
+ * This file is in the public domain.  See http://unlicense.org/ for details.
+ */
+if (window.yoob === undefined) yoob = {};
+
+/*
+ * An object representing a pointer (position vector) into two-dimensional
+ * Cartesian space (possibly a yoob.Playfield) with a direction vector
+ * (that need not be used).
+ *
+ * A cursor contains a built-in simple view, i.e. it knows how to render
+ * itself on a canvas (drawContext method) or in the midst of HTML text
+ * (wrapText method).  These methods are used by the playfield view classes.
+ * The supplied methods draw basic block cursors in the colour given by
+ * the fillStyle attribute, if present, or a light green if it is not set.
+ */
+yoob.Cursor = function(x, y, dx, dy) {
+    this.x = x;
+    this.y = y;
+    this.dx = dx;
+    this.dy = dy;
+
+    this.isHeaded = function(dx, dy) {
+        return this.dx === dx && this.dy === dy;
+    };
+
+    this.advance = function() {
+        this.x += this.dx;
+        this.y += this.dy;
+    };
+
+    this.rotateClockwise = function() {
+        if (this.dx === 0 && this.dy === -1) {
+            this.dx = 1; this.dy = -1;
+        } else if (this.dx === 1 && this.dy === -1) {
+            this.dx = 1; this.dy = 0;
+        } else if (this.dx === 1 && this.dy === 0) {
+            this.dx = 1; this.dy = 1;
+        } else if (this.dx === 1 && this.dy === 1) {
+            this.dx = 0; this.dy = 1;
+        } else if (this.dx === 0 && this.dy === 1) {
+            this.dx = -1; this.dy = 1;
+        } else if (this.dx === -1 && this.dy === 1) {
+            this.dx = -1; this.dy = 0;
+        } else if (this.dx === -1 && this.dy === 0) {
+            this.dx = -1; this.dy = -1;
+        } else if (this.dx === -1 && this.dy === -1) {
+            this.dx = 0; this.dy = -1;
+        }
+    };
+
+    this.rotateCounterclockwise = function() {
+        if (this.dx === 0 && this.dy === -1) {
+            this.dx = -1; this.dy = -1;
+        } else if (this.dx === -1 && this.dy === -1) {
+            this.dx = -1; this.dy = 0;
+        } else if (this.dx === -1 && this.dy === 0) {
+            this.dx = -1; this.dy = 1;
+        } else if (this.dx === -1 && this.dy === 1) {
+            this.dx = 0; this.dy = 1;
+        } else if (this.dx === 0 && this.dy === 1) {
+            this.dx = 1; this.dy = 1;
+        } else if (this.dx === 1 && this.dy === 1) {
+            this.dx = 1; this.dy = 0;
+        } else if (this.dx === 1 && this.dy === 0) {
+            this.dx = 1; this.dy = -1;
+        } else if (this.dx === 1 && this.dy === -1) {
+            this.dx = 0; this.dy = -1;
+        }
+    };
+
+    /*
+     * For HTML views.  Override if you like.
+     */
+    this.wrapText = function(text) {
+        var fillStyle = this.fillStyle || "#50ff50";
+        return '<span style="background: ' + fillStyle + '">' +
+               text + '</span>';
+    };
+
+    /*
+     * For canvas views.  Override if you like.
+     */
+    this.drawContext = function(ctx, x, y, cellWidth, cellHeight) {
+        ctx.fillStyle = this.fillStyle || "#50ff50";
+        ctx.fillRect(x, y, cellWidth, cellHeight);
+    };
+}

impl/wunnel.js/src/yoob/example-manager.js

+/*
+ * This file is part of yoob.js version 0.6-PRE
+ * Available from https://github.com/catseye/yoob.js/
+ * This file is in the public domain.  See http://unlicense.org/ for details.
+ */
+if (window.yoob === undefined) yoob = {};
+
+/*
+ * An object for managing a set of example programs (or other "pre-fab"
+ * things) for use in an esolang interpreter (or other thing that could
+ * use these things.  For example, games for an emulator, etc.)
+ *
+ * Mostly intended to be connected to a yoob.Controller.
+ */
+yoob.ExampleManager = function() {
+    /*
+     * The single argument is a dictionary (object) where the keys are:
+     *    selectElem: (required) the <select> DOM element that will be
+     *        populated with the available example programs.  Selecting one
+     *        will cause the .select() method of this manager to be called.
+     *        it will also call .onselect if that method is present.
+     */
+    this.init = function(cfg) {
+        this.selectElem = cfg.selectElem;
+        this.exampleClass = cfg.exampleClass || null;
+        this.controller = cfg.controller || null;
+        this.clear();
+        var $this = this;
+        this.selectElem.onchange = function() {
+            $this.select(this.options[this.selectedIndex].value);
+        }
+        return this;
+    };
+
+    /*
+     * Removes all options from the selectElem, and their associated data.
+     */
+    this.clear = function() {
+        this.reactTo = {};
+        while (this.selectElem.firstChild) {
+            this.selectElem.removeChild(this.selectElem.firstChild);
+        }
+        this.add('(select one...)', function() {});
+        return this;
+    };
+
+    /*
+     * Adds an example to this ExampleManager.  When it is selected,
+     * the given callback will be called, being passed the id as the
+     * first argument.  If no callback is provided, a default callback,
+     * which loads the contents of the element with the specified id
+     * into the configured controller, will be used.
+     */
+    this.add = function(id, callback) {
+        var opt = document.createElement("option");
+        opt.text = id;
+        opt.value = id;
+        this.selectElem.options.add(opt);
+        var $this = this;
+        this.reactTo[id] = callback || function(id) {
+            $this.controller.click_stop(); // in case it is currently running
+            $this.controller.loadSourceFromHTML(
+              document.getElementById(id).innerHTML
+            );
+        };
+        return this;
+    };
+
+    /*
+     * Called by the selectElem's onchange event.  For sanity, you should
+     * probably not call this yourself.
+     */
+    this.select = function(id) {
+        this.reactTo[id](id);
+        if (this.onselect) {
+            this.onselect(id);
+        }
+    };
+
+    /*
+     * When called, every DOM element in the document with the given
+     * class will be considered an example program, and the manager
+     * will be populated with these.  Generally the CSS for the class
+     * will have `display: none` and the elements will be <div>s.
+     *
+     * callback is as described for the .add() method.
+     */
+    this.populateFromClass = function(className, callback) {
+        var elements = document.getElementsByClassName(className);
+        for (var i = 0; i < elements.length; i++) {
+            var e = elements[i];
+            this.add(e.id, callback);
+        }
+        return this;
+    };
+};

impl/wunnel.js/src/yoob/playfield-html-view.js

+/*
+ * This file is part of yoob.js version 0.6-PRE
+ * Available from https://github.com/catseye/yoob.js/
+ * This file is in the public domain.  See http://unlicense.org/ for details.
+ */
+if (window.yoob === undefined) yoob = {};
+
+/*
+ * A view (in the MVC sense) for depicting a yoob.Playfield (-compatible)
+ * object onto any DOM element that supports innerHTML.
+ *
+ * TODO: this may be incomplete; use at your own risk
+ * TODO: have this and the canvas view inherit from a common ABC?
+ */
+yoob.PlayfieldHTMLView = function() {
+    this.pf = undefined;
+    this.element = undefined;
+
+    this.init = function(pf, element) {
+        this.pf = pf;
+        this.element = element;
+        this.cursors = [];
+        return this;
+    };
+
+    /*** Chainable setters ***/
+
+    /*
+     * Set the list of cursors to the given list of yoob.Cursor (or compatible)
+     * objects.
+     */
+    this.setCursors = function(cursors) {
+        this.cursors = cursors;
+        return this;
+    };
+
+    /*
+     * Return the requested bounds of the occupied portion of the playfield.
+     * "Occupation" in this sense includes all cursors.
+     *
+     * These may return 'undefined' if there is nothing in the playfield.
+     *
+     * Override these if you want to draw some portion of the
+     * playfield which is not the whole playfield.
+     */
+    this.getLowerX = function() {
+        var minX = this.pf.getMinX();
+        for (var i = 0; i < this.cursors.length; i++) {
+            if (minX === undefined || this.cursors[i].x < minX) {
+                minX = this.cursors[i].x;
+            }
+        }
+        return minX;
+    };
+    this.getUpperX = function() {
+        var maxX = this.pf.getMaxX();
+        for (var i = 0; i < this.cursors.length; i++) {
+            if (maxX === undefined || this.cursors[i].x > maxX) {
+                maxX = this.cursors[i].x;
+            }
+        }
+        return maxX;
+    };
+    this.getLowerY = function() {
+        var minY = this.pf.getMinY();
+        for (var i = 0; i < this.cursors.length; i++) {
+            if (minY === undefined || this.cursors[i].y < minY) {
+                minY = this.cursors[i].y;
+            }
+        }
+        return minY;
+    };
+    this.getUpperY = function() {
+        var maxY = this.pf.getMaxY();
+        for (var i = 0; i < this.cursors.length; i++) {
+            if (maxY === undefined || this.cursors[i].y > maxY) {
+                maxY = this.cursors[i].y;
+            }
+        }
+        return maxY;
+    };
+
+    /*
+     * Returns the number of occupied cells in the x direction.
+     * "Occupation" in this sense includes all cursors.
+     */
+    this.getExtentX = function() {
+        if (this.getLowerX() === undefined || this.getUpperX() === undefined) {
+            return 0;
+        } else {
+            return this.getUpperX() - this.getLowerX() + 1;
+        }
+    };
+
+    /*
+     * Returns the number of occupied cells in the y direction.
+     * "Occupation" in this sense includes all cursors.
+     */
+    this.getExtentY = function() {
+        if (this.getLowerY() === undefined || this.getUpperY() === undefined) {
+            return 0;
+        } else {
+            return this.getUpperY() - this.getLowerY() + 1;
+        }
+    };
+
+    /*
+     * Override to convert Playfield values to HTML.
+     */
+    this.render = function(value) {
+        return value;
+    };
+
+    /*
+     * Render the playfield, as HTML, on the DOM element.
+     */
+    this.draw = function() {
+        var text = "";
+        for (var y = this.getLowerY(); y <= this.getUpperY(); y++) {
+            var row = "";
+            for (var x = this.getLowerX(); x <= this.getUpperX(); x++) {
+                var rendered = this.render(this.pf.get(x, y));
+                for (var i = 0; i < this.cursors.length; i++) {
+                    if (this.cursors[i].x === x && this.cursors[i].y === y) {
+                        rendered = this.cursors[i].wrapText(rendered);
+                    }
+                }
+                row += rendered;
+            }
+            text += row + "\n";
+        }
+        this.element.innerHTML = text;
+    };
+
+};

impl/wunnel.js/src/yoob/playfield.js

+/*
+ * This file is part of yoob.js version 0.6-PRE
+ * Available from https://github.com/catseye/yoob.js/
+ * This file is in the public domain.  See http://unlicense.org/ for details.
+ */
+if (window.yoob === undefined) yoob = {};
+
+/*
+ * A two-dimensional Cartesian grid of values.
+ */
+yoob.Playfield = function() {
+    this._store = {};
+    this.minX = undefined;
+    this.minY = undefined;
+    this.maxX = undefined;
+    this.maxY = undefined;
+    this._default = undefined;
+
+    /*
+     * Set the default value for this Playfield.  This
+     * value is returned by get() for any cell that was
+     * never written to, or had `undefined` put() into it.
+     */
+    this.setDefault = function(v) {
+        this._default = v;
+        return this;
+    };
+
+    /*
+     * Obtain the value at (x, y).  The default value will
+     * be returned if the cell was never written to.
+     */
+    this.get = function(x, y) {
+        var v = this._store[x+','+y];
+        if (v === undefined) return this._default;
+        return v;
+    };
+
+    /*
+     * Write a new value into (x, y).  Note that writing
+     * `undefined` into a cell has the semantics of deleting
+     * the value at that cell; a subsequent get() for that
+     * location will return this Playfield's default value.
+     */
+    this.put = function(x, y, value) {
+        var key = x+','+y;
+        if (value === undefined || value === this._default) {
+            delete this._store[key];
+            return;
+        }
+        if (this.minX === undefined || x < this.minX) this.minX = x;
+        if (this.maxX === undefined || x > this.maxX) this.maxX = x;
+        if (this.minY === undefined || y < this.minY) this.minY = y;
+        if (this.maxY === undefined || y > this.maxY) this.maxY = y;
+        this._store[key] = value;
+    };
+
+    /*
+     * Like put(), but does not update the playfield bounds.  Do
+     * this if you must do a batch of put()s in a more efficient
+     * manner; after doing so, call recalculateBounds().
+     */
+    this.putDirty = function(x, y, value) {
+        var key = x+','+y;
+        if (value === undefined || value === this._default) {
+            delete this._store[key];
+            return;
+        }
+        this._store[key] = value;
+    };
+
+    /*
+     * Recalculate the bounds (min/max X/Y) which are tracked
+     * internally to support methods like foreach().  This is
+     * not needed *unless* you've used putDirty() at some point.
+     * (In which case, call this immediately after your batch
+     * of putDirty()s.)
+     */
+    this.recalculateBounds = function() {
+        this.minX = undefined;
+        this.minY = undefined;
+        this.maxX = undefined;
+        this.maxY = undefined;
+
+        for (var cell in this._store) {
+            var pos = cell.split(',');
+            var x = parseInt(pos[0], 10);
+            var y = parseInt(pos[1], 10);
+            if (this.minX === undefined || x < this.minX) this.minX = x;
+            if (this.maxX === undefined || x > this.maxX) this.maxX = x;
+            if (this.minY === undefined || y < this.minY) this.minY = y;
+            if (this.maxY === undefined || y > this.maxY) this.maxY = y;
+        }
+    };
+
+    /*
+     * Clear the contents of this Playfield.
+     */
+    this.clear = function() {
+        this._store = {};
+        this.minX = undefined;
+        this.minY = undefined;
+        this.maxX = undefined;
+        this.maxY = undefined;
+    };
+
+    /*
+     * Scroll a rectangular subrectangle of this Playfield, up.
+     * TODO: support other directions.
+     */
+    this.scrollRectangleY = function(dy, minX, minY, maxX, maxY) {
+        if (dy < 1) {
+            for (var y = minY; y <= (maxY + dy); y++) {
+                for (var x = minX; x <= maxX; x++) {
+                    this.put(x, y, this.get(x, y - dy));
+                }
+            }
+        } else { alert("scrollRectangleY(" + dy + ") notImplemented"); }
+    };
+
+    this.clearRectangle = function(minX, minY, maxX, maxY) {
+        // Could also do this with a foreach that checks
+        // each position.  Would be faster on sparser playfields.
+        for (var y = minY; y <= maxY; y++) {
+            for (var x = minX; x <= maxX; x++) {
+                this.put(x, y, undefined);
+            }
+        }
+    };
+
+    /*
+     * Load a string into this Playfield.
+     * The string may be multiline, with newline (ASCII 10)
+     * characters delimiting lines.  ASCII 13 is ignored.
+     *
+     * If transformer is given, it should be a one-argument
+     * function which accepts a character and returns the
+     * object you wish to write into the playfield upon reading
+     * that character.
+     */
+    this.load = function(x, y, string, transformer) {
+        var lx = x;
+        var ly = y;
+        if (transformer === undefined) {
+            transformer = function(c) {
+                if (c === ' ') {
+                    return undefined;
+                } else {
+                    return c;
+                }
+            }
+        }
+        for (var i = 0; i < string.length; i++) {
+            var c = string.charAt(i);
+            if (c === '\n') {
+                lx = x;
+                ly++;
+            } else if (c === '\r') {
+            } else {
+                this.putDirty(lx, ly, transformer(c));
+                lx++;
+            }
+        }
+        this.recalculateBounds();
+    };
+
+    /*
+     * Convert this Playfield to a multi-line string.  Each row
+     * is a line, delimited with a newline (ASCII 10).
+     *
+     * If transformer is given, it should be a one-argument
+     * function which accepts a playfield element and returns a
+     * character (or string) you wish to place in the resulting
+     * string for that element.
+     */
+    this.dump = function(transformer) {
+        var text = "";
+        if (transformer === undefined) {
+            transformer = function(c) { return c; }
+        }
+        for (var y = this.minY; y <= this.maxY; y++) {
+            var row = "";
+            for (var x = this.minX; x <= this.maxX; x++) {
+                row += transformer(this.get(x, y));
+            }
+            text += row + "\n";
+        }
+        return text;
+    };
+
+    /*
+     * Iterate over every defined cell in the Playfield.
+     * fun is a callback which takes three parameters:
+     * x, y, and value.  If this callback returns a value,
+     * it is written into the Playfield at that position.
+     * This function ensures a particular order.
+     */
+    this.foreach = function(fun) {
+        for (var y = this.minY; y <= this.maxY; y++) {
+            for (var x = this.minX; x <= this.maxX; x++) {
+                var key = x+','+y;
+                var value = this._store[key];
+                if (value === undefined)
+                    continue;
+                var result = fun(x, y, value);
+                if (result !== undefined) {
+                    if (result === ' ') {
+                        result = undefined;
+                    }
+                    this.put(x, y, result);
+                }
+            }
+        }
+    };
+
+    /*
+     * Analogous to (monoid) map in functional languages,
+     * iterate over this Playfield, transform each value using
+     * a supplied function, and write the transformed value into
+     * a destination Playfield.
+     *
+     * Supplied function should take a Playfield (this Playfield),
+     * x, and y, and return a value.
+     *
+     * The map source may extend beyond the internal bounds of
+     * the Playfield, by giving the min/max Dx/Dy arguments
+     * (which work like margin offsets.)
+     *
+     * Useful for evolving a cellular automaton playfield.  In this
+     * case, min/max Dx/Dy should be computed from the neighbourhood.
+     */
+    this.map = function(destPf, fun, minDx, minDy, maxDx, maxDy) {
+        if (minDx === undefined) minDx = 0;
+        if (minDy === undefined) minDy = 0;
+        if (maxDx === undefined) maxDx = 0;
+        if (maxDy === undefined) maxDy = 0;
+        for (var y = this.minY + minDy; y <= this.maxY + maxDy; y++) {
+            for (var x = this.minX + minDx; x <= this.maxX + maxDx; x++) {
+                destPf.putDirty(x, y, fun(pf, x, y));
+            }
+        }
+        destPf.recalculateBounds();
+    };
+
+    /*
+     * Accessors for the minimum (resp. maximum) x (resp. y) values of
+     * occupied (non-default-valued) cells in this Playfield.  If there are
+     * no cells in this Playfield, these will refturn undefined.  Note that
+     * these are not guaranteed to be tight bounds; if values in cells
+     * are deleted, these bounds may still be considered to be outside them.
+     */
+    this.getMinX = function() {
+        return this.minX;
+    };
+    this.getMaxX = function() {
+        return this.maxX;
+    };
+    this.getMinY = function() {
+        return this.minY;
+    };
+    this.getMaxY = function() {
+        return this.maxY;
+    };
+
+    /*
+     * Returns the number of occupied cells in the x direction.
+     */
+    this.getExtentX = function() {
+        if (this.maxX === undefined || this.minX === undefined) {
+            return 0;
+        } else {
+            return this.maxX - this.minX + 1;
+        }
+    };
+
+    /*
+     * Returns the number of occupied cells in the y direction.
+     */
+    this.getExtentY = function() {
+        if (this.maxY === undefined || this.minY === undefined) {
+            return 0;
+        } else {
+            return this.maxY - this.minY + 1;
+        }
+    };
+};

impl/wunnel.js/src/yoob/tape.js

+/*
+ * This file is part of yoob.js version 0.3
+ * Available from https://github.com/catseye/yoob.js/
+ * This file is in the public domain.  See http://unlicense.org/ for details.
+ */
+if (window.yoob === undefined) yoob = {};
+
+/*
+ * A (theoretically) unbounded tape, like you'd find on a Turing machine.
+ */
+yoob.Tape = function() {
+    this._store = {};
+    this.min = undefined;
+    this.max = undefined;
+
+    /*
+     * Obtain the value at the given position.
+     * Cells are undefined if they were never written to.
+     */
+    this.get = function(pos) {
+        return this._store[pos];
+    };
+
+    /*
+     * Write a new value into the given position.
+     */
+    this.put = function(pos, value) {
+        if (this.min === undefined || pos < this.min) this.min = pos;
+        if (this.max === undefined || pos > this.max) this.max = pos;
+        if (value === undefined) {
+            delete this._store[pos];
+        }
+        this._store[pos] = value;
+    };
+
+    /*
+     * Iterate over every defined cell on the Tape
+     * fun is a callback which takes two parameters:
+     * position and value.  If this callback returns a value,
+     * it is written into the Tape at that position.
+     * This function ensures a particular order.
+     */
+    this.foreach = function(fun) {
+        for (var pos = this.min; pos <= this.max; pos++) {
+            var value = this._store[pos];
+            if (value === undefined)
+                continue;
+            var result = fun(pos, value);
+            if (result !== undefined) {
+                if (result === ' ') {
+                    result = undefined;
+                }
+                this.put(pos, result);
+            }
+        }
+    };
+
+    /*
+     * Draws elements of the Tape in a drawing context.
+     * x and y are canvas coordinates, and width and height
+     * are canvas units of measure.
+     * The default implementation just renders them as text,
+     * in black.
+     * Override if you wish to draw them differently.
+     */
+    this.drawElement = function(ctx, x, y, cellWidth, cellHeight, elem) {
+        ctx.fillStyle = "black";
+        ctx.fillText(elem.toString(), x, y);
+    };
+
+    /*
+     * Draws the Tape in a drawing context.
+     * cellWidth and cellHeight are canvas units of measure for each cell.
+     */
+    this.drawContext = function(ctx, offsetX, offsetY, cellWidth, cellHeight) {
+        var me = this;
+        this.foreach(function (pos, elem) {
+            me.drawElement(ctx, offsetX + pos * cellWidth, offsetY,
+                           cellWidth, cellHeight, elem);
+        });
+    };
+
+    /*
+     * Draws the Tape, and a set of TapeHeads, on a canvas element.
+     * Resizes the canvas to the needed dimensions.
+     * cellWidth and cellHeight are canvas units of measure for each cell.
+     */
+    this.drawCanvas = function(canvas, cellWidth, cellHeight, heads) {
+        var ctx = canvas.getContext('2d');
+
+        var width = this.max - this.min + 1;
+        var height = 1;
+
+        if (cellWidth === undefined) {
+          ctx.textBaseline = "top";
+          ctx.font = cellHeight + "px monospace";
+          cellWidth = ctx.measureText("@").width;
+        }
+
+        canvas.width = width * cellWidth;
+        canvas.height = height * cellHeight;
+
+        ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+        ctx.textBaseline = "top";
+        ctx.font = cellHeight + "px monospace";
+
+        var offsetX = this.min * cellWidth * -1;
+        var offsetY = 0;
+
+        for (var i = 0; i < heads.length; i++) {
+            heads[i].drawContext(
+              ctx,
+              offsetX + heads[i].pos * cellWidth, offsetY,
+              cellWidth, cellHeight
+            );
+        }
+
+        this.drawContext(ctx, offsetX, offsetY, cellWidth, cellHeight);
+    };
+
+};
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.