<!-- RUBY-BACKUP · part 394 -->
+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$/);  /* absolute with size+height */
  if(am4){
    var key4 = codeToShapeKey(am4[1]); if(!key4) return {ok:false, error:'unknown code'};
    var nx4 = snap(+am4[3]), nz4 = snap(+am4[4]);
    ec.x = nx4; ec.z = nz4;
    return {ok:true, ops:[{shape:key4, size:parseInt(am4[2],10), x:nx4, z:nz4, y:parseInt(am4[5],10), proc:null}]};
  }
  var am = t.match(/^([a-zA-Z])\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$/);  /* absolute with size */
  if(am){
    var key3 = codeToShapeKey(am[1]); if(!key3) return {ok:false, error:'unknown code'};
    var nx2 = snap(+am[3]), nz2 = snap(+am[4]);
    ec.x = nx2; ec.z = nz2;
    return {ok:true, ops:[{shape:key3, size:parseInt(am[2],10), x:nx2, z:nz2, y:0, proc:null}]};
  }
  var am2 = t.match(/^([a-zA-Z])\s+(-?\d+)\s+(-?\d+)$/);    /* absolute, size 1 */
  if(am2){
    var key4 = codeToShapeKey(am2[1]); if(!key4) return {ok:false, error:'unknown code'};
    var nx3 = snap(+am2[2]), nz3 = snap(+am2[3]);
    ec.x = nx3; ec.z = nz3;
    return {ok:true, ops:[{shape:key4, size:1, x:nx3, z:nz3, y:0, proc:null}]};
  }
  return {ok:false, error:'cannot read line'};
}

/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   MARK: PROCEDURES — the library. Each takes ec (cursor), returns ops.
   EDIT: add/rename here. Law of the build: define HALF, let flip do the rest.
   mass = total rubies (for MARK: LAW OF GROUND). mkOp keeps ops uniform.
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
function mkOp(shapeKey, size, x, z, y){ return {shape:shapeKey, size:size, x:x, z:z, y:y||0, proc:null}; }
var PROCEDURES = {
  tower: function(ec){ return [mkOp('cyl', 8, ec.x, ec.z)]; },
  wall:  function(ec){ var o=[]; for(var i=0;i<4;i++) o.push(mkOp('cube', 4, ec.x+i*4, ec.z)); return o; },
  keep:  function(ec){ return [mkOp('cube', 12, ec.x, ec.z)]; },
  gate:  function(ec){ return [mkOp('cyl', 6, ec.x-4, ec.z), mkOp('cyl', 6, ec.x+4, ec.z)]; },
  square:function(ec){
    var o=[], pts=[[2,0],[4,0],[4,-2],[4,-4],[2,-4],[0,-4],[0,-2]];
    for(var i=0;i<pts.length;i++) o.push(mkOp(curShapeKey, 1, ec.x+pts[i][0], ec.z+pts[i][1]));
    return o;
  },
  'castle-west': function(ec){
    return [
      mkOp('cyl', 8, ec.x+10, ec.z+10),
      mkOp('cyl', 8, ec.x+10, ec.z-10),
      mkOp('cube', 4, ec.x+10, ec.z),
      mkOp('cube', 12, ec.x, ec.z+2)
    ];
  },
  castle: function(ec){
    var west = PROCEDURES['castle-west'](ec);
    return west.concat(mirrorOps(west, ec.x));
  },
  /* a real castle — west half; flip does the east. crenellated curtains,
     gatehouse south, the great keep with turrets. ghost openings: windows
     set into the walls, a door standing in the gate passage. */
  'castle-real-west': function(ec){
    return [
      mkOp('cyl', 10, ec.x+14, ec.z+14),
      mkOp('cyl', 10, ec.x+14, ec.z-14),
      mkOp('cube', 4, ec.x+14, ec.z-10),
      mkOp('cube', 6, ec.x+14, ec.z-6),
      mkOp('cube', 4, ec.x+14, ec.z-2),
      mkOp('cube', 6, ec.x+14, ec.z+2),
      mkOp('cube', 4, ec.x+14, ec.z+6),
      mkOp('cube', 6, ec.x+14, ec.z+10),
      mkOp('cube', 4, ec.x+10, ec.z-14),
      mkOp('cube', 6, ec.x+6, ec.z-14),
      mkOp('cube', 4, ec.x+2, ec.z-14),
      mkOp('cube', 4, ec.x+10, ec.z+14),
      mkOp('cube', 6, ec.x+6, ec.z+14),
      mkOp('cyl', 7, ec.x+4, ec.z+14),
      mkOp('cube', 14, ec.x, ec.z-4),
      mkOp('cyl', 6, ec.x+3, ec.z-7),
      mkOp('cyl', 6, ec.x+3, ec.z-1),
      mkOp('cube~ghost', 2, ec.x+14, ec.z-6, 2),
      mkOp('cube~ghost', 2, ec.x+14, ec.z+6, 2),
      mkOp('cube~ghost', 2, ec.x+6, ec.z-14, 2),
      mkOp('cube~ghost', 2, ec.x+6, ec.z+14, 2),
      mkOp('cube~ghost', 4, ec.x, ec.z+14, 0)
    ];
  },
  'castle-real': function(ec){
    var west = PROCEDURES['castle-real-west'](ec);
    return west.concat(mirrorOps(west, ec.x));
  },
  /* a fence — posts with sphere caps. anchor a cap with oops-my-bad,
     then `swap p` for pointed pyramids, `swap v` for cones. */
  fence: function(ec){
    var o = [];
    for(var i=0;i<4;i++){
      o.push(mkOp('cube', 4, ec.x+i*2, ec.z));
      o.push(mkOp('sphere', 2, ec.x+i*2, ec.z, 4));
    }
    return o;
  },
  /* solid1 — Solid's first: The Lean. A plinth, a wedge buttress, the rise,
     the bend, and the crown. Five stones, the whole family in one column. */
  /* baby step — one step for now, for my children; we grow it together */
  babystep: function(ec){
    return [mkOp('cube', 15, ec.x, ec.z, 0)];
  },
  solid1: function(ec){
    return [
      mkOp('cube', 6, ec.x, ec.z),
      mkOp('tri', 4, ec.x-2, ec.z),
      mkOp('pyramid', 6, ec.x, ec.z, 6),
      mkOp('poff', 6, ec.x, ec.z, 12),
      mkOp('pdown', 4, ec.x, ec.z, 18)
    ];
  },
  /* cas1 — THE FULL CASTLE. west half; flip does the east.
     Continuous walls, ghost windows standing IN the wall line,
     gatehouse south, the great keep. Presence, not ground plan. */
  'cas1-west': function(ec){
    var o = [];
    o.push(mkOp('cyl', 24, ec.x+18, ec.z+18));
