<commit_before>// TODO: better error handling
// TODO: Strict mode?
// TODO: Test in browser
// TODO: Tests

module.exports = {
    transform: transform
};

var RULE_REGEXP = /(#[\w\.\*\|=\(\)\[\]\-\d\^'\\]+)/g;
var SIMPLE_RULE = /([\w\.]+)/;

var ARRAY_INDEX_RULE = /\[([^\]]*)\]/;
var CAST_NAME_RULE = /\(([^\)]*)\)/;
var FILTER_RULE = /\|(\w+)=([\w']+)/;
var MAP_RULE = /\*(\w+)/;
var EXPAND_RULE = /\^(.*)/;

function transform (src, transformationObject) {
    var result = {};

    Object.keys(transformationObject).forEach(function(key) {
        if (typeof transformationObject[key] === 'object') {
            result[key] = transform(src, transformationObject[key]);
        }
        else if (isRule(transformationObject[key])) {
            result[key] = process(src, transformationObject[key])
        }
        // TODO: do something smart with version
        else if (key !== '@jsonlt') {
            result[key] = transformationObject[key];
        }
    });

    return result;
}

function isRule(rule) {
    if (typeof rule !== 'string') return false;

    // TODO: do something better
    if (rule.indexOf('#') === -1) return false;
    return true;
}

function process(src, rulesString) {
    var output = rulesString;

    var rules = rulesString.match(RULE_REGEXP);
    rules.forEach(function(rule) {
        var rawRule = rule;
        rule = rule.trim();

        // Array index like #orders[-1]
        var arrayIndexMatch = rule.match(ARRAY_INDEX_RULE);

        // Casting to type like #(string)field
        var castMatch = rule.match(CAST_NAME_RULE);
        if (castMatch) rule = rule.replace(castMatch[0], '');

        // Filer #field|type='cd'
        var filterMatch = rule.match(FILTER_RULE);

        // Map expand #field*sup
        var mapMatch = rule.match(MAP_RULE);

        // Expand #field^delimeter
        // TODO: Better comma handling
        var expandMatch = rule.match(EXPAND_RULE);
        if (expandMatch && expandMatch[1] === '') expandMatch[1] = ',';

        var match = rule.match(SIMPLE_RULE);
        var path = match[1].replace(/#/,'').trim();
        var value = getFromSource(src, path);

        if (arrayIndexMatch) value = getArrayIndex(value, arrayIndexMatch);
        if (filterMatch) value = filter(value, filterMatch);
        if (mapMatch) value = mapExpand(value, mapMatch);
        if (expandMatch) value = expand(value, expandMatch);

        // Casting should be the last
        if (castMatch) value = castToType(value, castMatch);

        if (output.length === rawRule.length) output = value;
        else output = output.replace(rawRule, value);

    });
    return output;
}

function getFromSource(src, path) {
    var value = JSON.parse(JSON.stringify(src));
    var parts = path.split('.');
    var _buffer;

    while (parts.length) {
        _buffer = parts.shift();
        if (typeof value[_buffer] !== 'undefined') value = value[_buffer];
        else return '';
    }
    return value;
}

function getArrayIndex(value, matchObject) {

    if (Array.isArray(value)) {
        var index = parseInt(matchObject[1], 10);

        // support for negative indexes
        if (index < 0) index = value.length + index;
        if (typeof value[index] !== 'undefined') return value[index];
    }
    return '';
}

function castToType(value, matchObject) {
    if (typeof value === 'undefined') return;

    if (matchObject[1] === 'string') return value.toString();
    else if (matchObject[1] === 'number') return parseFloat(value);

    // TODO: add proper error message
    else new TypeError('Available casts: string, number');

    return '';
}

function filter(value, matchObject) {
    var key = matchObject[1],
        filterValue = matchObject[2].replace(/('|\\|")/g,'');

    if (Array.isArray(value)) {
        return value.filter(function(item) { return item[key] === filterValue});
    }
    return '';
}

function mapExpand(value, matchObject) {
    if (Array.isArray(value)) {
        return value.map(function(item) { return item[matchObject[1]]});
    }
    return '';
}

function expand(value, matchObject) {
    if (typeof value !== 'object') return '';
    var delimiter = matchObject[1];

    delimiter = delimiter.replace(/\\s/g, ' ');

    var result = [];
    Object.keys(value).forEach(function(fieldName) { result.push(value[fieldName]); });
    return result.join(delimiter);
}<commit_msg>refactoring<commit_after>(function() {

    var RULE_REGEXP = /(#[\w\.\*\|=\(\)\[\]\-\d\^'\\]+)/g;
    var SIMPLE_RULE = /([\w\.]+)/;

    var ARRAY_INDEX_RULE = /\[([^\]]*)\]/;
    var CAST_NAME_RULE = /\(([^\)]*)\)/;
    var FILTER_RULE = /\|(\w+)=([\w']+)/;
    var MAP_RULE = /\*(\w+)/;
    var EXPAND_RULE = /\^(.*)/;

    // These things were stolen from Underscore:
    // https://github.com/jashkenas/underscore/blob/master/underscore.js

    // Establish the root object, `window` in the browser, or `exports` on the server.
    var root = this;

    // Export the JSONLT object for **Node.js**, with
    // backwards-compatibility for the old `require()` API. If we're in
    // the browser, add `_` as a global object.
    if (typeof exports !== 'undefined') {
        if (typeof module !== 'undefined' && module.exports) {
            exports = module.exports = JSONLT;
        }
        exports.JSONLT = JSONLT;
    } else {
        root.JSONLT = JSONLT;
    }

    function JSONLT(options) {
        options = options || {};

        this.options = options;
        this.options.strict = options.strict || false;

        this.transform = function (src, transformationObject) {
            var self = this,
                result = {};

            Object.keys(transformationObject).forEach(function (key) {
                if (typeof transformationObject[key] === 'object') {
                    result[key] = self.transform(src, transformationObject[key]);
                }
                else if (isRule(transformationObject[key])) {
                    result[key] = self.process(src, transformationObject[key])
                }
                // TODO: do something smart with version
                else if (key !== '@jsonlt') {
                    result[key] = transformationObject[key];
                }
            });

            return result;
        };

        function isRule(rule) {
            if (typeof rule !== 'string') return false;

            // TODO: do something better
            if (rule.indexOf('#') === -1) return false;
            return true;
        }

        this.error = function (message) {
            if (this.options.strict) throw new Error(message + ' (JSON path: ' + this.lastPath + ')');
            return '';
        };

        this.process = function (src, rulesString) {
            var self = this,
                output = rulesString,
                rules = rulesString.match(RULE_REGEXP);

            rules.forEach(function (rule) {
                var rawRule = rule;
                rule = rule.trim();

                // Array index like #orders[-1]
                var arrayIndexMatch = rule.match(ARRAY_INDEX_RULE);

                // Casting to type like #(string)field
                var castMatch = rule.match(CAST_NAME_RULE);
                if (castMatch) rule = rule.replace(castMatch[0], '');

                // Filer #field|type='cd'
                var filterMatch = rule.match(FILTER_RULE);

                // Map expand #field*sup
                var mapMatch = rule.match(MAP_RULE);

                // Expand #field^delimeter
                // TODO: Better comma handling
                var expandMatch = rule.match(EXPAND_RULE);
                if (expandMatch && expandMatch[1] === '') expandMatch[1] = ',';

                var match = rule.match(SIMPLE_RULE);
                var path = match[1].replace(/#/, '').trim();
                var value = self.getByPath(src, path);

                if (arrayIndexMatch) value = self.getArrayIndex(value, arrayIndexMatch);
                if (filterMatch) value = self.filter(value, filterMatch);
                if (mapMatch) value = self.mapExpand(value, mapMatch);
                if (expandMatch) value = self.expand(value, expandMatch);

                // Casting should be the last
                if (castMatch) value = self.castToType(value, castMatch);

                if (output.length === rawRule.length) output = value;
                else output = output.replace(rawRule, value);

            });
            return output;
        };

        this.getByPath = function (src, path) {
            var self = this,
                value = JSON.parse(JSON.stringify(src)),
                parts = path.split('.'),
                _buffer;

            self.lastPath = path;

            while (parts.length) {
                _buffer = parts.shift();
                if (typeof value[_buffer] !== 'undefined') value = value[_buffer];
                else value = self.error("Path '" + path + "' not found in object");
            }
            return value;
        };

        this.getArrayIndex = function (value, matchObject) {

            if (!Array.isArray(value)) return this.error("Value '" + value + "' is not an array");

            var index = parseInt(matchObject[1], 10);

            // support for negative indexes
            if (index < 0) index = value.length + index;
            if (typeof value[index] !== 'undefined') return value[index];
            else return this.error("Array out of bound, index: " + index);
        };

        this.castToType = function (value, matchObject) {
            if (typeof value === 'undefined') return;

            if (matchObject[1] === 'string') return value.toString();
            else if (matchObject[1] === 'number') return parseFloat(value);
            else return this.error("Casting to unknown type '" + matchObject[1] + "'. Available: string, number");
        };

        this.filter = function (value, matchObject) {
            var key = matchObject[1],
                filterValue = matchObject[2].replace(/('|\\|")/g, '');

            if (!Array.isArray(value)) return this.error("Item is not an array '" + value + "'");

            return value.filter(function (item) {
                return item[key] === filterValue
            });
        };

        this.mapExpand = function (value, matchObject) {
            if (!Array.isArray(value)) return this.error("Item is not an array '" + value + "'");

            return value.map(function (item) {
                return item[matchObject[1]]
            });
        };

        this.expand = function (value, matchObject) {
            if (typeof value !== 'object') return this.error("Item is not an object '" + value + "'");
            var delimiter = matchObject[1];

            delimiter = delimiter.replace(/\\s/g, ' ');

            var result = [];
            Object.keys(value).forEach(function (fieldName) {
                result.push(value[fieldName]);
            });
            return result.join(delimiter);
        };

    }

}.call(this));
<|endoftext|>


<commit_before>var tools = require("browserify-transform-tools");
var fs = require("fs");
var path = require("path");

var opts = {
  evaluateArguments: true
};

var fn = function (args, opts, cb) {
  var file = opts.file;
  var dir  = path.dirname(file);
  var relative = args[0];

  // Only look at local require statements
  if (relative[0] === "." || relative[0] === "/") {

    // Calculate absolute require path
    var absolute = path.resolve(dir, relative);

    // Check if path is a file in current directory
    if (fs.existsSync(absolute + ".jsx")) {
      return cb(null, "require(\"" + absolute + ".jsx\")");
    }
    // Check if path is actually a directory with an index.jsx in it
    else if (fs.existsSync(absolute + "/index.jsx")) {
      return cb(null, "require(\"" + absolute + "/index.jsx\")");
    }

  }

  // Default behaviour - just return original
  return cb();
};

var transform = tools.makeRequireTransform("jsxify", opts, fn);

module.exports = transform;<commit_msg>Fix formatting<commit_after>var tools = require("browserify-transform-tools");
var fs = require("fs");
var path = require("path");

var opts = {
  evaluateArguments: true
};

var fn = function (args, opts, cb) {
  var file = opts.file;
  var dir  = path.dirname(file);
  var relative = args[0];

  // Only look at local require statements
  if (relative[0] === "." || relative[0] === "/") {

    // Calculate absolute require path
    var absolute = path.resolve(dir, relative);

    // Check if path is a file in current directory
    // Otherwise, check if path is actually a directory with an index.jsx in it
    if (fs.existsSync(absolute + ".jsx")) {
      return cb(null, "require(\"" + absolute + ".jsx\")");
    } else if (fs.existsSync(absolute + "/index.jsx")) {
      return cb(null, "require(\"" + absolute + "/index.jsx\")");
    }

  }

  // Default behaviour - just return original
  return cb();
};

var transform = tools.makeRequireTransform("jsxify", opts, fn);

module.exports = transform;<|endoftext|>


<commit_before>'use strict';

/**
 * karmes.js
 *
 * This file contains Kärmes class which will handle basically main 'moves' of Kärmes. This class contains multiple
 * different maneuvers that kärmes will do in different situations.
 */

/**#@+
 * Module dependencies
 *
 * @type {exports}
 */
var _ = require('lodash');
var async = require('async');

/** @type {LoggerInstance} */
var Logger = require('./logger');
var Constants = require('./constants');
var Utils = require('./utils');
/**#@-*/

/**
 * Constructor of Kärmes class. This class handles actual movement process of snake.
 *
 * @param   {Level} level Level instance
 * @param   {Game}  game  Connection instance
 * @constructor
 */
function Karmes(level, game) {
  //Logger.silly('Karmes class initialized.');

  var self = this;

  // Store used object references
  self.level = level;
  self.game = game;
}

/**
 * Main method on Kärmes class, this is called from Handler whenever new position message is got from server. Basically
 * this contains all the logic for Kärmes movements; attacks, blocks and actual path find.
 *
 * Note that all jobs are done in main async waterfall job, and if those jobs calls callback function with 'error'
 * parameter (first parameter on callback) that means that current job has already handled necessary movements.
 *
 * @param {function}  next  Callback function which is called when all jobs are done
 */
Karmes.prototype.doStuff = function doStuff(next) {
  var _this = this;

  async.series(
    [
      // Defense: Avoid wall collision, this is not needed - maybe
      //function(callback) {
      //  _this.avoidWallCollision(callback);
      //},

      // Defense: Avoid reserved points, this is not needed - maybe
      //function(callback) {
      //  _this.avoidReservedPoints(callback);
      //},

      // Defense: Avoid apple that cannot be eaten
      function(callback) {
        _this._avoidRottenApple(callback);
      },

      // Offense: block opponents, this is really needed :D
      //function(callback) {
      //  _this.blockOpponents(callback);
      //},

      // Offense: apple siege!
      //function(callback) {
      //  _this.appleSiege(callback);
      //},

      // Routine: calculate paths to apple, if that is needed
      function(callback) {
        _this.level.calculatePath(_this.level.players[_this.level.playerIndex], true, callback);
      }
    ],
    function callback(error) {
      if (error) {
        Logger.info('Move already made, more info. ', JSON.stringify(error));
      } else {
        _this._makeMove();
      }

      next(null, null);
    }
  );
};

/**
 * Simple method to avoid 'rotten' apple on map. This is to avoid some special cases.
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._avoidRottenApple = function _avoidRottenApple(next) {
  var _this = this;

  // Initialize used variables
  var player = _this.level.players[_this.level.playerIndex];
  var positionX = player.body[0][0];
  var positionY = player.body[0][1];
  var appleX = _this.level.appleX;
  var appleY = _this.level.appleY;
  var levelWidth = _this.level.width;
  var levelHeight = _this.level.height;

  // Make flatten reserved points
  var reservedPointsFlatten = _.map(_this.level.reservedPoints, Utils.getPointString);

  // Possible entry points for apple
  var entryPoints = [
    [appleX, appleY - 1],
    [appleX + 1, appleY],
    [appleX, appleY + 1],
    [appleX - 1, appleY]
  ];

  // Remove illegal entry points
  _.remove(entryPoints, function iterator(point) {
    if (point[0] === positionX && point[1] === positionY) { // Do not count Kärmes itself as a rotten point :D
      return false;
    } else if (point[0] < 0 || point[0] >= levelWidth ||
      point[1] < 0 || point[1] >= levelHeight ||
      (reservedPointsFlatten.indexOf(Utils.getPointString(point)) !== -1)
    ) {
      return true;
    }
  });

  // This apple is 'rotten' there is no way to get that apple and get to the safety there.
  if (entryPoints.length === 0 || entryPoints.length === 1) {
    _this._changeDirection();

    next('avoidRottenApple triggered', null);
  } else {
    next(null, null);
  }
};

/**
 * Method to check if snake can block current opponent(s). Eg in case that is shown below snake A should block snake B
 * so that he will lose whole game (also note that snakes are going upwards in this scenario).
 *
 * [0 0 0 0 0 0 0 0]
 * [0 0 0 0 0 0 0 0]
 * [0 0 0 0 0 0 A 0]
 * [0 0 0 0 0 0 A 0]
 * [0 0 0 0 0 0 A B]
 * [0 0 0 0 0 0 A B]
 * [0 0 0 0 0 0 0 B]
 * [0 0 0 0 0 0 0 0]
 *
 * Basically kärmes should always try to block opponent if that is possible.
 *
 * todo: This will need some heavy coding to do.
 * todo: use similar logic that Ajaska on this, his algo is nice, really nice
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._blockOpponents = function _blockOpponents(next) {
  //Logger.silly('Entering to \'blockOpponents()\' method.');

  next(null, null);
};

/**
 * Defense method to avoid possible wall collision.
 *
 * @todo Check if there is path already, if yes use that please!
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._avoidWallCollision = function _avoidWallCollision(next) {
  //Logger.silly('Entering to \'avoidWallCollision()\' method.');

  var _this = this;

  // Determine player information
  var player = _this.level.players[_this.level.playerIndex];
  var posX = player.body[0][0];
  var posY = player.body[0][1];
  var direction = player.direction;

  // Initialize used variables
  var to = [];
  var collision = false;

  switch (direction) {
    case Constants.directionUp:
    case Constants.directionDown:
      if (posY === 0 || posY === _this.level.height - 1) {
        collision = true;

        if (posX === 0) { // Only path is to right
          to = [1, posY];
        } else if (posX === _this.level.width - 1) { // Only path is to left
          to = [_this.level.width - 2, posY];
        }
      }
      break;
    case Constants.directionLeft:
    case Constants.directionRight:
      if (posX === 0 || posX === _this.level.width - 1) {
        collision = true;

        if (posY === 0) { // Only path is to down
          to = [posX, 1];
        } else if (posY === _this.level.height - 1) { // Only path is to up
          to = [posX, _this.level.height - 2];
        }
      }
      break;
  }

  var message = null;

  if (collision) {
    if (to.length === 0) {
      // Determine possible 'good' paths
      var paths = Utils.getWallCollisionPoints(posX, posY, direction);

      // We have more choices on right OR down side
      if (paths[0].length > paths[1].length) {
        to = paths[0][0][0];
      } else if (paths[0].length < paths[1].length) { // We have more choices on left OR up side
        to = paths[1][0][0];
      } else if (paths[0].length === paths[1].length && paths[0].length > 0) { // Both 'sides' are good, use random :D
        to = paths[Utils.getRandomInt(0, 1)][0][0];
      }
    }

    message = 'Wall collision ahead, trying to avoid that.';

    Logger.warn(message);

    _this._changeDirection(to);
  }

  next(message, null);
};

/**
 * Method to avoid reserved points.
 *
 * @todo implement this...
 * @todo is this needed?
 * @todo check if path exists
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._avoidReservedPoints = function _avoidReservedPoints(next) {
  //Logger.silly('Entering to \'avoidWallCollision()\' method.');

  next(null, null);
};

/**
 * Method to siege current apple. Following conditions must be met in this offense move:
 *  1) My snake is longer than opponent(s)
 *  2) I can make a circle around the current apple before opponent gets it
 *
 * @todo implement this...
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._appleSiege = function _appleSiege(next) {
  //Logger.silly('Entering to \'appleSiege()\' method.');

  next(null, null);
};

/**
 * Method to make actual snake move according to current position and all other stuff (and note to everyone, this isn't
 * so easy..).
 *
 * @private
 */
Karmes.prototype._makeMove = function _makeMove() {
  //Logger.silly('Entering to \'makeMove()\' method.');

  var _this = this;

  // Get next step for snake
  var nextStep = _this.level.players[_this.level.playerIndex].path.shift();

  // Oh noes we don't know where to go
  if (_.isEmpty(nextStep)) {
    Logger.warn('Oh noes, I don\'t have route to the apple');
  }

  Logger.info('Making move to next position!', JSON.stringify(nextStep));

  _this._changeDirection(nextStep);
};

/**
 * Method to change snake direction
 *
 * @param {[number, number]}  [coordinates] Coordinates where we want to move.
 * @private
 */
Karmes.prototype._changeDirection = function _changeDirection(coordinates) {
  //Logger.silly('Entering to \'changeDirection()\' method.', JSON.stringify(arguments));

  var _this = this;

  // Current position and direction of the snake
  var xFrom = _this.level.players[_this.level.playerIndex].body[0][0];
  var yFrom = _this.level.players[_this.level.playerIndex].body[0][1];
  var direction = _this.level.players[_this.level.playerIndex].direction;

  if (!(coordinates && coordinates.length === 2)) {
    coordinates = _this._getPanicMove();
  }

  // Yeah we got coordinates where we want to go
  if (coordinates && coordinates.length === 2) {
    var xTo = coordinates[0];
    var yTo = coordinates[1];

    if (xFrom !== xTo && direction !== Constants.directionRight && direction !== Constants.directionLeft) {
      Logger.info('Snake is changing direction to ' +
        Constants.directions[(xFrom < xTo) ? Constants.directionRight : Constants.directionLeft]
      );

      _this.game.sendMessage({
        msg: 'control',
        data: {
          direction: (xFrom < xTo) ? Constants.directionRight : Constants.directionLeft
        }
      });
    } else if (yFrom !== yTo && direction !== Constants.directionDown && direction !== Constants.directionUp) {
      Logger.info('Snake is changing direction to ' +
        Constants.directions[(yFrom < yTo) ? Constants.directionRight : Constants.directionLeft]
      );

      _this.game.sendMessage({
        msg: 'control',
        data: {
          direction: (yFrom < yTo) ? Constants.directionDown : Constants.directionUp
        }
      });
    } else {
      Logger.info('Snake is continues to moving to ' + Constants.directions[direction]);

      /*
      Logger.silly('Weird could not get new direction...', JSON.stringify({
        from: [xFrom, yFrom],
        to: [xTo, yTo],
        direction: Constants.directions[direction]
      }));
      */
    }
  } else {
    Logger.warn('No coordinates, this isn\'t good.');
  }
};

/**
 * Method to make 'panic move' this is initialized every time when there's no route to apple. Basically this logic
 * calculates 2x3 and 3x2 tiles from current position and checks which one contains most "free" spots.
 *
 * @todo This needs some work still, it isn't taking all spots right now, and that isn't good. REALLY this isn't good...
 *
 * @return  {[number, number]}
 * @private
 */
Karmes.prototype._getPanicMove = function _panicMove() {
  //Logger.silly('Entering to \'getPanicMove()\' method.', JSON.stringify(arguments));

  var _this = this;

  Logger.error('Yeah, panic move!');

  // Player snake
  var snake = _this.level.players[_this.level.playerIndex];

  // Current position and direction of the snake
  var xFrom = snake.body[0][0];
  var yFrom = snake.body[0][1];

  // Determine level width and height
  var levelWidth = _this.level.width;
  var levelHeight = _this.level.height;

  // Get reserved points, normal array of coordinates and 'flatten' array
  var reservedPoints = _this.level.reservedPoints;
  var reservedPointsFlatten = _.map(reservedPoints, _this.getPointString);

  // Create 'squares' for each direction; top, right, bottom and left
  var squares = [
    [ // top
      [xFrom, yFrom + 1],
      [xFrom - 1, yFrom + 1],
      [xFrom - 1, yFrom + 2],
      [xFrom, yFrom + 2],
      [xFrom + 1, yFrom + 2],
      [xFrom + 1, yFrom + 1]
    ],
    [ // right
      [xFrom + 1, yFrom],
      [xFrom + 1, yFrom + 1],
      [xFrom + 2, yFrom + 1],
      [xFrom + 2, yFrom],
      [xFrom + 2, yFrom - 1],
      [xFrom + 1, yFrom - 1]
    ],
    [ // bottom
      [xFrom, yFrom - 1],
      [xFrom + 1, yFrom - 1],
      [xFrom + 1, yFrom - 2],
      [xFrom, yFrom - 2],
      [xFrom - 1, yFrom -2],
      [xFrom - 1, yFrom - 1]
    ],
    [ // left
      [xFrom - 1, yFrom],
      [xFrom - 1, yFrom - 1],
      [xFrom - 2, yFrom - 1],
      [xFrom - 2, yFrom],
      [xFrom - 2, yFrom + 1],
      [xFrom - 1, yFrom + 1]
    ]
  ];

  // Remove all 'first' possible path from squares, this is to remove 'not possible' square
  squares = _.remove(squares, function iterator(square) {
    return reservedPointsFlatten.indexOf(_this.getPointString(square[0])) === -1;
  });

  // Check each squares that we have some coordinates to go to
  squares = _.map(squares, function iterator(square) {
    // Remove not possible coordinates
    square = _.map(square, function iterator(coordinates) {
      var x = coordinates[0];
      var y = coordinates[1];

      // Oh noes, coordinates are not allowed
      if (
        (x < 0 || x >= levelWidth) ||
        (y < 0 || y >= levelHeight) ||
        (reservedPointsFlatten.indexOf(_this.getPointString(coordinates)) !== -1)
      ) {
        return false;
      }

      return coordinates;
    });

    return _.compact(square);
  });

  // Sort squares by it length => more available spots
  squares.sort(function sorter(a, b) {
    return b.length - a.length;
  });

  Logger.error('panic data', JSON.stringify(squares));

  return squares[0][0];
};

module.exports = Karmes;
<commit_msg>And fixed some wrong pointers...<commit_after>'use strict';

/**
 * karmes.js
 *
 * This file contains Kärmes class which will handle basically main 'moves' of Kärmes. This class contains multiple
 * different maneuvers that kärmes will do in different situations.
 */

/**#@+
 * Module dependencies
 *
 * @type {exports}
 */
var _ = require('lodash');
var async = require('async');

/** @type {LoggerInstance} */
var Logger = require('./logger');
var Constants = require('./constants');
var Utils = require('./utils');
/**#@-*/

/**
 * Constructor of Kärmes class. This class handles actual movement process of snake.
 *
 * @param   {Level} level Level instance
 * @param   {Game}  game  Connection instance
 * @constructor
 */
function Karmes(level, game) {
  //Logger.silly('Karmes class initialized.');

  var self = this;

  // Store used object references
  self.level = level;
  self.game = game;
}

/**
 * Main method on Kärmes class, this is called from Handler whenever new position message is got from server. Basically
 * this contains all the logic for Kärmes movements; attacks, blocks and actual path find.
 *
 * Note that all jobs are done in main async waterfall job, and if those jobs calls callback function with 'error'
 * parameter (first parameter on callback) that means that current job has already handled necessary movements.
 *
 * @param {function}  next  Callback function which is called when all jobs are done
 */
Karmes.prototype.doStuff = function doStuff(next) {
  var _this = this;

  async.series(
    [
      // Defense: Avoid wall collision, this is not needed - maybe
      //function(callback) {
      //  _this.avoidWallCollision(callback);
      //},

      // Defense: Avoid reserved points, this is not needed - maybe
      //function(callback) {
      //  _this.avoidReservedPoints(callback);
      //},

      // Defense: Avoid apple that cannot be eaten
      function(callback) {
        _this._avoidRottenApple(callback);
      },

      // Offense: block opponents, this is really needed :D
      //function(callback) {
      //  _this.blockOpponents(callback);
      //},

      // Offense: apple siege!
      //function(callback) {
      //  _this.appleSiege(callback);
      //},

      // Routine: calculate paths to apple, if that is needed
      function(callback) {
        _this.level.calculatePath(_this.level.players[_this.level.playerIndex], true, callback);
      }
    ],
    function callback(error) {
      if (error) {
        Logger.info('Move already made, more info. ', JSON.stringify(error));
      } else {
        _this._makeMove();
      }

      next(null, null);
    }
  );
};

/**
 * Simple method to avoid 'rotten' apple on map. This is to avoid some special cases.
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._avoidRottenApple = function _avoidRottenApple(next) {
  var _this = this;

  // Initialize used variables
  var player = _this.level.players[_this.level.playerIndex];
  var positionX = player.body[0][0];
  var positionY = player.body[0][1];
  var appleX = _this.level.appleX;
  var appleY = _this.level.appleY;
  var levelWidth = _this.level.width;
  var levelHeight = _this.level.height;

  // Make flatten reserved points
  var reservedPointsFlatten = _.map(_this.level.reservedPoints, Utils.getPointString);

  // Possible entry points for apple
  var entryPoints = [
    [appleX, appleY - 1],
    [appleX + 1, appleY],
    [appleX, appleY + 1],
    [appleX - 1, appleY]
  ];

  // Remove illegal entry points
  _.remove(entryPoints, function iterator(point) {
    if (point[0] === positionX && point[1] === positionY) { // Do not count Kärmes itself as a rotten point :D
      return false;
    } else if (point[0] < 0 || point[0] >= levelWidth ||
      point[1] < 0 || point[1] >= levelHeight ||
      (reservedPointsFlatten.indexOf(Utils.getPointString(point)) !== -1)
    ) {
      return true;
    }
  });

  // This apple is 'rotten' there is no way to get that apple and get to the safety there.
  if (entryPoints.length === 0 || entryPoints.length === 1) {
    _this._changeDirection();

    next('avoidRottenApple triggered', null);
  } else {
    next(null, null);
  }
};

/**
 * Method to check if snake can block current opponent(s). Eg in case that is shown below snake A should block snake B
 * so that he will lose whole game (also note that snakes are going upwards in this scenario).
 *
 * [0 0 0 0 0 0 0 0]
 * [0 0 0 0 0 0 0 0]
 * [0 0 0 0 0 0 A 0]
 * [0 0 0 0 0 0 A 0]
 * [0 0 0 0 0 0 A B]
 * [0 0 0 0 0 0 A B]
 * [0 0 0 0 0 0 0 B]
 * [0 0 0 0 0 0 0 0]
 *
 * Basically kärmes should always try to block opponent if that is possible.
 *
 * todo: This will need some heavy coding to do.
 * todo: use similar logic that Ajaska on this, his algo is nice, really nice
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._blockOpponents = function _blockOpponents(next) {
  //Logger.silly('Entering to \'blockOpponents()\' method.');

  next(null, null);
};

/**
 * Defense method to avoid possible wall collision.
 *
 * @todo Check if there is path already, if yes use that please!
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._avoidWallCollision = function _avoidWallCollision(next) {
  //Logger.silly('Entering to \'avoidWallCollision()\' method.');

  var _this = this;

  // Determine player information
  var player = _this.level.players[_this.level.playerIndex];
  var posX = player.body[0][0];
  var posY = player.body[0][1];
  var direction = player.direction;

  // Initialize used variables
  var to = [];
  var collision = false;

  switch (direction) {
    case Constants.directionUp:
    case Constants.directionDown:
      if (posY === 0 || posY === _this.level.height - 1) {
        collision = true;

        if (posX === 0) { // Only path is to right
          to = [1, posY];
        } else if (posX === _this.level.width - 1) { // Only path is to left
          to = [_this.level.width - 2, posY];
        }
      }
      break;
    case Constants.directionLeft:
    case Constants.directionRight:
      if (posX === 0 || posX === _this.level.width - 1) {
        collision = true;

        if (posY === 0) { // Only path is to down
          to = [posX, 1];
        } else if (posY === _this.level.height - 1) { // Only path is to up
          to = [posX, _this.level.height - 2];
        }
      }
      break;
  }

  var message = null;

  if (collision) {
    if (to.length === 0) {
      // Determine possible 'good' paths
      var paths = Utils.getWallCollisionPoints(posX, posY, direction);

      // We have more choices on right OR down side
      if (paths[0].length > paths[1].length) {
        to = paths[0][0][0];
      } else if (paths[0].length < paths[1].length) { // We have more choices on left OR up side
        to = paths[1][0][0];
      } else if (paths[0].length === paths[1].length && paths[0].length > 0) { // Both 'sides' are good, use random :D
        to = paths[Utils.getRandomInt(0, 1)][0][0];
      }
    }

    message = 'Wall collision ahead, trying to avoid that.';

    Logger.warn(message);

    _this._changeDirection(to);
  }

  next(message, null);
};

/**
 * Method to avoid reserved points.
 *
 * @todo implement this...
 * @todo is this needed?
 * @todo check if path exists
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._avoidReservedPoints = function _avoidReservedPoints(next) {
  //Logger.silly('Entering to \'avoidWallCollision()\' method.');

  next(null, null);
};

/**
 * Method to siege current apple. Following conditions must be met in this offense move:
 *  1) My snake is longer than opponent(s)
 *  2) I can make a circle around the current apple before opponent gets it
 *
 * @todo implement this...
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._appleSiege = function _appleSiege(next) {
  //Logger.silly('Entering to \'appleSiege()\' method.');

  next(null, null);
};

/**
 * Method to make actual snake move according to current position and all other stuff (and note to everyone, this isn't
 * so easy..).
 *
 * @private
 */
Karmes.prototype._makeMove = function _makeMove() {
  //Logger.silly('Entering to \'makeMove()\' method.');

  var _this = this;

  // Get next step for snake
  var nextStep = _this.level.players[_this.level.playerIndex].path.shift();

  // Oh noes we don't know where to go
  if (_.isEmpty(nextStep)) {
    Logger.warn('Oh noes, I don\'t have route to the apple');
  }

  Logger.info('Making move to next position!', JSON.stringify(nextStep));

  _this._changeDirection(nextStep);
};

/**
 * Method to change snake direction
 *
 * @param {[number, number]}  [coordinates] Coordinates where we want to move.
 * @private
 */
Karmes.prototype._changeDirection = function _changeDirection(coordinates) {
  //Logger.silly('Entering to \'changeDirection()\' method.', JSON.stringify(arguments));

  var _this = this;

  // Current position and direction of the snake
  var xFrom = _this.level.players[_this.level.playerIndex].body[0][0];
  var yFrom = _this.level.players[_this.level.playerIndex].body[0][1];
  var direction = _this.level.players[_this.level.playerIndex].direction;

  if (!(coordinates && coordinates.length === 2)) {
    coordinates = _this._getPanicMove();
  }

  // Yeah we got coordinates where we want to go
  if (coordinates && coordinates.length === 2) {
    var xTo = coordinates[0];
    var yTo = coordinates[1];

    if (xFrom !== xTo && direction !== Constants.directionRight && direction !== Constants.directionLeft) {
      Logger.info('Snake is changing direction to ' +
        Constants.directions[(xFrom < xTo) ? Constants.directionRight : Constants.directionLeft]
      );

      _this.game.sendMessage({
        msg: 'control',
        data: {
          direction: (xFrom < xTo) ? Constants.directionRight : Constants.directionLeft
        }
      });
    } else if (yFrom !== yTo && direction !== Constants.directionDown && direction !== Constants.directionUp) {
      Logger.info('Snake is changing direction to ' +
        Constants.directions[(yFrom < yTo) ? Constants.directionRight : Constants.directionLeft]
      );

      _this.game.sendMessage({
        msg: 'control',
        data: {
          direction: (yFrom < yTo) ? Constants.directionDown : Constants.directionUp
        }
      });
    } else {
      Logger.info('Snake is continues to moving to ' + Constants.directions[direction]);

      /*
      Logger.silly('Weird could not get new direction...', JSON.stringify({
        from: [xFrom, yFrom],
        to: [xTo, yTo],
        direction: Constants.directions[direction]
      }));
      */
    }
  } else {
    Logger.warn('No coordinates, this isn\'t good.');
  }
};

/**
 * Method to make 'panic move' this is initialized every time when there's no route to apple. Basically this logic
 * calculates 2x3 and 3x2 tiles from current position and checks which one contains most "free" spots.
 *
 * @todo This needs some work still, it isn't taking all spots right now, and that isn't good. REALLY this isn't good...
 *
 * @return  {[number, number]}
 * @private
 */
Karmes.prototype._getPanicMove = function _panicMove() {
  //Logger.silly('Entering to \'getPanicMove()\' method.', JSON.stringify(arguments));

  var _this = this;

  Logger.error('Yeah, panic move!');

  // Player snake
  var snake = _this.level.players[_this.level.playerIndex];

  // Current position and direction of the snake
  var xFrom = snake.body[0][0];
  var yFrom = snake.body[0][1];

  // Determine level width and height
  var levelWidth = _this.level.width;
  var levelHeight = _this.level.height;

  // Get reserved points, normal array of coordinates and 'flatten' array
  var reservedPoints = _this.level.reservedPoints;
  var reservedPointsFlatten = _.map(reservedPoints, Utils.getPointString);

  // Create 'squares' for each direction; top, right, bottom and left
  var squares = [
    [ // top
      [xFrom, yFrom + 1],
      [xFrom - 1, yFrom + 1],
      [xFrom - 1, yFrom + 2],
      [xFrom, yFrom + 2],
      [xFrom + 1, yFrom + 2],
      [xFrom + 1, yFrom + 1]
    ],
    [ // right
      [xFrom + 1, yFrom],
      [xFrom + 1, yFrom + 1],
      [xFrom + 2, yFrom + 1],
      [xFrom + 2, yFrom],
      [xFrom + 2, yFrom - 1],
      [xFrom + 1, yFrom - 1]
    ],
    [ // bottom
      [xFrom, yFrom - 1],
      [xFrom + 1, yFrom - 1],
      [xFrom + 1, yFrom - 2],
      [xFrom, yFrom - 2],
      [xFrom - 1, yFrom -2],
      [xFrom - 1, yFrom - 1]
    ],
    [ // left
      [xFrom - 1, yFrom],
      [xFrom - 1, yFrom - 1],
      [xFrom - 2, yFrom - 1],
      [xFrom - 2, yFrom],
      [xFrom - 2, yFrom + 1],
      [xFrom - 1, yFrom + 1]
    ]
  ];

  // Remove all 'first' possible path from squares, this is to remove 'not possible' square
  squares = _.remove(squares, function iterator(square) {
    return reservedPointsFlatten.indexOf(Utils.getPointString(square[0])) === -1;
  });

  // Check each squares that we have some coordinates to go to
  squares = _.map(squares, function iterator(square) {
    // Remove not possible coordinates
    square = _.map(square, function iterator(coordinates) {
      var x = coordinates[0];
      var y = coordinates[1];

      // Oh noes, coordinates are not allowed
      if (
        (x < 0 || x >= levelWidth) ||
        (y < 0 || y >= levelHeight) ||
        (reservedPointsFlatten.indexOf(Utils.getPointString(coordinates)) !== -1)
      ) {
        return false;
      }

      return coordinates;
    });

    return _.compact(square);
  });

  // Sort squares by it length => more available spots
  squares.sort(function sorter(a, b) {
    return b.length - a.length;
  });

  Logger.error('panic data', JSON.stringify(squares));

  return squares[0][0];
};

module.exports = Karmes;
<|endoftext|>


<commit_before>'use strict';

/**
 * karmes.js
 *
 * This file contains Kärmes class which will handle basically main 'moves' of Kärmes. This class contains multiple
 * different maneuvers that kärmes will do in different situations.
 */

/**#@+
 * Module dependencies
 *
 * @type {exports}
 */
var _ = require('lodash');
var async = require('async');

/** @type {LoggerInstance} */
var Logger = require('./logger');
var Constants = require('./constants');
var Utils = require('./utils');
/**#@-*/

/**
 * Constructor of Kärmes class. This class handles actual movement process of snake.
 *
 * @param   {Level} level Level instance
 * @param   {Game}  game  Connection instance
 * @constructor
 */
function Karmes(level, game) {
  //Logger.silly('Karmes class initialized.');

  var self = this;

  // Store used object references
  self.level = level;
  self.game = game;
}

/**
 * Main method on Kärmes class, this is called from Handler whenever new position message is got from server. Basically
 * this contains all the logic for Kärmes movements; attacks, blocks and actual path find.
 *
 * Note that all jobs are done in main async waterfall job, and if those jobs calls callback function with 'error'
 * parameter (first parameter on callback) that means that current job has already handled necessary movements.
 *
 * @param {function}  next  Callback function which is called when all jobs are done
 */
Karmes.prototype.doStuff = function doStuff(next) {
  var _this = this;

  async.series(
    [
      // Defense: Avoid wall collision, this is not needed - maybe
      //function(callback) {
      //  _this.avoidWallCollision(callback);
      //},

      // Defense: Avoid reserved points, this is not needed - maybe
      //function(callback) {
      //  _this.avoidReservedPoints(callback);
      //},

      // Defense: Avoid apple that cannot be eaten
      function(callback) {
        _this.avoidRottenApple(callback);
      },

      // Offense: block opponents, this is really needed :D
      //function(callback) {
      //  _this.blockOpponents(callback);
      //},

      // Offense: apple siege!
      //function(callback) {
      //  _this.appleSiege(callback);
      //},

      // Routine: calculate paths to apple, if that is needed
      function(callback) {
        _this.level.calculatePath(_this.level.players[_this.level.playerIndex], true, callback);
      }
    ],
    function callback(error) {
      if (error) {
        Logger.info('Move already made, more info. ', JSON.stringify(error));
      } else {
        _this.makeMove();
      }

      next(null, null);
    }
  );
};

/**
 * Simple method to avoid 'rotten' apple on map. This is to avoid some special cases.
 *
 * @param {function}  next
 */
Karmes.prototype.avoidRottenApple = function avoidRottenApple(next) {
  var _this = this;

  // Initialize used variables
  var player = _this.level.players[_this.level.playerIndex];
  var posX = player.body[0][0];
  var posY = player.body[0][1];
  var appleX = _this.level.appleX;
  var appleY = _this.level.appleY;
  var levelWidth = _this.level.width;
  var levelHeight = _this.level.height;

  // Make flatten reserved points
  var reservedPointsFlatten = _.map(_this.level.reservedPoints, Utils.getPointString);

  // Possible entry points for apple
  var entryPoints = [
    [appleX, appleY - 1],
    [appleX + 1, appleY],
    [appleX, appleY + 1],
    [appleX - 1, appleY]
  ];

  // Remove illegal entry points
  _.remove(entryPoints, function iterator(point) {
    if (point[0] === posX && point[1] === posY) { // Do not count Kärmes itself as a rotten point :D
      return false;
    } else if (point[0] < 0 || point[0] >= levelWidth ||
      point[1] < 0 || point[1] >= levelHeight ||
      (reservedPointsFlatten.indexOf(Utils.getPointString(point)) !== -1)
    ) {
      return true;
    }
  });

  // This apple is 'rotten' there is no way to get that apple and get to the safety there.
  if (entryPoints.length === 0 || entryPoints.length === 1) {
    _this.changeDirection();

    next('avoidRottenApple triggered', null);
  } else {
    next(null, null);
  }
};

/**
 * Method to check if snake can block current opponent(s). Eg in case that is shown below snake A should block snake B
 * so that he will lose whole game (also note that snakes are going upwards in this scenario).
 *
 * [0 0 0 0 0 0 0 0]
 * [0 0 0 0 0 0 0 0]
 * [0 0 0 0 0 0 A 0]
 * [0 0 0 0 0 0 A 0]
 * [0 0 0 0 0 0 A B]
 * [0 0 0 0 0 0 A B]
 * [0 0 0 0 0 0 0 B]
 * [0 0 0 0 0 0 0 0]
 *
 * todo: This will need some heavy coding to do.
 * todo: use similar logic that Ajaska on this, his algo is nice, really nice
 */
Karmes.prototype.blockOpponents = function blockOpponents(next) {
  //Logger.silly('Entering to \'blockOpponents()\' method.');

  next(null, null);
};

/**
 * Defense method to avoid possible wall collision.
 *
 * @returns {boolean}
 */
Karmes.prototype.avoidWallCollision = function avoidWallCollision(next) {
  //Logger.silly('Entering to \'avoidWallCollision()\' method.');

  var _this = this;

  // Determine player information
  var player = _this.level.players[_this.level.playerIndex];
  var posX = player.body[0][0];
  var posY = player.body[0][1];
  var direction = player.direction;

  // Initialize used variables
  var to = [];
  var collision = false;

  switch (direction) {
    case Constants.directionUp:
    case Constants.directionDown:
      if (posY === 0 || posY === _this.level.height - 1) {
        collision = true;

        if (posX === 0) { // Only path is to right
          to = [1, posY];
        } else if (posX === _this.level.width - 1) { // Only path is to left
          to = [_this.level.width - 2, posY];
        }
      }
      break;
    case Constants.directionLeft:
    case Constants.directionRight:
      if (posX === 0 || posX === _this.level.width - 1) {
        collision = true;

        if (posY === 0) { // Only path is to down
          to = [posX, 1];
        } else if (posY === _this.level.height - 1) { // Only path is to up
          to = [posX, _this.level.height - 2];
        }
      }
      break;
  }

  var message = null;

  if (collision) {
    if (to.length === 0) {
      // Determine possible 'good' paths
      var paths = Utils.getWallCollisionPoints(posX, posY, direction);

      // We have more choices on right OR down side
      if (paths[0].length > paths[1].length) {
        to = paths[0][0][0];
      } else if (paths[0].length < paths[1].length) { // We have more choices on left OR up side
        to = paths[1][0][0];
      } else if (paths[0].length === paths[1].length && paths[0].length > 0) { // Both 'sides' are good, use random :D
        to = paths[Utils.getRandomInt(0, 1)][0][0];
      }
    }

    message = 'Wall collision ahead, trying to avoid that.';

    Logger.warn(message);

    _this.changeDirection(to);
  }

  next(message, null);
};

/**
 * Method to avoid reserved points.
 *
 * @todo implement this...
 *
 * @param   {function}  next
 */
Karmes.prototype.avoidReservedPoints = function avoidReservedPoints(next) {
  //Logger.silly('Entering to \'avoidWallCollision()\' method.');

  next(null, null);
};

/**
 * Method to siege current apple. Following conditions must be met in this offense move:
 *  1) My snake is longer than opponent(s)
 *  2) I can make a circle around the current apple
 *
 * @todo implement this...
 *
 * @param   {function}  next
 */
Karmes.prototype.appleSiege = function appleSiege(next) {
  //Logger.silly('Entering to \'appleSiege()\' method.');

  next(null, null);
};

/**
 * Method to make actual snake move according to current position and all other stuff (and note to everyone, this isn't
 * so easy..).
 */
Karmes.prototype.makeMove = function makeMove() {
  //Logger.silly('Entering to \'makeMove()\' method.');

  var _this = this;

  // Get next step for snake
  var nextStep = _this.level.players[_this.level.playerIndex].path.shift();

  // Oh noes we don't know where to go
  if (_.isEmpty(nextStep)) {
    Logger.warn('Oh noes, I don\'t have route to the apple');
  }

  Logger.info('Making move to next position!', JSON.stringify(nextStep));

  _this.changeDirection(nextStep);
};

/**
 * Method to change snake direction
 *
 * @param {[number, number]}  [coordinates] Coordinates where we want to move.
 */
Karmes.prototype.changeDirection = function changeDirection(coordinates) {
  //Logger.silly('Entering to \'changeDirection()\' method.', JSON.stringify(arguments));

  var _this = this;

  // Current position and direction of the snake
  var xFrom = _this.level.players[_this.level.playerIndex].body[0][0];
  var yFrom = _this.level.players[_this.level.playerIndex].body[0][1];
  var direction = _this.level.players[_this.level.playerIndex].direction;

  if (!(coordinates && coordinates.length === 2)) {
    coordinates = _this.getPanicMove();
  }

  // Yeah we got coordinates where we want to go
  if (coordinates && coordinates.length === 2) {
    var xTo = coordinates[0];
    var yTo = coordinates[1];

    if (xFrom !== xTo && direction !== Constants.directionRight && direction !== Constants.directionLeft) {
      Logger.info('Snake is changing direction to ' +
        Constants.directions[(xFrom < xTo) ? Constants.directionRight : Constants.directionLeft]
      );

      _this.game.sendMessage({
        msg: 'control',
        data: {
          direction: (xFrom < xTo) ? Constants.directionRight : Constants.directionLeft
        }
      });
    } else if (yFrom !== yTo && direction !== Constants.directionDown && direction !== Constants.directionUp) {
      Logger.info('Snake is changing direction to ' +
        Constants.directions[(yFrom < yTo) ? Constants.directionRight : Constants.directionLeft]
      );

      _this.game.sendMessage({
        msg: 'control',
        data: {
          direction: (yFrom < yTo) ? Constants.directionDown : Constants.directionUp
        }
      });
    } else {
      Logger.info('Snake is continues to moving to ' + Constants.directions[direction]);

      /*
      Logger.silly('Weird could not get new direction...', JSON.stringify({
        from: [xFrom, yFrom],
        to: [xTo, yTo],
        direction: Constants.directions[direction]
      }));
      */
    }
  } else {
    Logger.warn('No coordinates, this isn\'t good.');
  }
};

/**
 * Method to make 'panic move' this is initialized every time when there's no route to apple. Basically this logic
 * calculates 2x3 and 3x2 tiles from current position and checks which one contains most "free" spots.
 *
 * @todo  This needs some work still, it isn't taking all spots right now, and that isn't good.
 *
 * @return  {[number, number]}
 */
Karmes.prototype.getPanicMove = function panicMove() {
  //Logger.silly('Entering to \'getPanicMove()\' method.', JSON.stringify(arguments));

  var _this = this;

  Logger.error('Yeah, panic move!');

  // Player snake
  var snake = _this.level.players[_this.level.playerIndex];

  // Current position and direction of the snake
  var xFrom = snake.body[0][0];
  var yFrom = snake.body[0][1];

  // Determine level width and height
  var levelWidth = _this.level.width;
  var levelHeight = _this.level.height;

  // Get reserved points, normal array of coordinates and 'flatten' array
  var reservedPoints = _this.level.reservedPoints;
  var reservedPointsFlatten = _.map(reservedPoints, _this.getPointString);

  // Create 'squares' for each direction; top, right, bottom and left
  var squares = [
    [ // top
      [xFrom, yFrom + 1],
      [xFrom - 1, yFrom + 1],
      [xFrom - 1, yFrom + 2],
      [xFrom, yFrom + 2],
      [xFrom + 1, yFrom + 2],
      [xFrom + 1, yFrom + 1]
    ],
    [ // right
      [xFrom + 1, yFrom],
      [xFrom + 1, yFrom + 1],
      [xFrom + 2, yFrom + 1],
      [xFrom + 2, yFrom],
      [xFrom + 2, yFrom - 1],
      [xFrom + 1, yFrom - 1]
    ],
    [ // bottom
      [xFrom, yFrom - 1],
      [xFrom + 1, yFrom - 1],
      [xFrom + 1, yFrom - 2],
      [xFrom, yFrom - 2],
      [xFrom - 1, yFrom -2],
      [xFrom - 1, yFrom - 1]
    ],
    [ // left
      [xFrom - 1, yFrom],
      [xFrom - 1, yFrom - 1],
      [xFrom - 2, yFrom - 1],
      [xFrom - 2, yFrom],
      [xFrom - 2, yFrom + 1],
      [xFrom - 1, yFrom + 1]
    ]
  ];

  // Remove all 'first' possible path from squares, this is to remove 'not possible' square
  squares = _.remove(squares, function iterator(square) {
    return reservedPointsFlatten.indexOf(_this.getPointString(square[0])) === -1;
  });

  // Check each squares that we have some coordinates to go to
  squares = _.map(squares, function iterator(square) {
    // Remove not possible coordinates
    square = _.map(square, function iterator(coordinates) {
      var x = coordinates[0];
      var y = coordinates[1];

      // Oh noes, coordinates are not allowed
      if (
        (x < 0 || x >= levelWidth) ||
        (y < 0 || y >= levelHeight) ||
        (reservedPointsFlatten.indexOf(_this.getPointString(coordinates)) !== -1)
      ) {
        return false;
      }

      return coordinates;
    });

    return _.compact(square);
  });

  // Sort squares by it length => more available spots
  squares.sort(function sorter(a, b) {
    return b.length - a.length;
  });

  Logger.error('panic data', JSON.stringify(squares));

  return squares[0][0];
};

/**
 * Simple helper function to make point string, this is just needed to flatten current reserved points.
 *
 * @param   {[number, number]}  point
 * @returns {string}
 */
Karmes.prototype.getPointString = function getPointString(point) {
  return 'x' + point[0].toString() + '-y' + point[1].toString();
};

module.exports = Karmes;
<commit_msg>yet another refactor, no changes to logic though.<commit_after>'use strict';

/**
 * karmes.js
 *
 * This file contains Kärmes class which will handle basically main 'moves' of Kärmes. This class contains multiple
 * different maneuvers that kärmes will do in different situations.
 */

/**#@+
 * Module dependencies
 *
 * @type {exports}
 */
var _ = require('lodash');
var async = require('async');

/** @type {LoggerInstance} */
var Logger = require('./logger');
var Constants = require('./constants');
var Utils = require('./utils');
/**#@-*/

/**
 * Constructor of Kärmes class. This class handles actual movement process of snake.
 *
 * @param   {Level} level Level instance
 * @param   {Game}  game  Connection instance
 * @constructor
 */
function Karmes(level, game) {
  //Logger.silly('Karmes class initialized.');

  var self = this;

  // Store used object references
  self.level = level;
  self.game = game;
}

/**
 * Main method on Kärmes class, this is called from Handler whenever new position message is got from server. Basically
 * this contains all the logic for Kärmes movements; attacks, blocks and actual path find.
 *
 * Note that all jobs are done in main async waterfall job, and if those jobs calls callback function with 'error'
 * parameter (first parameter on callback) that means that current job has already handled necessary movements.
 *
 * @param {function}  next  Callback function which is called when all jobs are done
 */
Karmes.prototype.doStuff = function doStuff(next) {
  var _this = this;

  async.series(
    [
      // Defense: Avoid wall collision, this is not needed - maybe
      //function(callback) {
      //  _this.avoidWallCollision(callback);
      //},

      // Defense: Avoid reserved points, this is not needed - maybe
      //function(callback) {
      //  _this.avoidReservedPoints(callback);
      //},

      // Defense: Avoid apple that cannot be eaten
      function(callback) {
        _this._avoidRottenApple(callback);
      },

      // Offense: block opponents, this is really needed :D
      //function(callback) {
      //  _this.blockOpponents(callback);
      //},

      // Offense: apple siege!
      //function(callback) {
      //  _this.appleSiege(callback);
      //},

      // Routine: calculate paths to apple, if that is needed
      function(callback) {
        _this.level.calculatePath(_this.level.players[_this.level.playerIndex], true, callback);
      }
    ],
    function callback(error) {
      if (error) {
        Logger.info('Move already made, more info. ', JSON.stringify(error));
      } else {
        _this._makeMove();
      }

      next(null, null);
    }
  );
};

/**
 * Simple method to avoid 'rotten' apple on map. This is to avoid some special cases.
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._avoidRottenApple = function _avoidRottenApple(next) {
  var _this = this;

  // Initialize used variables
  var player = _this.level.players[_this.level.playerIndex];
  var positionX = player.body[0][0];
  var positionY = player.body[0][1];
  var appleX = _this.level.appleX;
  var appleY = _this.level.appleY;
  var levelWidth = _this.level.width;
  var levelHeight = _this.level.height;

  // Make flatten reserved points
  var reservedPointsFlatten = _.map(_this.level.reservedPoints, Utils.getPointString);

  // Possible entry points for apple
  var entryPoints = [
    [appleX, appleY - 1],
    [appleX + 1, appleY],
    [appleX, appleY + 1],
    [appleX - 1, appleY]
  ];

  // Remove illegal entry points
  _.remove(entryPoints, function iterator(point) {
    if (point[0] === positionX && point[1] === positionY) { // Do not count Kärmes itself as a rotten point :D
      return false;
    } else if (point[0] < 0 || point[0] >= levelWidth ||
      point[1] < 0 || point[1] >= levelHeight ||
      (reservedPointsFlatten.indexOf(Utils.getPointString(point)) !== -1)
    ) {
      return true;
    }
  });

  // This apple is 'rotten' there is no way to get that apple and get to the safety there.
  if (entryPoints.length === 0 || entryPoints.length === 1) {
    _this._changeDirection();

    next('avoidRottenApple triggered', null);
  } else {
    next(null, null);
  }
};

/**
 * Method to check if snake can block current opponent(s). Eg in case that is shown below snake A should block snake B
 * so that he will lose whole game (also note that snakes are going upwards in this scenario).
 *
 * [0 0 0 0 0 0 0 0]
 * [0 0 0 0 0 0 0 0]
 * [0 0 0 0 0 0 A 0]
 * [0 0 0 0 0 0 A 0]
 * [0 0 0 0 0 0 A B]
 * [0 0 0 0 0 0 A B]
 * [0 0 0 0 0 0 0 B]
 * [0 0 0 0 0 0 0 0]
 *
 * Basically kärmes should always try to block opponent if that is possible.
 *
 * todo: This will need some heavy coding to do.
 * todo: use similar logic that Ajaska on this, his algo is nice, really nice
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._blockOpponents = function _blockOpponents(next) {
  //Logger.silly('Entering to \'blockOpponents()\' method.');

  next(null, null);
};

/**
 * Defense method to avoid possible wall collision.
 *
 * @todo Check if there is path already, if yes use that please!
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._avoidWallCollision = function _avoidWallCollision(next) {
  //Logger.silly('Entering to \'avoidWallCollision()\' method.');

  var _this = this;

  // Determine player information
  var player = _this.level.players[_this.level.playerIndex];
  var posX = player.body[0][0];
  var posY = player.body[0][1];
  var direction = player.direction;

  // Initialize used variables
  var to = [];
  var collision = false;

  switch (direction) {
    case Constants.directionUp:
    case Constants.directionDown:
      if (posY === 0 || posY === _this.level.height - 1) {
        collision = true;

        if (posX === 0) { // Only path is to right
          to = [1, posY];
        } else if (posX === _this.level.width - 1) { // Only path is to left
          to = [_this.level.width - 2, posY];
        }
      }
      break;
    case Constants.directionLeft:
    case Constants.directionRight:
      if (posX === 0 || posX === _this.level.width - 1) {
        collision = true;

        if (posY === 0) { // Only path is to down
          to = [posX, 1];
        } else if (posY === _this.level.height - 1) { // Only path is to up
          to = [posX, _this.level.height - 2];
        }
      }
      break;
  }

  var message = null;

  if (collision) {
    if (to.length === 0) {
      // Determine possible 'good' paths
      var paths = Utils.getWallCollisionPoints(posX, posY, direction);

      // We have more choices on right OR down side
      if (paths[0].length > paths[1].length) {
        to = paths[0][0][0];
      } else if (paths[0].length < paths[1].length) { // We have more choices on left OR up side
        to = paths[1][0][0];
      } else if (paths[0].length === paths[1].length && paths[0].length > 0) { // Both 'sides' are good, use random :D
        to = paths[Utils.getRandomInt(0, 1)][0][0];
      }
    }

    message = 'Wall collision ahead, trying to avoid that.';

    Logger.warn(message);

    _this._changeDirection(to);
  }

  next(message, null);
};

/**
 * Method to avoid reserved points.
 *
 * @todo implement this...
 * @todo is this needed?
 * @todo check if path exists
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._avoidReservedPoints = function _avoidReservedPoints(next) {
  //Logger.silly('Entering to \'avoidWallCollision()\' method.');

  next(null, null);
};

/**
 * Method to siege current apple. Following conditions must be met in this offense move:
 *  1) My snake is longer than opponent(s)
 *  2) I can make a circle around the current apple before opponent gets it
 *
 * @todo implement this...
 *
 * @param {function}  next
 * @private
 */
Karmes.prototype._appleSiege = function _appleSiege(next) {
  //Logger.silly('Entering to \'appleSiege()\' method.');

  next(null, null);
};

/**
 * Method to make actual snake move according to current position and all other stuff (and note to everyone, this isn't
 * so easy..).
 *
 * @private
 */
Karmes.prototype._makeMove = function _makeMove() {
  //Logger.silly('Entering to \'makeMove()\' method.');

  var _this = this;

  // Get next step for snake
  var nextStep = _this.level.players[_this.level.playerIndex].path.shift();

  // Oh noes we don't know where to go
  if (_.isEmpty(nextStep)) {
    Logger.warn('Oh noes, I don\'t have route to the apple');
  }

  Logger.info('Making move to next position!', JSON.stringify(nextStep));

  _this._changeDirection(nextStep);
};

/**
 * Method to change snake direction
 *
 * @param {[number, number]}  [coordinates] Coordinates where we want to move.
 * @private
 */
Karmes.prototype._changeDirection = function _changeDirection(coordinates) {
  //Logger.silly('Entering to \'changeDirection()\' method.', JSON.stringify(arguments));

  var _this = this;

  // Current position and direction of the snake
  var xFrom = _this.level.players[_this.level.playerIndex].body[0][0];
  var yFrom = _this.level.players[_this.level.playerIndex].body[0][1];
  var direction = _this.level.players[_this.level.playerIndex].direction;

  if (!(coordinates && coordinates.length === 2)) {
    coordinates = _this._getPanicMove();
  }

  // Yeah we got coordinates where we want to go
  if (coordinates && coordinates.length === 2) {
    var xTo = coordinates[0];
    var yTo = coordinates[1];

    if (xFrom !== xTo && direction !== Constants.directionRight && direction !== Constants.directionLeft) {
      Logger.info('Snake is changing direction to ' +
        Constants.directions[(xFrom < xTo) ? Constants.directionRight : Constants.directionLeft]
      );

      _this.game.sendMessage({
        msg: 'control',
        data: {
          direction: (xFrom < xTo) ? Constants.directionRight : Constants.directionLeft
        }
      });
    } else if (yFrom !== yTo && direction !== Constants.directionDown && direction !== Constants.directionUp) {
      Logger.info('Snake is changing direction to ' +
        Constants.directions[(yFrom < yTo) ? Constants.directionRight : Constants.directionLeft]
      );

      _this.game.sendMessage({
        msg: 'control',
        data: {
          direction: (yFrom < yTo) ? Constants.directionDown : Constants.directionUp
        }
      });
    } else {
      Logger.info('Snake is continues to moving to ' + Constants.directions[direction]);

      /*
      Logger.silly('Weird could not get new direction...', JSON.stringify({
        from: [xFrom, yFrom],
        to: [xTo, yTo],
        direction: Constants.directions[direction]
      }));
      */
    }
  } else {
    Logger.warn('No coordinates, this isn\'t good.');
  }
};

/**
 * Method to make 'panic move' this is initialized every time when there's no route to apple. Basically this logic
 * calculates 2x3 and 3x2 tiles from current position and checks which one contains most "free" spots.
 *
 * @todo This needs some work still, it isn't taking all spots right now, and that isn't good. REALLY this isn't good...
 *
 * @return  {[number, number]}
 * @private
 */
Karmes.prototype._getPanicMove = function _panicMove() {
  //Logger.silly('Entering to \'getPanicMove()\' method.', JSON.stringify(arguments));

  var _this = this;

  Logger.error('Yeah, panic move!');

  // Player snake
  var snake = _this.level.players[_this.level.playerIndex];

  // Current position and direction of the snake
  var xFrom = snake.body[0][0];
  var yFrom = snake.body[0][1];

  // Determine level width and height
  var levelWidth = _this.level.width;
  var levelHeight = _this.level.height;

  // Get reserved points, normal array of coordinates and 'flatten' array
  var reservedPoints = _this.level.reservedPoints;
  var reservedPointsFlatten = _.map(reservedPoints, _this.getPointString);

  // Create 'squares' for each direction; top, right, bottom and left
  var squares = [
    [ // top
      [xFrom, yFrom + 1],
      [xFrom - 1, yFrom + 1],
      [xFrom - 1, yFrom + 2],
      [xFrom, yFrom + 2],
      [xFrom + 1, yFrom + 2],
      [xFrom + 1, yFrom + 1]
    ],
    [ // right
      [xFrom + 1, yFrom],
      [xFrom + 1, yFrom + 1],
      [xFrom + 2, yFrom + 1],
      [xFrom + 2, yFrom],
      [xFrom + 2, yFrom - 1],
      [xFrom + 1, yFrom - 1]
    ],
    [ // bottom
      [xFrom, yFrom - 1],
      [xFrom + 1, yFrom - 1],
      [xFrom + 1, yFrom - 2],
      [xFrom, yFrom - 2],
      [xFrom - 1, yFrom -2],
      [xFrom - 1, yFrom - 1]
    ],
    [ // left
      [xFrom - 1, yFrom],
      [xFrom - 1, yFrom - 1],
      [xFrom - 2, yFrom - 1],
      [xFrom - 2, yFrom],
      [xFrom - 2, yFrom + 1],
      [xFrom - 1, yFrom + 1]
    ]
  ];

  // Remove all 'first' possible path from squares, this is to remove 'not possible' square
  squares = _.remove(squares, function iterator(square) {
    return reservedPointsFlatten.indexOf(_this.getPointString(square[0])) === -1;
  });

  // Check each squares that we have some coordinates to go to
  squares = _.map(squares, function iterator(square) {
    // Remove not possible coordinates
    square = _.map(square, function iterator(coordinates) {
      var x = coordinates[0];
      var y = coordinates[1];

      // Oh noes, coordinates are not allowed
      if (
        (x < 0 || x >= levelWidth) ||
        (y < 0 || y >= levelHeight) ||
        (reservedPointsFlatten.indexOf(_this.getPointString(coordinates)) !== -1)
      ) {
        return false;
      }

      return coordinates;
    });

    return _.compact(square);
  });

  // Sort squares by it length => more available spots
  squares.sort(function sorter(a, b) {
    return b.length - a.length;
  });

  Logger.error('panic data', JSON.stringify(squares));

  return squares[0][0];
};

module.exports = Karmes;
<|endoftext|>


<commit_before>#!/usr/bin/env node

/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2015, Nicolas Riesco and others as credited in the AUTHORS file
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 * may be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 */

var DEBUG = false;

var console = require("console");
var fs = require("fs");
var zmq = require("zmq");

var sm = require("./sm.js"); // Javascript sessions
var Message = require("./jp.js").Message; // IPython/Jupyter protocol

var protocolVersion = [4, 1];
// node_version can be left as str in protocol v5
var nodeVersion = process.versions.node.split('.').map(function(v) {
    return parseInt(v, 10);
});

/**
 * @class
 * @classdesc Implements a Javascript kernel for an IPython notebook.
 * @param {Object} ipythonConfig Configuration provided by IPython.
 */
function Kernel(ipythonConfig) {
    /**
     * Configuration provided by IPython
     * @member {Object}
     */
    this.config = ipythonConfig;

    /**
     * HeartBeat socket
     * @member {module:zmq~Socket}
     */
    this.hbSocket = zmq.createSocket("rep");

    /**
     * IOPub socket
     * @member {module:zmq~Socket}
     */
    this.iopubSocket = zmq.createSocket("pub");

    /**
     * Shell socket
     * @member {module:zmq~Socket}
     */
    this.shellSocket = zmq.createSocket("router");

    /**
     * Control socket
     * @member {module:zmq~Socket}
     */
    this.controlSocket = zmq.createSocket("router");

    /**
     * Javascript session
     * @member {module:sm~Session}
     */
    this.sm = new sm.Session();

    /**
     * Collection of message handlers that links a message type with the method
     * handling the response
     * @member {Object.<String, Function>}
     * @see {@link module:handler_v4}
     */
    this.handlers = require("./handlers_v4.js");

    var address = "tcp://" + this.config.ip + ":";

    this.hbSocket.bind(address + this.config.hb_port);
    this.hbSocket.on("message", onHBMessage.bind(this));

    this.iopubSocket.bind(address + this.config.iopub_port);

    this.shellSocket.bind(address + this.config.shell_port);
    this.shellSocket.on("message", onShellMessage.bind(this));

    this.controlSocket.bind(address + this.config.control_port);
    this.controlSocket.on("message", onControlMessage.bind(this));

    function onShellMessage() {
        var msg = new Message(
            arguments,
            this.config.signature_scheme.slice("hmac-".length),
            this.config.key
        );

        if (!msg.signatureOK) return;

        var msg_type = msg.header.msg_type;
        if (this.handlers.hasOwnProperty(msg_type)) {
            this.handlers[msg_type].call(this, msg);
        } else {
            // Ignore unimplemented msg_type requests
            console.warn("KERNEL: SHELL_SOCKET: Unhandled message type:", msg_type);
        }
    }

    function onControlMessage() {
        var msg = new Message(
            arguments,
            this.config.signature_scheme.slice("hmac-".length),
            this.config.key
        );

        if (!msg.signatureOK) return;

        if (msg.header.msg_type === "shutdown_request") {
            this.handlers.shutdown_request.call(this, msg);
        } else {
            // Ignore unimplemented msg_type requests
            console.warn("KERNEL: CONTROL: Unhandled message type:", msg_type);
        }
    }

    function onHBMessage(message) {
        this.hbSocket.send(message);
    }
}

/**
 * Destroy kernel
 *
 * @param {DestroyCB} [destroyCB] Callback run after the session server has been
 *                                killed and before closing the sockets
 */
Kernel.prototype.destroy = function(destroyCB) {
    if (DEBUG) console.log("KERNEL: Being destroyed");

    // TODO(NR) Handle socket `this.stdin` once it is implemented
    this.controlSocket.removeAllListeners();
    this.shellSocket.removeAllListeners();
    this.iopubSocket.removeAllListeners();
    this.hbSocket.removeAllListeners();

    this.sm.kill("SIGTERM", function(code, signal) {
        if (destroyCB) {
            destroyCB(code, signal);
        }

        this.controlSocket.close();
        this.shellSocket.close();
        this.iopubSocket.close();
        this.hbSocket.close();
    }.bind(this));
};

/**
 * @callback DestroyCB
 * @param {?Number} code   Exit code from session server if exited normally
 * @param {?String} signal Signal passed to kill the session server
 * @description Callback run after the session server has been killed and before
 * the sockets have been closed
 * @see {@link Kernel.destroy}
 */

/**
 * Restart kernel
 *
 * @param {RestartCB} [restartCB] Callback run after the session server has been
 *                                restarted
 */
Kernel.prototype.restart = function(restartCB) {
    if (DEBUG) console.log("KERNEL: Being restarted");

    this.sm.restart("SIGTERM", restartCB);
};

/**
 * @callback RestartCB
 * @param {?Number} code   Exit code from session server if exited normally
 * @param {?String} signal Signal passed to kill the session server
 * @description Callback run after the session server has been restarted
 * @see {@link Kernel.restart}
 */

// Parse command arguments
var ipythonConfig;
process.argv.slice(2).forEach(function(arg) {
    if (arg.lastIndexOf("-", 0) === 0) {
        console.warn("Warning: Argument '%s' skipped", arg);
    } else {
        ipythonConfig = JSON.parse(fs.readFileSync(arg));
    }
});

if (!ipythonConfig) throw new Error("Error: Missing connection file");

// Start up the kernel
var kernel = new Kernel(ipythonConfig);

// Interpret a SIGINT signal as a request to interrupt the kernel
process.on("SIGINT", function() {
    if (DEBUG) console.log("KERNEL: Interrupting kernel");

    // TODO(NR) Implement kernel interruption
    kernel.restart();
});
<commit_msg>send busy/idle status messages<commit_after>#!/usr/bin/env node

/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2015, Nicolas Riesco and others as credited in the AUTHORS file
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 * may be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 */

var DEBUG = false;

var console = require("console");
var fs = require("fs");
var zmq = require("zmq");

var sm = require("./sm.js"); // Javascript sessions
var Message = require("./jp.js").Message; // IPython/Jupyter protocol

var protocolVersion = [4, 1];
// node_version can be left as str in protocol v5
var nodeVersion = process.versions.node.split('.').map(function(v) {
    return parseInt(v, 10);
});

/**
 * @class
 * @classdesc Implements a Javascript kernel for an IPython notebook.
 * @param {Object} ipythonConfig Configuration provided by IPython.
 */
function Kernel(ipythonConfig) {
    /**
     * Configuration provided by IPython
     * @member {Object}
     */
    this.config = ipythonConfig;

    /**
     * HeartBeat socket
     * @member {module:zmq~Socket}
     */
    this.hbSocket = zmq.createSocket("rep");

    /**
     * IOPub socket
     * @member {module:zmq~Socket}
     */
    this.iopubSocket = zmq.createSocket("pub");

    /**
     * Shell socket
     * @member {module:zmq~Socket}
     */
    this.shellSocket = zmq.createSocket("router");

    /**
     * Control socket
     * @member {module:zmq~Socket}
     */
    this.controlSocket = zmq.createSocket("router");

    /**
     * Javascript session
     * @member {module:sm~Session}
     */
    this.sm = new sm.Session();

    /**
     * Collection of message handlers that links a message type with the method
     * handling the response
     * @member {Object.<String, Function>}
     * @see {@link module:handler_v4}
     */
    this.handlers = require("./handlers_v4.js");

    var address = "tcp://" + this.config.ip + ":";

    this.hbSocket.bind(address + this.config.hb_port);
    this.hbSocket.on("message", onHBMessage.bind(this));

    this.iopubSocket.bind(address + this.config.iopub_port);

    this.shellSocket.bind(address + this.config.shell_port);
    this.shellSocket.on("message", onShellMessage.bind(this));

    this.controlSocket.bind(address + this.config.control_port);
    this.controlSocket.on("message", onControlMessage.bind(this));

    function onShellMessage() {
        var msg = new Message(
            arguments,
            this.config.signature_scheme.slice("hmac-".length),
            this.config.key
        );

        if (!msg.signatureOK) return;

        var msg_type = msg.header.msg_type;
        if (this.handlers.hasOwnProperty(msg_type)) {
            msg.respond(this.iopubSocket, 'status', {
                execution_state: 'busy'
            });
            this.handlers[msg_type].call(this, msg);
            msg.respond(this.iopubSocket, 'status', {
                execution_state: 'idle'
            });
        } else {
            // Ignore unimplemented msg_type requests
            console.warn("KERNEL: SHELL_SOCKET: Unhandled message type:", msg_type);
        }
    }

    function onControlMessage() {
        var msg = new Message(
            arguments,
            this.config.signature_scheme.slice("hmac-".length),
            this.config.key
        );

        if (!msg.signatureOK) return;

        if (msg.header.msg_type === "shutdown_request") {
            this.handlers.shutdown_request.call(this, msg);
        } else {
            // Ignore unimplemented msg_type requests
            console.warn("KERNEL: CONTROL: Unhandled message type:", msg_type);
        }
    }

    function onHBMessage(message) {
        this.hbSocket.send(message);
    }
}

/**
 * Destroy kernel
 *
 * @param {DestroyCB} [destroyCB] Callback run after the session server has been
 *                                killed and before closing the sockets
 */
Kernel.prototype.destroy = function(destroyCB) {
    if (DEBUG) console.log("KERNEL: Being destroyed");

    // TODO(NR) Handle socket `this.stdin` once it is implemented
    this.controlSocket.removeAllListeners();
    this.shellSocket.removeAllListeners();
    this.iopubSocket.removeAllListeners();
    this.hbSocket.removeAllListeners();

    this.sm.kill("SIGTERM", function(code, signal) {
        if (destroyCB) {
            destroyCB(code, signal);
        }

        this.controlSocket.close();
        this.shellSocket.close();
        this.iopubSocket.close();
        this.hbSocket.close();
    }.bind(this));
};

/**
 * @callback DestroyCB
 * @param {?Number} code   Exit code from session server if exited normally
 * @param {?String} signal Signal passed to kill the session server
 * @description Callback run after the session server has been killed and before
 * the sockets have been closed
 * @see {@link Kernel.destroy}
 */

/**
 * Restart kernel
 *
 * @param {RestartCB} [restartCB] Callback run after the session server has been
 *                                restarted
 */
Kernel.prototype.restart = function(restartCB) {
    if (DEBUG) console.log("KERNEL: Being restarted");

    this.sm.restart("SIGTERM", restartCB);
};

/**
 * @callback RestartCB
 * @param {?Number} code   Exit code from session server if exited normally
 * @param {?String} signal Signal passed to kill the session server
 * @description Callback run after the session server has been restarted
 * @see {@link Kernel.restart}
 */

// Parse command arguments
var ipythonConfig;
process.argv.slice(2).forEach(function(arg) {
    if (arg.lastIndexOf("-", 0) === 0) {
        console.warn("Warning: Argument '%s' skipped", arg);
    } else {
        ipythonConfig = JSON.parse(fs.readFileSync(arg));
    }
});

if (!ipythonConfig) throw new Error("Error: Missing connection file");

// Start up the kernel
var kernel = new Kernel(ipythonConfig);

// Interpret a SIGINT signal as a request to interrupt the kernel
process.on("SIGINT", function() {
    if (DEBUG) console.log("KERNEL: Interrupting kernel");

    // TODO(NR) Implement kernel interruption
    kernel.restart();
});
<|endoftext|>