Anonymous avatar Anonymous committed 6697222 Draft

Use yoob.Playfield 0.2, and inherit from yoob.Controller.

Comments (0)

Files changed (4)

 #canvas { border: 1px solid blue; }
 #canvas_viewport {
     width: 420px; height: 420px; overflow: scroll; border: 1px solid black;
-    display: inline-block;
 }
 #info { float: right; }
+#program {
+  display: none;
+}
+#load {
+  display: none;
+}
   </style>
 </head>
 <body>
 <a href="http://esolangs.org/wiki/Gemooy">esolangs wiki</a>
 </p>
 
-<h1>Gemooy
+<h1>Gemooy</h1>
+
 <button id="load">Load</button>
+<button id="edit">Edit</button>
 <button id="start">Start</button>
 <button id="stop">Stop</button>
-</h1>
+<button id="step">Step</button>
+Speed: <input id="speed" type="range" min="0" max="200" value="0" />
 
 <div id="canvas_viewport">
   <canvas id="canvas" width="400" height="400">
 </textarea>
 
 </body>
+<script src="../src/yoob/controller.js"></script>
 <script src="../src/yoob/playfield.js"></script>
 <script src="../src/yoob/cursor.js"></script>
 <script src="../src/gemooy.js"></script>
 <script>
-var eso = new GemooyController(document.getElementById('canvas'));
-document.getElementById('load').onclick = function() {
-    eso.load(document.getElementById('program'));
-};
-document.getElementById('start').onclick = function() {
-    eso.start();
-};
-document.getElementById('stop').onclick = function() {
-    eso.stop();
-};
+  var c = new GemooyController();
+  c.init(document.getElementById('canvas'));
+  c.connect({
+    'start': 'start',
+    'stop': 'stop',
+    'step': 'step',
+    'load': 'load',
+    'edit': 'edit',
+    'source': 'program',
+    'display': 'canvas_viewport',
+    'speed': 'speed'
+  });
+  c.click_load();
 </script>
+
+</script>
 /*
+ * requires yoob.Controller
  * requires yoob.Playfield
  * requires yoob.Cursor
  */
 function GemooyPlayfield() {
+    this.setDefault(' ');
+
     this.increment = function(x, y) {
         var data = this.get(x, y);
-        if (data === undefined) {
+        if (data === ' ') {
             data = '#';
         } else if (data === '#') {
             data = '@';
         } else if (data === '@') {
-            data = undefined;
+            data = ' ';
         }
         this.put(x, y, data);
-    }
+    };
 
     this.decrement = function(x, y) {
         var data = this.get(x, y);
-        if (data === undefined) {
+        if (data === ' ') {
             data = '@';
         } else if (data === '@') {
             data = '#';
         } else if (data === '#') {
-            data = undefined;
+            data = ' ';
         }
         this.put(x, y, data);
-    }
-}
+    };
+};
 GemooyPlayfield.prototype = new yoob.Playfield();
 
 
