/*************/ /* 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;