§ 4. The Snake Constructor Function

Shize? I should shee! Macool, Macool, orra whyi deed ye diie?
of a trying thirstay mournin? Sobs they sighdid at Fillagain’s
chrissormiss wake, all the hoolivans of the nation, prostrated in
their consternation, and their duodisimally profusive plethora of
ululation.

[Click.]

  1. Constructor Function: Snake
function Snake(options) {

  "use strict";

  /* Private Fields */

  const parameters = assign({}, Snake.defaultParameters);
  Object.freeze(assign(parameters, options));

  const
    thisSnake = this,
    delay = parameters.delay,
    initialSnakeLength = parameters.length,
    snakeWidth = parameters.width,
    eyeColor = parameters.eyeColor,
    stepSize = parameters.width,
    snakeBody = new List(),
    methods = {},
    steps = new List(),
    callStack = new Stack(),
    keyCodeAngleMap = (function () {
      const map = Object.create(null);
      map[KeyCode.UpArrow] = Snake.North;
      map[KeyCode.DownArrow] = Snake.South;
      map[KeyCode.RightArrow] = Snake.East;
      map[KeyCode.LeftArrow] = Snake.West;
      return Object.freeze(map);
    })();

  let thisSnakeIsDead = false,
    commands = new Queue,
    angle = Snake.South,
    sT = 1,
    sL = 0,
    timeoutId = undefined,
    skinColor = parameters.skinColor;

  let commandSet = commands;

  /* Public Properties */

  Object.defineProperties(this, {
    skinColor: { get: getSkinColor, set: setSkinColor },
    isHalted: { get: isHalted },
    isSleeping: { get: isSleeping },
    isDead: { get: isDead },
    isBitingOwnBody: { get: isBitingOwnBody },
    isBitingBodyOf: { value: isBitingBodyOf },
    isBodyBeingBittenBy: { value: isBodyBeingBittenBy } 
  });

  /* Public Mutator Methods */

  this.go = go;
  this.stop = halt;
  this.grow = grow;
  this.kill = die;
  this.goNorth = function (n) { return go(n, Snake.North); };
  this.goSouth = function (n) { return go(n, Snake.South); };
  this.goEast = function (n) { return go(n, Snake.East); };
  this.goWest = function (n) { return go(n, Snake.West); };
  this.sleep = sleep;
  this.wake = wake;
  this.pause = function(ms) { return interrupt().sleep(ms); }
  this.beginMethod = beginMethod;
  this.endMethod = endMethod;
  this.doMethod = doMethod;
  this.ifTrue = ifTrue;
  this.whileTrue = whileTrue;

  /* Initialize new snake. */

  (function initialize() {
    createSnake(initialSnakeLength);
    window.addEventListener('keydown', function (e) {
      if (commands.length > 0)
        commands.clear();
      if (callStack.length > 0)
        callStack.clear();
      angle = keyCodeAngleMap[e.keyCode];
      if (isHalted())
        poke();
    });
  })();

  /* Return new snake. */

  return Object.freeze(this);

  /* Private Utilities */

  // Operations

  function wake() {
    angle = getHead().angle;
    poke();
  }
  function poke() {
    if (thisSnakeIsDead) return;
    if (commands.length === 0 && callStack.length > 0) {
      commands = callStack.pop();
    }
    if (commands.length > 0) {
      angle = commands.remove().call();
    }
    if (angle === undefined) {
      halt();
    }
    else {
      performNextStep();
      if (isBitingOwnBody()) {
        die();
      }
      else {
        timeoutId = window.setTimeout(poke, delay);
      }
    }
    return angle;
  }
  function halt() {
    if (!isHalted()) {
      window.clearTimeout(timeoutId);
      timeoutId = undefined;
    }
  }
  function die() {
    thisSnakeIsDead = true;
    thisSnake.skinColor = 'transparent';
  }
  function go(n, a) {
    if (a === undefined) a = angle;
    if (!isFinite(n)) n = 0;
    while (n-- > 0) {
      commandSet.add(function () { return a; });
    }
    if (isHalted() && !isBeingProgrammed()) poke();
    return thisSnake;
  }
  function performNextStep() {
    sT = sL = 0;
    switch(angle) {
      case Snake.North: sT = -1; break;
      case Snake.South: sT = 1; break;
      case Snake.East: sL = 1; break;
      case Snake.West: sL = -1; break;
    }
    step(sT*stepSize, sL*stepSize, angle);
  }
  function step(distTop, distLeft, angle) {
    if (distTop === 0 && distLeft === 0) return;
    steps.addAtTail({
      distT: distTop,
      distL: distLeft,
      angle: angle}
    );
    moveSegments();
  }
  function moveSegments() {
    const n = snakeBody.length;
    let m = steps.length;
    for (let i = 0; i < n && m-- > 0; i++) {
      const snakeSegment = snakeBody.elementAt(i);
      const step = steps.elementAt(m);
      snakeSegment.top += step.distT;
      snakeSegment.left += step.distL;
      snakeSegment.rotate(step.angle);
    }
    moveBackBone();
  }
  function moveBackBone() {
    const n = snakeBody.length;
    let m = steps.length;
    for (let i = 1; i < n && m-- > 0; i++) {
      const curr = getTransformValue(i);
      const prev = getTransformValue(i - 1);
      if (curr !== prev) { // Turning.
        if (turnedRight(curr, prev))
          rotateVertebrae(i, Angle.quarterTurnLeft);
        else if (turnedLeft(curr, prev))
          rotateVertebrae(i, Angle.quarterTurnRight);
        else if (reversed(curr, prev))
          rotateVertebrae(i, Angle.halfTurn);
        if (i === (n - 1)) { // Straighten tail.
          const snakeSegment = snakeBody.elementAt(i);
          rotateVertebrae(i, Angle.straight);
          snakeSegment.transform = prev;
        }
      }
      else { // Going straight.
        rotateVertebrae(i, Angle.straight);
      }
    }
  }
  function rotateVertebrae(n, angle) {
    snakeBody.elementAt(n).vertebrae2.rotate(angle);
  }
  function setSkinColor(color) {
    skinColor = color;
    snakeBody.forEach(function (segment) {
      segment.skin.backgroundColor = color;
    });
  }

  // Values

  function getHead() {
    return snakeBody.elementAt(0);
  }
  function getSkinColor() {
    return skinColor;
  }
  function isBeingProgrammed() {
    return commandSet !== commands;
  }
  function isHalted() {
    return timeoutId === undefined;
  }
  function isDead() {
    return thisSnakeIsDead;
  }
  function isSleeping() {
    return isHalted() && !isDead();
  }
  function isBitingOwnBody() {
    return isBitingBodyOf(thisSnake);
  }
  function isBodyBeingBittenBy(head) {
    const n = snakeBody.length;
    for (let i = 1; i < n; i++) {
      if (snakeBody.elementAt(i).intersectsWith(head))
        return true;
    }
    return false;    
  }
  function isBitingBodyOf(snake) {
    return snake.isBodyBeingBittenBy(getHead());
  }
  function getTransformValue(n) {
    return (n < 0) ? undefined : snakeBody.elementAt(n).transform;
  }
  function turnedRight(curr, prev) {
    switch (prev) {
      case Snake.GoingNorth: return curr === Snake.GoingEast;
      case Snake.GoingSouth: return curr === Snake.GoingWest;
      case Snake.GoingEast: return curr === Snake.GoingSouth;
      case Snake.GoingWest: return curr === Snake.GoingNorth;
    }
    return false;
  }
  function turnedLeft(curr, prev) {
    switch (prev) {
      case Snake.GoingNorth: return curr === Snake.GoingWest;
      case Snake.GoingSouth: return curr === Snake.GoingEast;
      case Snake.GoingEast: return curr === Snake.GoingNorth;
      case Snake.GoingWest: return curr === Snake.GoingSouth;
    }
    return false;
  }
  function reversed(curr, prev) {
    switch (prev) {
      case Snake.GoingNorth: return curr === Snake.GoingSouth;
      case Snake.GoingSouth: return curr === Snake.GoingNorth;
      case Snake.GoingEast: return curr === Snake.GoingWest;
      case Snake.GoingWest: return curr === Snake.GoingEast;
    }
    return false;
  }

  // Programming

  function interrupt() {
    if (commands.length > 0) {
      callStack.push(commands);
      commands = new Queue();
    }
    return thisSnake;
  }
  function isTrue(expression) {
    if (typeof expression === 'function')
      return expression() === true;
    else
      return expression === true;
  }
  function fetchNextCommand() {
    return commands.remove();
  }
  function skipNextCommand() {
    fetchNextCommand();
  }
  function executeNextCommand() {
    return fetchNextCommand().call();
  }
  function ifTrue(expression) {
    commandSet.add(function () {
      if (!isTrue(expression)) {
        skipNextCommand();
      }
      return executeNextCommand();
    });
  }
  function whileTrue(expression) {
    const whileLoop = 
    function () {
      const command = fetchNextCommand();
      if (isTrue(expression)) {
        interrupt;
        commands.add(command);
        commands.add(whileLoop);
        commands.add(command);
      }
      return executeNextCommand();
    };
    commandSet.add(whileLoop);
    return thisSnake;
  }
  function doMethod(name) {
    commandSet.add(function () {
      interrupt();
      loadMethod(name);
      return executeNextCommand();
    });
    if (isHalted() && !isBeingProgrammed()) {
      poke();
    }
    return thisSnake;
  }
  function sleep(ms) {
    commandSet.add(function () {
      if (Number.isFinite(ms)) {
        window.setTimeout(wake, ms);
      }
      return undefined;
    });
    return thisSnake;
  }
  function parseMethodName(name) {
    return (name === undefined) ? 'anonymous' : name;
  }
  function beginMethod(name) {
    name = parseMethodName(name);
    commandSet = methods[name] = new Queue();
    return thisSnake;
  }
  function endMethod() {
    commandSet = commands;
    return thisSnake;
  }
  function loadMethod(name) {
    name = parseMethodName(name);
    const method = methods[name];
    if (method !== undefined && method.length !== undefined) {
      let n = method.length;
      while (n-- > 0) {
        const cmd = method.remove();
        commands.add(cmd);
        method.add(cmd);
      }
    }
    return thisSnake;
  }

  // Create

  function grow(n) {
    repeat(appendBodySegment, n === undefined ? 1 : n);
    return thisSnake;
  }
  function createSnake(n) {
    createHeadSegment();
    return grow(n-1);
  }
  function createHeadSegment() {
    const head = createHead();
    head.appendChild(head.skin = createFace());
    return attachToSnake(head);
  }
  function appendBodySegment() {
    const segment = createBackboneSegment();
    segment.appendChild(segment.skin = createSkin());
    return attachToSnake(segment);
  }
  function attachToSnake(segment) {
    let last, top = 0, left = 0;
    if (snakeBody.length > 0) {
      last = snakeBody.elementAt(snakeBody.length - 1);
      top = last.top;
      left = last.left;
    }
    snakeBody.addAtTail(segment);
    segment.zIndex = 1000 - snakeBody.length;
    segment.render();
    step(sT * stepSize, sL * stepSize, angle);
    segment.top = top;
    segment.left = left;
    return segment;
  }
  function createSkin() {
    return new Shape({
      width: snakeWidth + Snake.units,
      height: snakeWidth + Snake.units,
      backgroundColor: skinColor
    });
  }
  function createVertebrae(propertyName) {
    const width = snakeWidth / 8;
    const style = {
      left: ((snakeWidth - width) / 2) + Snake.units,
      width: width + Snake.units,      
      height: (snakeWidth / 2) + Snake.units,
      backgroundColor: 'white'
    };
    style[propertyName] = 0;
    return new Shape(style);
  }
  function createIntervertebralDisc() {
    const width = snakeWidth / 6;
    return new Shape({
      left: ((snakeWidth - width) / 2) + Snake.units,
      top: ((snakeWidth - width) / 2) + Snake.units,
      width: width + Snake.units,      
      height: width + Snake.units,
      backgroundColor: 'white',
      borderRadius: '50%',
    });
  }
  function createBackground(color) {
    return new Shape({
      width: snakeWidth + Snake.units,
      height: snakeWidth + Snake.units,
      backgroundColor: color
    });
  }
  function createBackboneSegment() {
    const background = createBackground('black');
    background.vertebrae2 = createBackground('transparent');
    background.vertebrae2.appendChild(createVertebrae('bottom'))
    background.appendChild(background.vertebrae2);
    background.appendChild(createVertebrae('top'));
    return background.appendChild(createIntervertebralDisc());
  }
  function createSkull() {
    const skull = new Shape({
      width: snakeWidth + Snake.units,      
      height: snakeWidth + Snake.units,
      bottom: 0,
      backgroundColor: 'white',
      borderRadius: '45%'
    });
    return addEyes(skull, 'black');
  }
  function createHead() {
    const background = createBackground('black');
    background.appendChild(createTongue(background));
    return background.appendChild(createSkull());
  }
  function createFace() {
    return addEyes(createSkin(), eyeColor);
  }
  function createTongue(head) {
    const tongueLength = head.height / 2;
    const tongueWidth = head.width / 5;
    return new Shape({
      width: tongueWidth + Snake.units,
      height: tongueLength + Snake.units,
      top: head.height + Snake.units,
      left: ((head.width / 2) - (tongueWidth / 2)) + Snake.units,
      backgroundColor: 'red',
      // Bifurcated (forked) tongue:
      clipPath: 'polygon(0% 0%, 100% 0%, 100% 100%, 50% 70%, 0% 100%)'
    });
  }
  function createEye(eyeColor) {
    const eyeDiameter = snakeWidth / 5;
    return new Shape({
      borderRadius: '50%',
      width: eyeDiameter + Snake.units,
      height: eyeDiameter + Snake.units,
      backgroundColor: eyeColor
    });
  }
  function addEyes(face, eyeColor) {
    const leftEye = createEye(eyeColor);
    const rightEye = createEye(eyeColor);
    const eyeInset = face.width / 6;
    const eyeDepth = 3 * face.height / 7;
    rightEye.right = leftEye.left = eyeInset + Snake.units;
    rightEye.bottom = leftEye.bottom = eyeDepth + Snake.units;
    face.appendChild(leftEye);
    return face.appendChild(rightEye);
  }
}

/* Snake Constants */

Object.defineProperties(Snake, {
  units: {value: 'px'},
  North: {value: Angle.halfTurn},
  South: {value: Angle.straight},
  East: {value: Angle.quarterTurnLeft},
  West: {value: Angle.quarterTurnRight}
});

Object.defineProperties(Snake, {
  GoingNorth: {value: 'rotate(' + Snake.North + ')'},
  GoingSouth: {value: 'rotate(' + Snake.South + ')'},
  GoingEast: {value: 'rotate(' + Snake.East + ')'},
  GoingWest: {value: 'rotate(' + Snake.West + ')'}
});

Snake.defaultParameters = Object.freeze({
  delay: 500,
  length: 4,
  width: 20,
  skinColor: 'green',
  eyeColor: 'blue'
});