-function GemooyController(canvas) {
-    var interval_id;
+function GemooyController() {
+    var intervalId;
+    var canvas;
+    var ctx;
 
-    var p = new GemooyPlayfield();
-    var ip = new yoob.Cursor(0, 0, 1, 1);
-    var dp = new yoob.Cursor(0, 0, 0, 0);
+    var p;
+    var ip;
+    var dp;
+
+    this.init = function(c) {
+        p = new GemooyPlayfield();
+        ip = new yoob.Cursor(0, 0, 1, 1);
+        dp = new yoob.Cursor(0, 0, 0, 0);
+        canvas = c;
+        ctx = canvas.getContext('2d');
+    };
 
     this.draw = function() {
-        var ctx = canvas.getContext('2d');
-
         var height = 20;
         ctx.font = height + "px monospace";
         var width = ctx.measureText("@").width;
 
-        canvas.width = (p.max_x - p.min_x + 1) * width;
-        canvas.height = (p.max_y - p.min_y + 1) * height;
+        canvas.width = p.getExtentX() * width;
+        canvas.height = p.getExtentY() *  height;
 
         ctx.clearRect(0, 0, canvas.width, canvas.height);
         ctx.textBaseline = "top";
         p.foreach(function (x, y, value) {
             ctx.fillText(value, x * width, y * height);
         });
-    }
+    };
 
     this.step = function() {
         var instr = p.get(ip.x, ip.y);
 
         if (instr === '@') {
             var data = p.get(dp.x, dp.y);
-            if (data === undefined) {
+            if (data === ' ') {
                 ip.rotateClockwise();
             } else if (data == '#') {
                 ip.rotateCounterclockwise();
 
         ip.advance();
         this.draw();
-    }
+    };
 
-    this.start = function() {
-        if (interval_id !== undefined)
-            return;
-        this.step();
-        var controller = this;
-        interval_id = setInterval(function() { controller.step(); }, 100);
-    }
-
-    this.stop = function() {
-        if (interval_id === undefined)
-            return;
-        clearInterval(interval_id);
-        interval_id = undefined;
-    }
-
-    this.load = function(textarea) {
-        this.stop();
+    this.load = function(text) {
         p.clear();
-        p.load(0, 0, textarea.value);
+        p.load(0, 0, text);
         p.foreach(function (x, y, value) {
             if (value === '$') {
                 ip.x = x;
         ip.dx = 1;
         ip.dy = 1;
         this.draw();
-    }
-}
+    };
+};
+GemooyController.prototype = new yoob.Controller();

src/yoob/controller.js

+/*
+ * This file is part of yoob.js version 0.3-PRE
+ * 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
+ */
+yoob.Controller = function() {
+    this.intervalId = undefined;
+    this.delay = 100;
+    this.source = undefined;
+    this.speed = undefined;
+    this.controls = {};
+
+    var makeOnClick = function(controller, key) {
+        if (controller['click_' + key] !== undefined)
+            key = 'click_' + key;
+        return function(e) { controller[key](); }
+    };
+
+    /*
+     * 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 self = this;
+        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 !== undefined) {
+                value.onclick = makeOnClick(this, key);
+                this.controls[key] = value;
+            }
+        }
+
+        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 !== undefined) {
+                this[key] = value;
+            }
+        }
+
+        var speed = dict.speed;
+        if (typeof speed === 'string') {
+            speed = document.getElementById(speed);
+        }
+        if (speed !== undefined) {
+            this.speed = speed;
+            speed.value = self.delay;
+            speed.onchange = function(e) {
+                self.delay = speed.value;
+                if (self.intervalId !== undefined) {
+                    self.stop();
+                    self.start();
+                }
+            }
+        }        
+    };
+
+    this.click_step = function(e) {
+        this.stop();
+        this.step();
+    };
+
+    this.step = function() {
+        alert("step() NotImplementedError");
+    };
+
+    this.click_load = function(e) {
+        this.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 = false;
+        if (this.display) this.display.style.display = "block";
+        if (this.source) this.source.style.display = "none";
+    };
+
+    this.load = function(text) {
+        alert("load() NotImplementedError");
+    };
+
+    this.click_edit = function(e) {
+        this.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.start = function() {
+        if (this.intervalId !== undefined)
+            return;
+        this.step();
+        var self = this;
+        this.intervalId = setInterval(function() { self.step(); }, this.delay);
+    };
+
+    this.stop = function() {
+        if (this.intervalId === undefined)
+            return;
+        clearInterval(this.intervalId);
+        this.intervalId = undefined;
+    };
+};

src/yoob/playfield.js

 /*
- * This file is part of yoob.js version 0.1
+ * This file is part of yoob.js version 0.2
  * This file is in the public domain.  See http://unlicense.org/ for details.
  */
 if (window.yoob === undefined) yoob = {};
  */
 yoob.Playfield = function() {
     this._store = {};
-    this.min_x = undefined;
-    this.min_y = undefined;
-    this.max_x = undefined;
-    this.max_y = undefined;
+    this.minX = undefined;
+    this.minY = undefined;
+    this.maxX = undefined;
+    this.maxY = undefined;
+    this._default = undefined;
 
     /*
-     * Obtain the value at (x, y).
-     * Cells are undefined if they were never written to.
+     * 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.get = function(x, y) {
-        return this._store[x+','+y];
+    this.setDefault = function(v) {
+        this._default = v;
     };
 
     /*
-     * Write a new value into (x, y).
+     * 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) {
-        if (this.min_x === undefined || x < this.min_x) this.min_x = x;
-        if (this.max_x === undefined || x > this.max_x) this.max_x = x;
-        if (this.min_y === undefined || y < this.min_y) this.min_y = y;
-        if (this.max_y === undefined || y > this.max_y) this.max_y = y;
-        if (value === undefined) {
-            delete this._store[x+','+y];
+        var key = x+','+y;
+        if (value === undefined || value === this._default) {
+            delete this._store[key];
+            return;
         }
-        this._store[x+','+y] = value;
+        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.maxX = 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;
+        }
     };
 
     /*
      */
     this.clear = function() {
         this._store = {};
-        this.min_x = undefined;
-        this.min_y = undefined;
-        this.max_x = undefined;
-        this.max_y = undefined;
+        this.minX = undefined;
+        this.minY = undefined;
+        this.maxX = undefined;
+        this.maxX = undefined;
     };
-          
+
     /*
-     * Load a string into the playfield.
+     * 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) {
+    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 === ' ') {
-                this.put(lx, ly, undefined);
-                lx++;
             } else if (c === '\r') {
             } else {
-                this.put(lx, ly, c);
+                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;
     };
 
     /*
      * This function ensures a particular order.
      */
     this.foreach = function(fun) {
-        for (var y = this.min_y; y <= this.max_y; y++) {
-            for (var x = this.min_x; x <= this.max_x; x++) {
+        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)
     };
 
     /*
+     * 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();
+    };
+
+    /*
      * Draws elements of the Playfield in a drawing context.
      * x and y are canvas coordinates, and width and height
      * are canvas units of measure.
         });
     };
 
+    this.getExtentX = function() {
+        if (this.maxX === undefined || this.minX === undefined) {
+            return 0;
+        } else {
+            return this.maxX - this.minX + 1;
+        }
+    };
+
+    this.getExtentY = function() {
+        if (this.maxY === undefined || this.minY === undefined) {
+            return 0;
+        } else {
+            return this.maxY - this.minY + 1;
+        }
+    };
+
     /*
      * Draws the Playfield, and a set of Cursors, on a canvas element.
      * Resizes the canvas to the needed dimensions.
     this.drawCanvas = function(canvas, cellWidth, cellHeight, cursors) {
         var ctx = canvas.getContext('2d');
       
-        var width = this.max_x - this.min_x + 1;
-        var height = this.max_y - this.min_y + 1;
+        var width = this.getExtentX();
+        var height = this.getExtentY();
 
         if (cellWidth === undefined) {
-          ctx.textBaseline = "top";
-          ctx.font = cellHeight + "px monospace";
-          cellWidth = ctx.measureText("@").width;
+            ctx.textBaseline = "top";
+            ctx.font = cellHeight + "px monospace";
+            cellWidth = ctx.measureText("@").width;
         }
 
         canvas.width = width * cellWidth;
         ctx.textBaseline = "top";
         ctx.font = cellHeight + "px monospace";
 
-        var offsetX = this.min_x * cellWidth * -1;
-        var offsetY = this.min_y * cellHeight * -1;
+        var offsetX = this.minX * cellWidth * -1;
+        var offsetY = this.minY * cellHeight * -1;
 
         for (var i = 0; i < cursors.length; i++) {
             cursors[i].drawContext(
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.