<!-- RUBY-BACKUP · part 146 -->
(p, ec){
  var t = p.trim();
  if(/^x$/i.test(t)) return {ok:true, ops:[{remove:true, x:ec.x, z:ec.z}]};
  var xm = t.match(/^x\s+(-?\d+)\s+(-?\d+)$/i);
  if(xm) return {ok:true, ops:[{remove:true, x:snap(+xm[1]), z:snap(+xm[2])}]};
  var rm = t.match(/^([a-zA-Z])(\d*)\s*(\d+)([rlud])$/);   /* relative: b2r, l8 2r */
  if(rm){
    var key = codeToShapeKey(rm[1]); if(!key) return {ok:false, error:'unknown code'};
    var size = rm[2] ? parseInt(rm[2],10) : 1;
    var n = parseInt(rm[3],10), d = DIRS[rm[4]];
    var nx = ec.x + d[0]*n*BLOCK, nz = ec.z + d[1]*n*BLOCK;
    ec.x = nx; ec.z = nz;
    return {ok:true, ops:[{shape:key, size:size, x:nx, z:nz, y:0, proc:null}]};
  }
  var bm = t.match(/^([a-zA-Z])$/);                        /* bare: seed at cursor */
  if(bm){
    var key2 = codeToShapeKey(bm[1]); if(!key2) return {ok:false, error:'unknown code'};
    return {ok:true, ops:[{shape:key2, size:1, x:ec.x, z:ec.z, y:0, proc:null}]};
  }
  var sm = t.match(/^([a-zA-Z])(\d*)\^$/);                   /* stack: on the stone at cursor */
  if(sm){
    var keyS = codeToShapeKey(sm[1]); if(!keyS) return {ok:false, error:'unknown code'};
    var sizeS = sm[2] ? parseInt(sm[2],10) : 1;
    var top = 0;
    for(var si=0; si<stones.children.length; si++){
      var ss = stones.children[si], su = ss.userData;
      if(Math.abs(ss.position.x-ec.x)<0.01 && Math.abs(ss.position.z-ec.z)<0.01)
        top = Math.max(top, (su.y||0) + (su.rubies||1)*(su.k1||1));
    }
    return {ok:true, ops:[{shape:keyS, size:sizeS, x:ec.x, z:ec.z, y:top, proc:null}]};
  }
  var am4 = t.match(/^([a-zA-Z])\s+(-?\d+)\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