/*************/
/* UTILITIES */
/*************/
/**
* Returns true if the character of string s at position i is a digit;
* otherwise, returns false.
*/
function isDigit(s, i) {
return CharCode.isDigit(s.charCodeAt(i));
}
/**
* Calls function f n times.
*/
function repeat(n, f) {
while (n-- > 0) f();
}
function assign(target, source) {
for (let propertyName in source) {
target[propertyName] = source[propertyName];
}
return target;
}
/**
* Returns the index of the beginning of the longest suffix of string s
* that does not contain any digits.
*/
function indexOfLongestSuffixSansDigits(s) {
let i = s.length - 1;
while (i > 0 && !isDigit(s, i)) {
i--;
}
return i + 1;
}
const CharCode = Object.freeze({
Zero: '0'.charCodeAt(0),
Nine: '9'.charCodeAt(0),
isDigit: function(code) {
return CharCode.Zero <= code && code <= CharCode.Nine;
}
});
const KeyCode = Object.defineProperties({}, {
Space: {value: 32},
LeftArrow: {value: 37},
UpArrow: {value: 38},
RightArrow: {value: 39},
DownArrow: {value: 40}
});
const Angle = Object.defineProperties({}, {
halfTurn: {value: '180deg'},
straight: {value: '0deg'},
quarterTurnLeft: {value: '-90deg'},
quarterTurnRight: {value: '90deg'}
});
const Measure = {
indexOfUnits: function (string) {
return indexOfLongestSuffixSansDigits(string);
},
parseNumber: function (value) {
const number = parseFloat(value);
return (isFinite(number)) ? number : 0;
},
units: function (string) {
return string.slice(Measure.indexOfUnits(string));
},
separate: function (string) {
const i = Measure.indexOfUnits(string);
return {
number: Measure.parseNumber(string.slice(0, i)),
units: string.slice(i)
};
}
};
const FunctionCall = {
arguments: {
value: function (string, fname) {
if (fname === undefined) {
fname = "";
}
const s = fname + '(';
const i = string.indexOf(s);
const j = string.indexOf(')', i);
return i < 0 || j < 0 ?
"" : string.substring(i + s.length, j);
}
}
};
/**************/
/* PROTOTYPES */
/**************/
Element.prototype = Object.defineProperties({}, {
measureProperties: {
value: Object.freeze([
'top', 'bottom',
'left', 'right',
'width', 'height',
'borderWidth', 'borderRadius'
])
},
simpleProperties: {
value: Object.freeze([
'transform',
'backgroundColor',
'zIndex'
])
},
defaultStyle: {
value: {}
},
containsPoint: {
value: function(x, y) {
return x > this.left &&
x < this.left + this.width &&
y > this.top &&
y < this.top + this.height;
}
},
containsCornerOrMiddle: {
value: function(o) {
const top = o.top,
left = o.left,
width = o.width,
height = o.height,
right = left + width,
bottom = top + height,
xMid = (left + right) / 2,
yMid = (top + bottom) / 2;
return this.containsPoint(xMid, yMid) ||
this.containsPoint(xMid, top) ||
this.containsPoint(right, yMid) ||
this.containsPoint(xMid, bottom) ||
this.containsPoint(left, yMid) ||
this.containsPoint(left, top) ||
this.containsPoint(right, top) ||
this.containsPoint(left, bottom) ||
this.containsPoint(right, bottom);
}
},
intersectsWith: {
value: function(o) {
return this.containsCornerOrMiddle(o) ||
o.containsCornerOrMiddle(this);
}
}
});
Shape.prototype =
Object.defineProperties(Object.create(Element.prototype), {
defaultStyle: {
value: Object.freeze({
backgroundColor: 'black',
height: '100px',
width: '100px',
position: 'absolute'
})
}
});
/*************************/
/* CONSTRUCTOR FUNCTIONS */
/*************************/
function Element(tagName, style) {
/* Private Fields */
const thisElement = this;
const element = document.createElement(tagName);
/* Public Properties */
this.measureProperties.forEach(function (propertyName) {
Object.defineProperty(thisElement, propertyName, {
get: function () { return getNumberPart(propertyName); },
set: function (value) { setMeasure(propertyName, value); }
});
});
this.simpleProperties.forEach(function (propertyName) {
Object.defineProperty(thisElement, propertyName, {
get: function () { return element.style[propertyName]; },
set: function (value) { element.style[propertyName] = value; }
});
});
Object.defineProperty(thisElement, 'angle', {
get: function () {
return FunctionCall.arguments(thisElement.style.transform, 'rotate');
}
});
/* Public Methods */
this.render = function () {
document.body.appendChild(element);
return thisElement;
};
this.rotate = function (amt) {
element.style.transform = 'rotate(' + amt + ')';
return thisElement;
};
this.appendChild = function (otherElement) {
if (otherElement instanceof Element)
otherElement.appendTo(element);
else
element.appendChild(otherElement);
return thisElement;
};
this.appendTo = function (otherElement) {
otherElement.appendChild(element);
return thisElement
};
/* Initialization */
(function initialize() {
const thisStyle = element.style;
assign(thisStyle, thisElement.defaultStyle);
assign(thisStyle, style);
})();
/* Private Utilities */
function getNumberPart(propertyName) {
return Measure.separate(element.style[propertyName]).number;
}
function setMeasure(property, value) {
const m = Measure.separate(value.toString());
const units = m.units.length > 0 ?
m.units : Measure.units(element.style[property]);
element.style[property] = m.number + units;
}
}
function Shape(style) {
Element.call(this, 'div', style);
}
function List() {
let list = [];
Object.defineProperty(this, 'length', {
get: function () { return list.length; }
});
this.elementAt = function (i) { return list[i]; };
this.addAtHead = function (e) { return list.unshift(e); };
this.addAtTail = function (e) { return list.push(e); };
this.removeAtHead = function () { return list.shift(); };
this.removeAtTail = function () { return list.pop(); };
this.clear = function () {
const oldList = list;
list = [];
return oldList;
};
this.forEach = function (f) { return list.forEach(f); };
this.forSome = function (f, lower, upper) {
if (lower === undefined) lower = 0;
if (upper === undefined) upper = list.length;
for (let i = lower; i < upper; i++) f(list[i]);
};
}
function Queue() {
const list = new List();
Object.defineProperty(this, 'length', {
get: function () { return list.length; }
});
this.add = list.addAtTail;
this.remove = list.removeAtHead;
this.clear = list.clear;
}
function Stack() {
const list = new List();
Object.defineProperty(this, 'length', {
get: function () { return list.length; }
});
this.push = list.addAtHead;
this.pop = list.removeAtHead;
this.clear = list.clear;
}
/**********/
/* SNAKES */
/**********/
/* 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'
});
/* Snake Constructor */
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) {
if (n === undefined) n = 1;
repeat(n, appendBodySegment);
}
function createSnake(n) {
createHeadSegment();
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, left;
if (snakeBody.length > 0) {
last = snakeBody.elementAt(snakeBody.length - 1);
top = last.top;
left = last.left;
}
snakeBody.addAtTail(segment);
segment.zIndex = -snakeBody.length;
segment.render();
step(sT * stepSize, sL * stepSize, angle);
if (last !== undefined) {
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,
height: tongueLength + 'px',
top: head.height + 'px',
left: ((head.width / 2) - (tongueWidth / 2)) + 'px',
backgroundColor: 'red',
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;
rightEye.bottom = leftEye.bottom = eyeDepth;
face.appendChild(leftEye);
return face.appendChild(rightEye);
}
}
/* Various Types of Snakes */
function GreenSnake() {
Snake.call(this, {
delay: 800,
length: 10,
width: 25,
skinColor: 'green',
eyeColor: 'blue'
});
}
function BlueSnake() {
Snake.call(this, {
delay: 400,
length: 20,
width: 15,
skinColor: 'blue',
eyeColor: 'yellow'
});
}
function OrangeSnake() {
Snake.call(this, {
delay: 100,
length: 8,
width: 20,
skinColor: 'orange',
eyeColor: 'purple'
});
}
function RedEyeSnake() {
Snake.call(this, {
delay: 600,
length: 12,
width: 18,
skinColor: 'transparent',
eyeColor: 'red'
});
}
const DefaultSnake = Snake;
const LongSnake = BlueSnake;
const ShortSnake = OrangeSnake;
const FastSnake = OrangeSnake;
const SlowSnake = GreenSnake;
const BareBonesSnake = RedEyeSnake;