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.
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.]
- 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'
});