diff --git a/.gitignore b/.gitignore index 32f2926..0d80782 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ desktop.ini /client/dist/app.js.map /client/dist/app.js /.idea +/pics/life-has-created.png diff --git a/README.md b/README.md index 1fe5305..8c5723b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # construct -[![Build Status](https://travis-ci.org/tmptrash/jevo.js.svg?branch=master)](https://travis-ci.org/tmptrash/jevo.js) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/9bd160adb2da4ea08ff64ea8c4dbe14e)](https://www.codacy.com/app/tmptrash/jevo.js?utm_source=github.com&utm_medium=referral&utm_content=tmptrash/jevo.js&utm_campaign=Badge_Grade) +[![Build Status](https://travis-ci.org/tmptrash/construct.svg?branch=master)](https://travis-ci.org/tmptrash/construct) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/da2d5c5f53d04df79c9aae3599555b4e)](https://www.codacy.com/app/flatline/construct?utm_source=github.com&utm_medium=referral&utm_content=tmptrash/construct&utm_campaign=Badge_Grade) -construct is a native JavaScript/ES6 based, digital organisms evolution simulator. It's used for study the evolutionary biology of self-replicating and evolving computer programs ([digital organisms](https://en.wikipedia.org/wiki/Digital_organism)). This project similar to [Avida](https://en.wikipedia.org/wiki/Avida), but works with more abstract language (Digital Organism Script - DOS) instead of assembler. It uses special DOSVM for running DOS byte code and [distributed computing](https://en.wikipedia.org/wiki/Distributed_computing) to speed up the calculations. Generally, it consists of [servers](https://github.com/tmptrash/jevo.js/tree/v0.2/server/src), which just a proxy between [clients](https://github.com/tmptrash/jevo.js/tree/v0.2/client/src). All calculations are made on a client side only. It's possible to run the system in a "serverless" mode. For this, you have to run `index.html` (just drop it into the browser) in Chrome without server. - -More details on [blog](https://jevosite.wordpress.com) and youtube [channel](https://www.youtube.com/playlist?list=PL1NiKjXMaBimPuybPIXkVuO1MYy53XcdW). Video presentation in russian is [here](https://www.youtube.com/watch?v=9ykr9KzcKq8). +

construct is a native JavaScript/ES6 based, digital organisms evolution simulator. It's used for study the evolutionary biology of self-replicating and evolving computer programs (digital organisms). This project similar to Avida, but works with more abstract language (Digital Organism Script - DOS) instead of assembler. It uses special DOSVM for running DOS byte code and distributed computing to speed up the calculations. Generally, it consists of servers, which just a proxies between clients. All calculations are made on a client side only. It's possible to run the system in a "serverless" mode. For this, you have to run index.html (just drop it into the browser) in Chrome without server. More details for russian speaking people on blog and youtube channel. See video presentation here. +

# Requirements - Last version of Chrome browser @@ -14,7 +13,7 @@ More details on [blog](https://jevosite.wordpress.com) and youtube [channel](htt - Install [Node.js](https://nodejs.org/uk/) - Clone this repo to your local machine - Go to the root folder of cloned repo -- Run `npm run install` to install all construct dependencies +- Run `npm install` to install all dependencies - Run `npm run build` to build client part - Run tests using `npm run test` command if you need @@ -22,11 +21,27 @@ More details on [blog](https://jevosite.wordpress.com) and youtube [channel](htt - To run construct in a "serverless" mode, just open `./client/dist/index.html` in Chrome - To run construct in a "distributed" mode, you have to: - Choose some host in your local netwok for server - - Clone construct repo to this host + - Clone construct repo to this host - Go to configuration `./client/src/share/Config.js`, find `serverHost` option and change it to the IP, of your server host. You may use `ipconfig` under windows to get server's IP - Run `npm run build` command in a terminal from the root folder - Run server `npm run server` on chosen host - Copy `./client/dist/index.html` and `./client/dist/app.js` on all your remote machines and run it there under Chrome +# Main commands +As an administrator, you may affect the system by command line API. For instance, you may obtain amount of organisms in current population or set new configuration in real time. For this, you have to open Chrome console (press `F12`) and type `man.api[.namespace].xxx()`. Where `namespace` is an optional unit or module and `xxx()` is supported command of this module. It's possible to use `desc` property to get command description. Example: `man.api.getConfig.desc`. Here are all available commands separated by namespace: +- global namespace - `man.api`: + - `man.api.visualize(show:Boolean = true)` - Turns on/off visualization in browser for current instance (world). Turning visualization off, increases application speed. + - `man.api.formatCode(code:Array)` - Converts byte code array into human readable JavaScript based code. This function is low level. For using it you have to get organism's virtual machine reference and then use it's `code` property. For example: `man.api.formatCode(man.api.organisms.getOrganism('128').vm.code)`. This example will find organism with id `128` and shows his byte code. + - `man.api.version` - Returns current app version + - `man.api.getConfig(path:String)` - Returns specified config value. First parameter is a namespace (optional) and config name. For example, to get maximum amount of organisms in current instance/world type: `man.api.getConfig('organisms.orgMaxOrgs')`. Example of organism related configs you may find [here](https://github.com/tmptrash/construct/blob/master/client/src/manager/plugins/organisms/Config.js). Other configuration parameters are located in files with name `Config.js`. + - `man.api.setConfig(path:String, value:Any)` - Sets configuration value in real time. Opposite to `getConfig()`. +- charts namespace - `man.api.charts`. This namespace is related to statistics in charts. There are many parameters like average code size, organisms amount, amount of picked energy and so on. See details [here](https://github.com/tmptrash/construct/blob/master/client/src/manager/plugins/status/charts/Config.js) in charts property. You may show and hide different charts on a canvas, locate them and reset any time you need: + - `man.api.charts.on([[name:String = undefined[, show:Boolean = true]])` - shows chart(s) by name. List of all available names you may find [here](https://github.com/tmptrash/construct/blob/master/client/src/manager/plugins/status/charts/Config.js). Example: `man.api.charts.on('energy')` - will show chart of average organism energy at the moment. Calling this method without parameters shows all available charts. Calling this method with only one string parameter shows specified chart. Calling this method with two parameters shows/hides specified chart depending on second Boolean parameter. Example: `man.api.charts.on()` - shows all charts. `man.api.charts.on('energy')` - shows energy chart only. `man.api.charts.on('energy', false)` - hides energy chart only. + - `man.api.charts.off(name:String = undefined)` - opposite to `on()`. Hides specified or all charts (without parameters) from the canvas. + - `man.api.charts.pos(name:String, pos:String)` - Locates chart according to specified position. Available positions are: `full`, `top`, `down`, `left`, `right`, `topleft`, `downleft`, `topright`, `downright`. Example: `man.api.charts.pos('code', 'full')` - shows `code` trend chart on full screen. All available chart names are [here](https://github.com/tmptrash/construct/blob/master/client/src/manager/plugins/status/charts/Config.js). + - `man.api.charts.pos9(name:String, x:Number, y:Number)` - The same like `pos()`, but with chart coordinates in 3x3 grid. For example: `man.api.charts.pos9('energy', 0, 2)` - will positioning energy chart at the location `x:0, y:2`. + - `man.api.charts.pos16(name:String, x:Number, y:Number)` - The same like `pos9()`, but for grid 4x4. + - `man.api.charts.transparent(name:String, val:Number)` - Sets chart transparency. `val` should be between `0..1`. `val` parameter is optional. In this case all charts will have same transparency. +Note: to improve speed, type `man.api.visualize(false)` in Chrome's devtool console during application run ___ -P.S. If you `ES6 js developer` | `Canvas 2D developer` | `Node.js developer` | you just a - join us! \ No newline at end of file +P.S. If you are a `ES6 js developer` | `Canvas 2D developer` | `Node.js developer` | you just a - join us! diff --git a/client/dist/index.html b/client/dist/index.html index 7aa278c..0dad7b7 100644 --- a/client/dist/index.html +++ b/client/dist/index.html @@ -1,15 +1,10 @@ - - - - - - - construct - - - - - + + + construct - digital organisms simulator + + + + + \ No newline at end of file diff --git a/client/src/jsvm/JSVM.js b/client/src/jsvm/JSVM.js deleted file mode 100644 index 56b1cd2..0000000 --- a/client/src/jsvm/JSVM.js +++ /dev/null @@ -1,270 +0,0 @@ -/** - * Implements organism's code logic. - * TODO: explain here code one number format,... - * - * @author flatline - * TODO: may be this module is redundant - * TODO: think about custom operators callbacks from outside. This is how - * TODO: we may solve custom tasks - */ -const Helper = require('./../../../common/src/Helper'); -const Observer = require('./../../../common/src/Observer'); -const Config = require('./../../src/share/Config').Config; -const OConfig = require('./../manager/plugins/organisms/Config'); -const EVENTS = require('./../../src/share/Events').EVENTS; -const EVENT_AMOUNT = require('./../../src/share/Events').EVENT_AMOUNT; -const Num = require('./Num'); -/** - * {Number} Maximum stack size, which may be used for recursion or function parameters - */ -const MAX_STACK_SIZE = 30000; - -class JSVM extends Observer { - /** - * Creates JSVM instance. codeEndCb will be called after last code line is run. - * parent is used if JSVM instance is in a cloning mode and we have to create - * a copy of it. - * @param {Function} codeEndCb - * @param {Observer} obs Observer instance for Operators class - * @param {Function} operatorCls Class of operators - * @param {JSVM} parent Parent JSVM instance in case of cloning - */ - constructor(codeEndCb, obs, operatorCls, parent = null) { - super(EVENT_AMOUNT); - - this._obs = obs; - /** - * {Function} Class of operators, with implementation of all available - * script parts for current JSVM instance - */ - this._operatorCls = operatorCls; - /** - * {Function} Callback, which is called on every organism - * jsvm iteration. On it's end. - */ - this._onCodeEnd = codeEndCb; - /** - * {Array} Array of two numbers. first - line number where we have - * to return if first line appears. second - line number, where ends - * closing block '}' of block operator (e.g. for, if,...). - */ - this._offsets = []; - this._vars = parent && parent.vars && parent.vars.slice() || this._getVars(); - /** - * {Function} Class, which implement all supported operators - */ - this._operators = new operatorCls(this._offsets, this._vars, obs); - this._code = parent && parent.code.slice() || []; - this._line = 0; - this._linesRun = 0; - } - - get code() {return this._code} - get size() {return this._code.length} - get operators() {return this._operators} - get vars() {return this._vars} - get offsets() {return this._offsets} - get line() {return this._line} - - serialize() { - return { - offsets: this._offsets.slice(), - vars : this._vars.slice(), - // 'operators' field will be added after insertion - code : this._code.slice(), - line : this._line - }; - } - - unserialize(json) { - this._offsets = json.offsets; - this._vars = json.vars; - this._code = json.code; - this._line = json.line; - this._operators = new this._operatorCls(this._offsets, this._vars, this._obs); - } - - /** - * Walks through code lines (32bit numbers) one by one and runs associated - * with line type callback. These callbacks interpret one line of code like: - * condition, loop, function call etc... - * @param {Organism} org Current organism - */ - run(org) { - let line = this._line; - let code = this._code; - let lines = code.length; - let len = lines === 0 ? 0 : OConfig.codeYieldPeriod || lines; - let len2 = len; - let ops = this._operators.operators; - let getOp = Num.getOperator; - let ret = false; - let offs = this._offsets; - - while (len-- > 0 && org.alive) { - line = ops[getOp(code[line])](code[line], line, org, lines, ret); - // - // We found closing bracket '}' of some loop and have to return - // to the beginning of operator (e.g.: for) - // - if ((ret = (offs.length > 0 && line === offs[offs.length - 1]))) { - offs.pop(); - line = offs.pop(); - continue; - } - if (line >= lines) { - line = 0; - org.alive && (this._operators.offsets = (this._offsets = [])); - if (this._onCodeEnd) { - this._onCodeEnd(this._linesRun + (len2 - len)); - this._linesRun = -(len2 - len); - } - break; - } - } - - this._linesRun += (len2 - len); - this._line = line; - } - - destroy() { - this._operators.destroy && this._operators.destroy(); - this._operators = null; - this._vars = null; - this._code = null; - this._onCodeEnd = null; - - super.destroy(); - } - - /** - * Does crossover between two parent byte codes. Takes second jsvm's code part - * (from start1 to end1 offset) and inserts it instead first jsvm code part (start...end). - * For example: - * code1 : [1,2,3] - * code2 : [4,5,6] - * start : 1 - * end : 2 - * start1: 0 - * end1 : 2 - * jsvm1.crossover(jsvm2) // [4,5,6] instead [2,3] ->, jsvm1 === [1,4,5,6] - * - * @param {JSVM} jsvm JSVM instance, from where we have to cut code part - * @returns {Number} Amount of changes in current (this) jsvm - */ - crossover(jsvm) { - const rand = Helper.rand; - const len = this._code.length; - const len1 = jsvm.code.length; - let start = rand(len); - let end = rand(len); - let start1 = rand(len1); - let end1 = rand(len1); - let adds; - - if (start > end) { - [start, end] = [end, start] - } - if (start1 > end1) { - [start1, end1] = [end1, start1] - } - - adds = Math.abs(end1 - start1 - end + start); - if (this._code.length + adds >= OConfig.codeMaxSize) { - return 0 - } - this._code.splice.apply(this._code, [start, end - start + 1].concat(jsvm.code.slice(start1, end1 + 1))); - this._reset(); - - return adds; - } - - /** - * Takes few lines from itself and makes a copy of them. After that inserts - * them before or after copied part. All positions are random - */ - copyLines() { - const rand = Helper.rand; - const code = this._code; - const codeLen = code.length; - const start = rand(codeLen); - const end = start + rand(codeLen - start); - // - // Because we use spread (...) operator stack size is important - // for amount of parameters and we shouldn't exceed it - // - if (end - start > MAX_STACK_SIZE) { - return; - } - // - // Organism size should be less them codeMaxSize - // - if (code.length + end - start >= OConfig.codeMaxSize) {return} - // - // We may insert copied piece before "start" (0) or after "end" (1) - // - if (rand(2) === 0) { - code.splice(rand(start), 0, ...code.slice(start, end)); - return; - } - - code.splice(end + rand(codeLen - end + 1), 0, ...code.slice(start, end)); - } - - /** - * Inserts random generated number into the byte code at random position - */ - insertLine() { - this._code.splice(Helper.rand(this._code.length), 0, Num.get()); - this._reset(); - } - - updateLine(index, number) { - this._code[index] = number; - this._reset(); - } - - /** - * Removes random generated number into byte jsvm at random position - */ - removeLine() { - this._code.splice(Helper.rand(this._code.length), 1); - this._reset(); - } - - getLine(index) { - return this._code[index]; - } - - _reset() { - this.fire(EVENTS.RESET_CODE); - this._line = 0; - this._operators.offsets = (this._offsets = []); - } - - /** - * Generates default variables jsvm. It should be in ES5 version, because - * speed is important. Amount of vars depends on OConfig.codeBitsPerVar config. - * @returns {Array} vars jsvm - * @private - */ - _getVars() { - if (this._vars && this._vars.length > 0) { - return this._vars - } - - const len = Math.pow(2, OConfig.codeBitsPerVar); - let vars = new Array(len); - const range = OConfig.codeVarInitRange; - const range2 = range / 2; - const rand = Helper.rand; - - for (let i = 0; i < len; i++) { - vars[i] = rand(range) - range2; - } - - return (this._vars = vars); - } -} - -module.exports = JSVM; \ No newline at end of file diff --git a/client/src/jsvm/JSVMSpec.js b/client/src/jsvm/JSVMSpec.js deleted file mode 100644 index c083c97..0000000 --- a/client/src/jsvm/JSVMSpec.js +++ /dev/null @@ -1,518 +0,0 @@ -describe("client/src/organism/JSVM", () => { - let Observer = require('./../../../common/src/Observer'); - let Helper = require('./../../../common/src/Helper'); - let THelper = require('./../../../common/tests/Helper'); - let Config = require('./../share/Config').Config; - let OConfig = require('./../manager/plugins/organisms/Config'); - let api = require('./../share/Config').api; - let JSVM = require('./JSVM'); - let Num = require('./Num'); - let Operators = require('./Operators'); - let cls = null; - - it("Checking jsvm creation", () => { - let flag = false; - const obs = new Observer(1); - const jsvm = new JSVM(() => flag = true, obs, ()=>{}); - - jsvm.run(); - expect(flag).toEqual(false); - - jsvm.destroy(); - }); - - it("Checking parent argument and 'cloning' mode", () => { - const obs = new Observer(1); - const parent = new JSVM(()=>{}, obs, ()=>{}); - - parent.insertLine(); - const jsvm = new JSVM(()=>{}, obs, ()=>{}, parent); - - expect(jsvm.code[0] === parent.code[0]).toEqual(true); - expect(jsvm.size === parent.size).toEqual(true); - expect(jsvm.vars[0] === parent.vars[0]).toEqual(true); - - parent.destroy(); - jsvm.destroy(); - }); - - it("Checking 'vars' getter for non 'cloning' mode", () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - - expect(jsvm.vars.length === Math.pow(2, OConfig.codeBitsPerVar)).toEqual(true); - - jsvm.destroy(); - }); - - it("Checking no code size", () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - - expect(jsvm.size).toEqual(0); - jsvm.run(); - expect(jsvm.size).toEqual(0); - - jsvm.destroy(); - }); - - it("Checking destroy", () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - - jsvm.destroy(); - expect(jsvm.code).toEqual(null); - }); - - it("Checking 'code' and 'size' properties", () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - - expect(jsvm.code instanceof Array).toEqual(true); - expect(jsvm.size).toEqual(0); - - jsvm.insertLine(); - expect(jsvm.code instanceof Array).toEqual(true); - expect(jsvm.size).toEqual(1); - - jsvm.insertLine(); - expect(jsvm.size).toEqual(2); - jsvm.removeLine(); - expect(jsvm.size).toEqual(1); - jsvm.removeLine(); - expect(jsvm.size).toEqual(0); - - jsvm.destroy(); - }); - - it("Checking 'operators' property", () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, Operators); - - expect(jsvm.operators instanceof Operators).toEqual(true); - - jsvm.destroy(); - }); - - // it("Checking run method", () => { - // let flag = ''; - // class Ops extends Operators { - // get operators() {return {1: (n,l)=>{flag=n+''+l;return l+1}}} - // get size () {return 1} - // } - // const obs = new Observer(1); - // const jsvm = new JSVM(()=>{}, obs, ()=>{}); - // const coc = api.get('codeOperatorsCls'); - // const yp = api.get('codeYieldPeriod'); - // const fc = api.get('codeFitnessCls'); - // // - // // Small hack. Use of private field for this test only - // // - // jsvm._code.push(0b1000000000000000000000000); - // api.set('codeYieldPeriod', 1); - // api.set('codeFitnessCls', null); - // api.set('codeOperatorsCls', ''); - // jsvm.run({alive: true}); - // expect(flag === '167772160').toEqual(true); - // api.set('codeYieldPeriod', yp); - // api.set('codeFitnessCls', fc); - // api.set('codeOperatorsCls', coc); - // - // jsvm.destroy(); - // }); - - it("Checking crossover with increasing child code", () => { - const obs = new Observer(1); - const jsvm1 = new JSVM(()=>{}, obs, ()=>{}); - const jsvm2 = new JSVM(()=>{}, obs, ()=>{}); - const rand = Helper.rand; - let i = -1; - - Helper.rand = () => { - i++; - if (i === 0) {return 1} - if (i === 1) {return 2} - if (i === 2) {return 1} - if (i === 3) {return 3} - }; - - jsvm1._code.push(16000000); - jsvm1._code.push(16000001); - jsvm1._code.push(16000002); - jsvm1._code.push(16000003); - jsvm1._code.push(16000004); - - jsvm2._code.push(17000000); - jsvm2._code.push(17000001); - jsvm2._code.push(17000002); - jsvm2._code.push(17000003); - jsvm2._code.push(17000004); - - jsvm1.crossover(jsvm2); - expect(THelper.compare(jsvm1.code, [ - 16000000, - 17000001, - 17000002, - 17000003, - 16000003, - 16000004 - ])).toEqual(true); - - Helper.rand = rand; - jsvm1.destroy(); - jsvm2.destroy(); - }); - it("Checking crossover with decreasing child code", () => { - const obs = new Observer(1); - const jsvm1 = new JSVM(()=>{}, obs, ()=>{}); - const jsvm2 = new JSVM(()=>{}, obs, ()=>{}); - const rand = Helper.rand; - let i = -1; - - Helper.rand = () => { - i++; - if (i === 0) {return 1} - if (i === 1) {return 2} - if (i === 2) {return 1} - if (i === 3) {return 1} - }; - - jsvm1._code.push(16000000); - jsvm1._code.push(16000001); - jsvm1._code.push(16000002); - jsvm1._code.push(16000003); - jsvm1._code.push(16000004); - - jsvm2._code.push(17000000); - jsvm2._code.push(17000001); - jsvm2._code.push(17000002); - jsvm2._code.push(17000003); - jsvm2._code.push(17000004); - - jsvm1.crossover(jsvm2); - expect(THelper.compare(jsvm1.code, [ - 16000000, - 17000001, - 16000003, - 16000004 - ])).toEqual(true); - - Helper.rand = rand; - jsvm1.destroy(); - jsvm2.destroy(); - }); - it("Checking crossover with the same child code size", () => { - const obs = new Observer(1); - const jsvm1 = new JSVM(()=>{}, obs, ()=>{}); - const jsvm2 = new JSVM(()=>{}, obs, ()=>{}); - const rand = Helper.rand; - let i = -1; - - Helper.rand = () => { - i++; - if (i === 0) {return 1} - if (i === 1) {return 3} - if (i === 2) {return 1} - if (i === 3) {return 3} - }; - - jsvm1._code.push(16000000); - jsvm1._code.push(16000001); - jsvm1._code.push(16000002); - jsvm1._code.push(16000003); - jsvm1._code.push(16000004); - - jsvm2._code.push(17000000); - jsvm2._code.push(17000001); - jsvm2._code.push(17000002); - jsvm2._code.push(17000003); - jsvm2._code.push(17000004); - - jsvm1.crossover(jsvm2); - expect(THelper.compare(jsvm1.code, [ - 16000000, - 17000001, - 17000002, - 17000003, - 16000004 - ])).toEqual(true); - - Helper.rand = rand; - jsvm1.destroy(); - jsvm2.destroy(); - }); - it("Checking crossover with no code size in parents", () => { - const obs = new Observer(1); - const jsvm1 = new JSVM(()=>{}, obs, ()=>{}); - const jsvm2 = new JSVM(()=>{}, obs, ()=>{}); - - jsvm1.crossover(jsvm2); - expect(jsvm1.size).toEqual(0); - expect(jsvm2.size).toEqual(0); - - jsvm1.destroy(); - jsvm2.destroy(); - }); - it("Checking crossover with no code size for one parent and twp lines of code for other", () => { - const obs = new Observer(1); - const jsvm1 = new JSVM(()=>{}, obs, ()=>{}); - const jsvm2 = new JSVM(()=>{}, obs, ()=>{}); - const rand = Helper.rand; - let i = -1; - - Helper.rand = () => { - i++; - if (i === 0) {return 0} - if (i === 1) {return 0} - if (i === 2) {return 1} - if (i === 3) {return 2} - }; - - jsvm2._code.push(17000000); - jsvm2._code.push(17000001); - jsvm2._code.push(17000002); - jsvm2._code.push(17000003); - - jsvm1.crossover(jsvm2); - expect(THelper.compare(jsvm1.code, [ - 17000001, - 17000002 - ])).toEqual(true); - - Helper.rand = rand; - jsvm1.destroy(); - jsvm2.destroy(); - }); - it("Checking crossover with no code size for one parent and twp lines of code for other 2", () => { - const obs = new Observer(1); - const jsvm1 = new JSVM(()=>{}, obs, ()=>{}); - const jsvm2 = new JSVM(()=>{}, obs, ()=>{}); - const rand = Helper.rand; - let i = -1; - - Helper.rand = () => { - i++; - if (i === 0) {return 1} - if (i === 1) {return 2} - if (i === 2) {return 0} - if (i === 3) {return 0} - }; - - jsvm1._code.push(16000000); - jsvm1._code.push(16000001); - jsvm1._code.push(16000002); - jsvm1._code.push(16000003); - - jsvm1.crossover(jsvm2); - expect(THelper.compare(jsvm1.code, [ - 16000000, - 16000003, - ])).toEqual(true); - expect(jsvm2.size).toEqual(0); - - Helper.rand = rand; - jsvm1.destroy(); - jsvm2.destroy(); - }); - - it('Checking insertLine() method', () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - - expect(jsvm.size).toEqual(0); - jsvm.insertLine(); - expect(jsvm.size).toEqual(1); - jsvm.insertLine(); - expect(jsvm.size).toEqual(2); - - jsvm.destroy(); - }); - it('Checking insertLine() method 2', () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - let get = Num.get; - - Num.get = () => 0xabcdefff; - expect(jsvm.size).toEqual(0); - jsvm.insertLine(); - expect(jsvm.size).toEqual(1); - - expect(jsvm.code[0]).toEqual(0xabcdefff); - - Num.get = get; - jsvm.destroy(); - }); - - it('Checking copyLines() method', () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - let rand = Helper.rand; - let i = -1; - - jsvm.insertLine(); - jsvm.insertLine(); - jsvm.insertLine(); - jsvm.insertLine(); - Helper.rand = function (n) { - i++; - if (i === 0) { // start - return 1; - } else if (i === 1) { // end - return 2; - } else if (i === 2) { // rand(2) - return 0; - } else if (i === 3) { // rand(start) - return 0; - } - }; - expect(jsvm.size).toEqual(4); - jsvm.copyLines(); - expect(jsvm.size).toEqual(6); - expect(jsvm.code[0]).toEqual(jsvm.code[3]); - expect(jsvm.code[1]).toEqual(jsvm.code[4]); - - Helper.rand = rand; - jsvm.destroy(); - }); - it('Checking copyLines() method 2', () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - let rand = Helper.rand; - let i = -1; - - jsvm.insertLine(); - jsvm.insertLine(); - jsvm.insertLine(); - jsvm.insertLine(); - Helper.rand = function (n) { - i++; - if (i === 0) { // start - return 1; - } else if (i === 1) { // end - return 2; - } else if (i === 2) { // rand(2) - return 1; - } else if (i === 3) { // rand(codeLen - end) - return 0; - } - }; - expect(jsvm.size).toEqual(4); - jsvm.copyLines(); - expect(jsvm.size).toEqual(6); - expect(jsvm.code[1]).toEqual(jsvm.code[3]); - expect(jsvm.code[2]).toEqual(jsvm.code[4]); - - Helper.rand = rand; - jsvm.destroy(); - }); - it('Checking copyLines() method 3', () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - let rand = Helper.rand; - let i = -1; - - jsvm.insertLine(); - jsvm.insertLine(); - jsvm.insertLine(); - jsvm.insertLine(); - Helper.rand = function (n) { - i++; - if (i === 0) { // start - return 1; - } else if (i === 1) { // end - return 2; - } else if (i === 2) { // rand(2) - return 1; - } else if (i === 3) { // rand(codeLen - end) - return 1; - } - }; - expect(jsvm.size).toEqual(4); - jsvm.copyLines(); - expect(jsvm.size).toEqual(6); - expect(jsvm.code[1]).toEqual(jsvm.code[4]); - expect(jsvm.code[2]).toEqual(jsvm.code[5]); - - Helper.rand = rand; - jsvm.destroy(); - }); - it('Checking copyLines() method with no code', () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - let rand = Helper.rand; - - Helper.rand = () => 0; - expect(jsvm.size).toEqual(0); - jsvm.copyLines(); - expect(jsvm.size).toEqual(0); - - Helper.rand = rand; - jsvm.destroy(); - }); - - it('Checking updateLine() method', () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - let get = Num.get; - - Num.get = () => 0xabcdefff; - jsvm.insertLine(); - expect(jsvm.code[0]).toEqual(0xabcdefff); - - jsvm.updateLine(0, 0xffffffff); - expect(jsvm.code[0]).toEqual(0xffffffff); - - jsvm.updateLine(0, 0x12345678); - expect(jsvm.code[0]).toEqual(0x12345678); - - Num.get = get; - jsvm.destroy(); - }); - - it('Checking removeLine() method', () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - - jsvm.insertLine(); - expect(jsvm.size).toEqual(1); - jsvm.removeLine(); - expect(jsvm.size).toEqual(0); - - jsvm.destroy(); - }); - it('Checking removeLine() for empty code', () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - - expect(jsvm.size).toEqual(0); - jsvm.removeLine(); - expect(jsvm.size).toEqual(0); - jsvm.removeLine(); - expect(jsvm.size).toEqual(0); - - jsvm.destroy(); - }); - - it('Checking getLine()', () => { - const obs = new Observer(2); - const jsvm = new JSVM(()=>{}, obs, ()=>{}); - let get = Num.get; - - Num.get = () => 0xabcdefff; - expect(jsvm.size).toEqual(0); - expect(jsvm.getLine(0)).toEqual(undefined); - expect(jsvm.getLine(1)).toEqual(undefined); - jsvm.insertLine(); - expect(jsvm.size).toEqual(1); - expect(jsvm.getLine(0)).toEqual(0xabcdefff); - - jsvm.removeLine(); - expect(jsvm.size).toEqual(0); - expect(jsvm.getLine(0)).toEqual(undefined); - expect(jsvm.getLine(1)).toEqual(undefined); - expect(jsvm.getLine(9)).toEqual(undefined); - - Num.get = get; - jsvm.destroy(); - }); -}); \ No newline at end of file diff --git a/client/src/jsvm/Num.js b/client/src/jsvm/Num.js deleted file mode 100644 index e6b7dc4..0000000 --- a/client/src/jsvm/Num.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Class - helper for working with with byte code numbers - * - * @author flatline - */ -const Helper = require('./../../../common/src/Helper'); -const Config = require('./../../src/share/Config').Config; -const OConfig = require('./../manager/plugins/organisms/Config'); - -const BITS_PER_VAR = OConfig.codeBitsPerVar; -const BITS_PER_OPERATOR = OConfig.codeBitsPerOperator; -const NO_OPERATOR_MASK = 0xffffffff >>> BITS_PER_OPERATOR; -const BITS_OF_TWO_VARS = BITS_PER_VAR * 2; -const BITS_OF_FIRST_VAR = 32 - BITS_PER_VAR; -const MAX_VAR = 1 << BITS_PER_VAR; -const MAX_OPERATOR = 1 << BITS_PER_OPERATOR; -const VAR_BITS_OFFS = 32 - BITS_PER_OPERATOR; -const BITS_WITHOUT_2_VARS = 1 << (VAR_BITS_OFFS - BITS_PER_VAR * 2); - -class Num { - static get VAR_BITS_OFFS() {return VAR_BITS_OFFS} - static get BITS_PER_VAR() {return BITS_PER_VAR} - static get BITS_PER_OPERATOR() {return BITS_PER_OPERATOR} - static get VARS() {return (32 - BITS_PER_OPERATOR) / BITS_PER_VAR} - static get MAX_VAR() {return MAX_VAR} - static get BITS_OF_TWO_VARS() {return BITS_OF_TWO_VARS} - static get MAX_OPERATOR() {return MAX_OPERATOR} - static get BITS_WITHOUT_2_VARS() {return BITS_WITHOUT_2_VARS} - - /** - * Sets amount of available operators for first bits - * @param {Number} amount - */ - static setOperatorAmount(amount) { - this._operatorsAmount = amount; - } - - /** - * We have to use >>> 0 at the end, because << operator works - * with signed 32bit numbers, but not with unsigned like we need - * @returns {number} - */ - static get() { - const rand = Helper.rand; - return (rand(this._operatorsAmount) << (VAR_BITS_OFFS) | rand(NO_OPERATOR_MASK)) >>> 0; - } - - static getOperator(num) { - return num >>> VAR_BITS_OFFS; - } - - static setOperator(num, op) { - return (op << VAR_BITS_OFFS | (num & NO_OPERATOR_MASK)) >>> 0; - } - - static getVar(num, index = 0) { - return num << (BITS_PER_OPERATOR + index * BITS_PER_VAR) >>> BITS_OF_FIRST_VAR; - } - - /** - * Sets variable bits into value 'val' and returns updated full number. - * Example: _setVar(0xaabbccdd, 2, 0x3) -> 0x - * @param {Number} num Original number - * @param {Number} index Variable index - * @param {Number} val New variable value - * @returns {Number} - */ - static setVar(num, index, val) { - const bits = index * BITS_PER_VAR; - const lBits = VAR_BITS_OFFS - bits; - const rBits = BITS_PER_OPERATOR + bits + BITS_PER_VAR; - - return (num >>> lBits << lBits | val << (VAR_BITS_OFFS - bits - BITS_PER_VAR) | num << rBits >>> rBits) >>> 0; - } - - /** - * Returns specified bits from 32bit number. e.g.: getBits(0b11001100, 3, 2) -> 01 - * @param {Number} num - * @param {Number} start first bit offset - * @param {Number} len Amount of bits to get - * @return {Number} Cut bits (number) - */ - static getBits(num, start, len) { - return num << start >>> (32 - len); - } -} - -module.exports = Num; \ No newline at end of file diff --git a/client/src/jsvm/NumSpec.js b/client/src/jsvm/NumSpec.js deleted file mode 100644 index 5b0a675..0000000 --- a/client/src/jsvm/NumSpec.js +++ /dev/null @@ -1,77 +0,0 @@ -describe("client/src/organism/Num", () => { - let Num = require('./Num'); - - it("Checking getting random zero operator", () => { - Num.setOperatorAmount(0); - const n = Num.get() >>> 24; - - expect(n).toEqual(0); - }); - it("Checking getting random operator (0..1)", () => { - Num.setOperatorAmount(1); - const n = Num.get() >>> 24; - - expect(n === 0 || n === 1).toEqual(true); - }); - it("Checking getting random operator with probability", () => { - Num.setOperatorAmount(3); - let n; - - for (let i = 0; i < 10000; i++) { - n = Num.get() >>> 24; - expect(n >= 0 && n <= 2).toEqual(true); - } - }); - - it('Checking getOperator() method', () => { - const n = 0xabffffff; - - expect(Num.getOperator(n)).toEqual(0xab); - }); - - it('Checking setOperator() method', () => { - const n = 0xabffffff; - - expect(Num.setOperator(n, 0xbd)).toEqual(0xbdffffff); - expect(Num.setOperator(n, 0x00)).toEqual(0x00ffffff); - expect(Num.setOperator(n, 0x01)).toEqual(0x01ffffff); - expect(Num.setOperator(n, 0xff)).toEqual(0xffffffff); - }); - - it('Checking getVar() method', () => { - let n = 0xabffffff; - - expect(Num.getVar(n, 0)).toEqual(3); - expect(Num.getVar(n, 1)).toEqual(3); - expect(Num.getVar(n, 3)).toEqual(3); - - n = 0xbcbfffff; - expect(Num.getVar(n, 0)).toEqual(2); - expect(Num.getVar(n, 1)).toEqual(3); - - n = 0xbc9fffff; - expect(Num.getVar(n, 0)).toEqual(2); - expect(Num.getVar(n, 1)).toEqual(1); - - n = 0xbc00ffff; - expect(Num.getVar(n, 0)).toEqual(0); - expect(Num.getVar(n, 1)).toEqual(0); - }); - - it('Checking setVar() method', () => { - expect(Num.setVar(0xabffffff, 0, 2)).toEqual(0xabbfffff); - expect(Num.setVar(0xabffffff, 0, 3)).toEqual(0xabffffff); - expect(Num.setVar(0xabffffff, 0, 0)).toEqual(0xab3fffff); - expect(Num.setVar(0xabffffff, 2, 0)).toEqual(0xabf3ffff); - expect(Num.setVar(0xabffffff, 2, 2)).toEqual(0xabfbffff); - }); - - it('Checking getBits() method', () => { - expect(Num.getBits(0xabffffff, 0, 8)).toEqual(0xab); - expect(Num.getBits(0xabffffff, 0, 4)).toEqual(0xa); - expect(Num.getBits(0xabffffff, 4, 4)).toEqual(0xb); - expect(Num.getBits(0xabfbffff, 12, 2)).toEqual(0x2); - expect(Num.getBits(0xabcdffff, 8, 8)).toEqual(0xcd); - expect(Num.getBits(0xabcdffff, 16, 8)).toEqual(0xff); - }); -}); \ No newline at end of file diff --git a/client/src/manager/Manager.js b/client/src/manager/Manager.js index 48b6de3..190f614 100644 --- a/client/src/manager/Manager.js +++ b/client/src/manager/Manager.js @@ -20,9 +20,14 @@ const Plugins = require('./Plugins'); const EVENTS = require('./../share/Events').EVENTS; const EVENT_AMOUNT = require('./../share/Events').EVENT_AMOUNT; const Console = require('./../share/Console'); +const Helper = require('./../../../common/src/Helper'); const World = require('./../view/World').World; const WEVENTS = require('./../view/World').EVENTS; const Canvas = require('./../view/Canvas'); +/** + * {Function} Shortcut to the datetime stamp getter + */ +const TIMER = Date.now; class Manager extends Observer { /** @@ -32,6 +37,8 @@ class Manager extends Observer { */ constructor(hasView = true) { super(EVENT_AMOUNT); + const width = Config.worldWidth; + const height = Config.worldHeight; /** * {Queue} Queue of organisms in current Manager. Should be used by plugins. * Organisms plugin walk through this queue and run organism's code all the @@ -42,14 +49,16 @@ class Manager extends Observer { * {Object} positions of organisms in a world. Is used to prevent collisions * and track all world objects */ - this.positions = {}; + this.positions = []; + for (let x = 0; x < width; x++) {this.positions[x] = (new Array(height)).fill(0)} /** * {Object} This field is used as a container for public API of the Manager. * It may be used in a user console by the Operator of construct. Plugins * may add their methods to this map also. */ - this.api = {version: () => '0.2.0'}; - hasView && (this.api.visualize = this._visualize.bind(this)); + this.api = {}; + Helper.setApi(this.api, 'version', '0.2.1.1', 'Shows construct version'); + hasView && Helper.setApi(this.api, 'visualize', this._visualize.bind(this), 'Turns on/off visualization in browser for current instance (world). Turning visualization off, increases application speed.'); /** * {Boolean} Means that this manager instance doesn't contain view(canvas). @@ -85,9 +94,10 @@ class Manager extends Observer { // have an ability access Manager's API from them // this._plugins = new Plugins(this, { - plugins: Config.plugIncluded, - async : true, - run : this._onDone.bind(this) + plugins : Config.plugIncluded, + async : true, + run : this._onDone.bind(this), + isBrowser: !Config.MODE_NODE_JS }); } @@ -97,9 +107,6 @@ class Manager extends Observer { get activeAround() {return this._activeAround} get active() {return this._active} get codeRuns() {return this._codeRuns} - // TODO: this getter will be removed when hasView will be - // TODO: removed from Status plugin - get hasView() {return this._hasView} set codeRuns(cr) {this._codeRuns = cr} set clientId(id) {this._clientId = id} @@ -141,6 +148,15 @@ class Manager extends Observer { this.fire(EVENTS.ITERATION); } + /** + * Is called after all iterations + * @param {Number} counter Global counter as an analog of time + * @param {Number} stamp UNIX time stamp + */ + onLoop(counter, stamp) { + this.fire(EVENTS.LOOP); + } + /** * Returns true if at least one other Manager/client is around and is connected * to the current @@ -150,6 +166,14 @@ class Manager extends Observer { return this._activeAround.indexOf(true) !== -1; } + /** + * Resets active around clients/Managers. It means, that connection with + * server has closed or interrupted + */ + resetActive() { + for (let i = 0, active = this._activeAround, len = active.length; i < len; i++) {active[i] = false} + } + destroy(done = () => {}) { if (this._active || this._stopping) { this._destroying = true; @@ -198,7 +222,7 @@ class Manager extends Observer { (() => { let callback; - if (Config.modeNodeJs) { + if (Config.MODE_NODE_JS) { this.zeroTimeout = (fn) => setTimeout(callback = fn); return; } @@ -232,20 +256,21 @@ class Manager extends Observer { * (onIteration()) inside by calling this.zeroTimeout(). */ _onLoop () { + if (!this._active) {return} // // This conditions id needed for turned on visualization mode to // prevent flickering of organisms in a canvas. It makes their // movement smooth // - const amount = this._visualized ? 1 : OConfig.codeIterationsPerOnce; - const timer = Date.now; - let counter = this._counter; + let amount = this._visualized ? 1 : OConfig.codeIterationsPerOnce; + let counter = this._counter; + let i; - for (let i = 0; i < amount; i++) { - this.onIteration(counter++, timer()); + for (i = counter, amount = counter + amount; i < amount; i++) { + this.onIteration(i, TIMER()); } - this._counter = counter; - this._active && this.zeroTimeout(this._onLoopCb); + this.onLoop(this._counter = i, TIMER()); + this.zeroTimeout(this._onLoopCb); } _addHandlers() { diff --git a/client/src/manager/ManagerSpec.js b/client/src/manager/ManagerSpec.js index 2af478b..254c847 100644 --- a/client/src/manager/ManagerSpec.js +++ b/client/src/manager/ManagerSpec.js @@ -1,21 +1,26 @@ describe("client/src/manager/Manager", () => { + const SERVER_HOST = 'ws://127.0.0.1'; const Config = require('./../../../client/src/share/Config').Config; const OConfig = require('./../manager/plugins/organisms/Config'); const SConfig = require('./../../../server/src/share/Config').Config; - const OLD_MODE = Config.modeNodeJs; - Config.modeNodeJs = true; const Server = require('./../../../server/src/server/Server').Server; const EVENTS = require('./../../../client/src/share/Events').EVENTS; const SEVENTS = require('./../../../server/src/server/Server').EVENTS; const Console = require('./../../../client/src/share/Console'); const SConsole = require('./../../../server/src/share/Console'); const THelper = require('./../../../common/tests/Helper'); + const ConfigHelper = require('./../../../common/tests/Config'); const World = require('./../../../client/src/view/World').World; const Manager = require('./Manager'); - const emptyFn = () => {}; const waitEvent = THelper.waitEvent; const wait = THelper.wait; + const testQ = THelper.testQ; const host = Config.serverHost; + const port = SConfig.port; + const maxConns = SConfig.maxConnections; + const startOrgs = OConfig.orgStartAmount; + const energyCheck = Config.worldEnergyCheckPeriod; + const emp = () => {}; let error; let warn; @@ -23,30 +28,42 @@ describe("client/src/manager/Manager", () => { let serror; let swarn; let sinfo; + let dist; let timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; - - beforeEach(() => { + function deletePluginConfigs() { delete Config.ips; delete Config.organisms; - }); + delete Config.status; + delete Config.charts; + delete Config.console; + } + + beforeEach(() => deletePluginConfigs()); beforeAll(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - Config.serverHost = 'ws://127.0.0.1'; + Config.serverHost = SERVER_HOST; Config.plugIncluded.splice(Config.plugIncluded.indexOf('ips/Ips')); + dist = SConfig.modeDistributed; + SConfig.modeDistributed = false; + SConfig.port = Config.serverPort; + SConfig.maxConnections = 100; + OConfig.orgStartAmount = 0; + Config.worldEnergyCheckPeriod = 0; + error = Console.error; warn = Console.warn; info = Console.info; - Console.error = emptyFn; - Console.warn = emptyFn; - Console.info = emptyFn; + Console.error = emp; + Console.warn = emp; + Console.info = emp; serror = SConsole.error; swarn = SConsole.warn; sinfo = SConsole.info; - SConsole.error = emptyFn; - SConsole.warn = emptyFn; - SConsole.info = emptyFn; + SConsole.error = emp; + SConsole.warn = emp; + SConsole.info = emp; }); afterAll(() => { SConsole.error = serror; @@ -56,509 +73,832 @@ describe("client/src/manager/Manager", () => { Console.error = error; Console.warn = warn; Console.info = info; - Config.modeNodeJs = OLD_MODE; Config.plugIncluded.push('ips/Ips'); jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout; Config.serverHost = host; + SConfig.modeDistributed = dist; + SConfig.port = port; + SConfig.maxConnections = maxConns; + OConfig.orgStartAmount = startOrgs; + Config.worldEnergyCheckPeriod = energyCheck; }); - it("Checking manager creation", (done) => { - const man = new Manager(false); - expect(man.canvas).toBe(null); - man.destroy(done); - }); - it("Checking manager creation and it's properties", (done) => { - const man = new Manager(false); - expect(man.organisms.size).toBe(0); - expect(Object.keys(man.positions).length).toBe(0); - expect(man.codeRuns).toBe(0); - expect(!!man.api.version).toBe(true); - expect(man.api.visualize).toBe(undefined); - expect(man.active).toBe(false); - expect(man.clientId).toBe(null); - expect(man.isDistributed()).toBe(false); - expect(man.hasView).toBe(false); - man.destroy(done); - }); - it("Checking creation of two managers", (done) => { - const man1 = new Manager(false); - delete Config.organisms; - const man2 = new Manager(false); - - waitEvent(man1, EVENTS.DESTROY, () => man1.destroy(), () => { - waitEvent(man2, EVENTS.DESTROY, () => man2.destroy(), done); + describe('Manager creation/destroy', () => { + it("Checking manager creation without view", (done) => { + const man = new Manager(false); + expect(man.canvas).toBe(null); + man.destroy(done); + }); + it("Checking manager creation and it's properties", (done) => { + const man = new Manager(false); + expect(man.organisms.size).toBe(0); + expect(man.positions.length).toBe(Config.worldWidth); + expect(man.codeRuns).toBe(0); + expect(!!man.api.version).toBe(true); + expect(man.api.visualize).toBe(undefined); + expect(man.active).toBe(false); + expect(man.clientId).toBe(null); + expect(man.isDistributed()).toBe(false); + expect(man.canvas).toBe(null); + expect(man.active).toBe(false); + expect(man.world instanceof World).toBe(true); + expect(man.activeAround).toEqual([false, false, false, false]); + man.destroy(done); + }); + it("Checking creation of two managers", (done) => { + const man1 = new Manager(false); + deletePluginConfigs(); + const man2 = new Manager(false); + + testQ(done, + [man1, EVENTS.DESTROY, () => man1.destroy(), emp], + [man2, EVENTS.DESTROY, () => man2.destroy(), emp] + ); + }); + it("Checking creation 100 managers", (done) => { + const mans = []; + const amount = 100; + const width = Config.worldWidth; + const height = Config.worldHeight; + let destroyed = 0; + let waitObj = {done: false}; + + Config.worldWidth = 10; + Config.worldHeight = 10; + for (let i = 0; i < amount; i++) {deletePluginConfigs(); mans.push(new Manager(false))} + for (let i = 0; i < amount; i++) {mans[i].destroy(() => ++destroyed === amount && (waitObj.done = true))} + + if (waitObj.done) { + Config.worldWidth = width; + Config.worldHeight = height; + done(); + return; + } + wait(waitObj, () => { + Config.worldWidth = width; + Config.worldHeight = height; + done(); + }, 31000); + }); + it("Checking DESTROY event", (done) => { + const man = new Manager(false); + testQ(done, + [man, EVENTS.RUN, () => man.run(), emp], + [man, EVENTS.STOP, () => man.stop(), emp], + [man, EVENTS.DESTROY, () => man.destroy(), emp] + ); }); - }); - it("Checking creation 100 managers", (done) => { - const mans = []; - const amount = 100; - const width = Config.worldWidth; - const height = Config.worldHeight; - let destroyed = 0; - let waitObj = {done: false}; - - Config.worldWidth = 10; - Config.worldHeight = 10; - for (let i = 0; i < amount; i++) {delete Config.organisms; mans.push(new Manager(false))} - for (let i = 0; i < amount; i++) {mans[i].destroy(() => ++destroyed === amount && (waitObj.done = true))} - - if (waitObj.done) { - Config.worldWidth = width; - Config.worldHeight = height; - done(); - return; - } - wait(waitObj, () => { - Config.worldWidth = width; - Config.worldHeight = height; - done(); - }, 31000); }); - it("Checking running manager", (done) => { - const man = new Manager(false); - man.run(() => man.on(EVENTS.ITERATION, () => man.destroy(done))); - }); - it("Checking if manager runs main loop", (done) => { - const man = new Manager(false); - let count = 0; - man.run(() => man.on(EVENTS.ITERATION, () => { - ++count === 100 && man.destroy(done); - })); + describe('Run/Stop manager', () => { + it("Checking running manager", (done) => { + const man = new Manager(false); + man.run(() => man.on(EVENTS.ITERATION, () => man.destroy(done))); + }); + it("Checking RUN event", (done) => { + const man = new Manager(false); + waitEvent(man, EVENTS.RUN, () => man.run(), () => man.destroy(done)); + }); + it("Checking STOP event", (done) => { + const man = new Manager(false); + testQ(done, + [man, EVENTS.RUN, () => man.run(), emp], + [man, EVENTS.STOP, () => man.stop(), emp], + [man, EVENTS.DESTROY, () => man.destroy(), emp] + ); + }); }); - it("Checking RUN event", (done) => { - const man = new Manager(false); - waitEvent(man, EVENTS.RUN, () => man.run(), () => man.destroy(done)); - }); - it("Checking STOP event", (done) => { - const man = new Manager(false); - waitEvent(man, EVENTS.RUN, () => man.run(), () => { - waitEvent(man, EVENTS.STOP, () => man.stop(), () => man.destroy(done)); + describe('Main loop checks', () => { + let log; + beforeAll(() => {log = console.log; console.log = () => {}}); + afterAll (() => console.log = log); + + it("Checking if manager runs main loop", (done) => { + const man = new Manager(false); + let count = 0; + man.run(() => man.on(EVENTS.ITERATION, () => { + ++count === 100 && man.destroy(done); + })); }); - }); - it("Checking DESTROY event", (done) => { - const man = new Manager(false); - waitEvent(man, EVENTS.RUN, () => man.run(), () => { - waitEvent(man, EVENTS.STOP, () => man.stop(), () => { - waitEvent(man, EVENTS.DESTROY, () => man.destroy(), done); + it("Checking ITERATION event", (done) => { + const man = new Manager(false); + let ok = false; + + man.on(EVENTS.ITERATION, () => ok = true); + waitEvent(man, EVENTS.RUN, () => man.run(), () => { + expect(ok).toBe(true); + waitEvent(man, EVENTS.STOP, () => man.stop(), () => man.destroy(done)); }); }); - }); - it("Checking ITERATION event", (done) => { - const man = new Manager(false); - let ok = false; - - man.on(EVENTS.ITERATION, () => ok = true); - waitEvent(man, EVENTS.RUN, () => man.run(), () => { - expect(ok).toBe(true); - waitEvent(man, EVENTS.STOP, () => man.stop(), () => man.destroy(done)); + it("Checking LOOP event", (done) => { + const man = new Manager(false); + let ok = false; + + man.on(EVENTS.LOOP, () => ok = true); + waitEvent(man, EVENTS.RUN, () => man.run(), () => { + expect(ok).toBe(true); + waitEvent(man, EVENTS.STOP, () => man.stop(), () => man.destroy(done)); + }); }); }); - it("Checking isDistributed() method", (done) => { - const man = new Manager(false); + describe('isDistributed() method', () => { + it("Checking isDistributed() method", (done) => { + const man = new Manager(false); - expect(man.isDistributed()).toBe(false); - waitEvent(man, EVENTS.RUN, () => man.run(), () => { expect(man.isDistributed()).toBe(false); - waitEvent(man, EVENTS.STOP, () => man.stop(), () => { + waitEvent(man, EVENTS.RUN, () => man.run(), () => { expect(man.isDistributed()).toBe(false); - man.destroy(done); + waitEvent(man, EVENTS.STOP, () => man.stop(), () => { + expect(man.isDistributed()).toBe(false); + man.destroy(done); + }); }); }); }); - it("Checking 'codeRuns' property", (done) => { - const man = new Manager(false); - let ok = false; - - man.on(EVENTS.ITERATION, () => ok = true); - expect(man.codeRuns).toBe(0); - waitEvent(man, EVENTS.RUN, () => man.run(), () => { - // codeRuns should be 0, because there is no code lines - expect(man.codeRuns).toBe(0); - waitEvent(man, EVENTS.STOP, () => man.stop(), () => man.destroy(done)); - }); - }); - it("Checking running of manager with a server", (done) => { - const server = new Server(); - const man = new Manager(false); - - expect(man.clientId).toBe(null); - server.run(); - man.run(() => { - expect(man.active).toBe(true); - expect(man.clientId !== null).toBe(true); - man.destroy(() => { - waitEvent(server, SEVENTS.DESTROY, () => server.destroy(), done); + describe('codeRunc property', () => { + it("Checking 'codeRuns' property", (done) => { + const man = new Manager(false); + let ok = false; + + man.on(EVENTS.ITERATION, () => ok = true); + expect(man.codeRuns).toBe(0); + waitEvent(man, EVENTS.RUN, () => man.run(), () => { + // codeRuns should be 0, because there is no code lines + expect(man.codeRuns).toBe(0); + waitEvent(man, EVENTS.STOP, () => man.stop(), () => man.destroy(done)); }); }); }); - it("Checking one organism creation in a manager", (done) => { - const man = new Manager(false); - const amount = OConfig.orgStartAmount; - const period = OConfig.orgRainMutationPeriod; - const percent = OConfig.orgCloneMutationPercent; - const clone = OConfig.orgClonePeriod; - let iterated = false; - - OConfig.orgStartAmount = 1; - OConfig.orgRainMutationPeriod = 0; - OConfig.orgCloneMutationPercent = 0; - OConfig.orgClonePeriod = 0; - expect(man.organisms.size).toBe(0); - man.on(EVENTS.ITERATION, () => { - if (iterated) {return} - expect(man.organisms.size).toBe(1); - man.stop(() => { - man.destroy(() => { - OConfig.orgClonePeriod = clone; - OConfig.orgCloneMutationPercent = percent; - OConfig.orgRainMutationPeriod = period; - OConfig.orgStartAmount = amount; - done(); + describe('Managers with a server', () => { + it("Checking running of manager with a server", (done) => { + const server = new Server(); + const man = new Manager(false); + + expect(man.clientId).toBe(null); + waitEvent(server, SEVENTS.RUN, () => server.run(), () => { + man.run(() => { + expect(man.active).toBe(true); + expect(man.clientId !== null).toBe(true); + man.destroy(() => { + waitEvent(server, SEVENTS.DESTROY, () => server.destroy(), done); + }); }); }); - iterated = true; }); - man.run(); - }); - it("Checking two managers with a server", (done) => { - const amount = OConfig.orgStartAmount; - const period = OConfig.orgRainMutationPeriod; - const percent = OConfig.orgCloneMutationPercent; - const period1 = OConfig.orgEnergySpendPeriod; - const clone = OConfig.orgClonePeriod; - const server = new Server(); - const man1 = new Manager(false); - delete Config.organisms; - const man2 = new Manager(false); - let iterated1 = false; - let iterated2 = false; - let blocked = false; - const destroy = () => { - blocked = true; - man1.destroy(() => { - man2.destroy(() => { - waitEvent(server, SEVENTS.DESTROY, () => server.destroy(), () => { - OConfig.orgClonePeriod = clone; - OConfig.orgEnergySpendPeriod = period1; - OConfig.orgCloneMutationPercent = percent; - OConfig.orgRainMutationPeriod = period; - OConfig.orgStartAmount = amount; - done(); + it("Checking two managers with a server", (done) => { + const amount = OConfig.orgStartAmount; + const period = OConfig.orgRainMutationPeriod; + const percent = OConfig.orgCloneMutationPercent; + const period1 = OConfig.orgEnergySpendPeriod; + const max = OConfig.orgMaxOrgs; + const log = console.log; + const server = new Server(); + const man1 = new Manager(false); + deletePluginConfigs(); + const man2 = new Manager(false); + let iterated1 = false; + let iterated2 = false; + let blocked = false; + const destroy = () => { + blocked = true; + man1.destroy(() => { + man2.destroy(() => { + waitEvent(server, SEVENTS.DESTROY, () => server.destroy(), () => { + OConfig.orgEnergySpendPeriod = period1; + OConfig.orgCloneMutationPercent = percent; + OConfig.orgRainMutationPeriod = period; + OConfig.orgStartAmount = amount; + OConfig.orgMaxOrgs = max; + console.log = log; + done(); + }); }); }); + }; + + OConfig.orgStartAmount = 1; + OConfig.orgRainMutationPeriod = 0; + OConfig.orgCloneMutationPercent = 0; + OConfig.orgEnergySpendPeriod = 0; + OConfig.orgMaxOrgs = 1; + console.log = () => {}; + expect(man1.clientId).toBe(null); + expect(man2.clientId).toBe(null); + expect(man1.organisms.size).toBe(0); + expect(man2.organisms.size).toBe(0); + + man1.on(EVENTS.LOOP, () => { + if (blocked) {return} + expect(man1.organisms.size).toBe(1); + if (iterated1 && iterated2) {destroy(); return} + iterated1 = true; + }); + man2.on(EVENTS.LOOP, () => { + if (blocked) {return} + expect(man2.organisms.size).toBe(1); + if (iterated2 && iterated1) {destroy(); return} + iterated2 = true; }); - }; - - OConfig.orgStartAmount = 1; - OConfig.orgRainMutationPeriod = 0; - OConfig.orgCloneMutationPercent = 0; - OConfig.orgEnergySpendPeriod = 0; - OConfig.orgClonePeriod = 0; - expect(man1.clientId).toBe(null); - expect(man2.clientId).toBe(null); - expect(man1.organisms.size).toBe(0); - expect(man2.organisms.size).toBe(0); - server.run(); - - man1.on(EVENTS.ITERATION, () => { - if (blocked) {return} - expect(man1.organisms.size).toBe(1); - if (iterated1 && iterated2) {destroy(); return} - iterated1 = true; - }); - man2.on(EVENTS.ITERATION, () => { - if (blocked) {return} - expect(man2.organisms.size).toBe(1); - if (iterated2 && iterated1) {destroy(); return} - iterated2 = true; - }); - man1.run(() => { - expect(man1.active).toBe(true); - expect(man1.clientId !== null).toBe(true); - man2.run(() => { - expect(man2.active).toBe(true); - expect(man2.clientId !== null).toBe(true); + waitEvent(server, server.EVENTS.RUN, () => server.run(), () => { + man1.run(() => { + expect(man1.active).toBe(true); + expect(man1.clientId !== null).toBe(true); + man2.run(() => { + expect(man2.active).toBe(true); + expect(man2.clientId !== null).toBe(true); + }); + }); }); }); }); - it("Checking moving of organism from one Manager to another", (done) => { - const amount = OConfig.orgStartAmount; - const period = OConfig.orgRainMutationPeriod; - const percent = OConfig.orgCloneMutationPercent; - const period1 = OConfig.orgEnergySpendPeriod; - const clone = OConfig.orgClonePeriod; - const height = Config.worldHeight; - const energy = OConfig.orgStartEnergy; - const server = new Server(); - const man1 = new Manager(false); - delete Config.organisms; - const man2 = new Manager(false); - let iterated1 = 0; - let iterated2 = 0; - let freePos = World.prototype.getFreePos; - let org1 = null; - const destroy = () => { - man1.destroy(() => { - man2.destroy(() => { - waitEvent(server, SEVENTS.DESTROY, () => server.destroy(), () => { - World.prototype.getFreePos = freePos; - OConfig.orgStartEnergy = energy; - OConfig.orgClonePeriod = clone; - OConfig.orgEnergySpendPeriod = period1; + describe('Organism related tests', () => { + let originalTimeout; + beforeEach(() => { + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; + }); + afterEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout); + + it("Checking one organism creation in a manager", (done) => { + const man = new Manager(false); + const amount = OConfig.orgStartAmount; + const period = OConfig.orgRainMutationPeriod; + const percent = OConfig.orgCloneMutationPercent; + let iterated = false; + + OConfig.orgStartAmount = 1; + OConfig.orgRainMutationPeriod = 0; + OConfig.orgCloneMutationPercent = 0; + expect(man.organisms.size).toBe(0); + man.on(EVENTS.LOOP, () => { + if (iterated) {return} + expect(man.organisms.size).toBe(1); + man.stop(() => { + man.destroy(() => { OConfig.orgCloneMutationPercent = percent; OConfig.orgRainMutationPeriod = period; OConfig.orgStartAmount = amount; - Config.worldHeight = height; done(); }); }); + iterated = true; }); - }; - - OConfig.orgStartAmount = 1; - OConfig.orgRainMutationPeriod = 0; - OConfig.orgCloneMutationPercent = 0; - OConfig.orgEnergySpendPeriod = 0; - OConfig.orgClonePeriod = 0; - Config.worldHeight = 400; - OConfig.orgStartEnergy = 10000; - World.prototype.getFreePos = () => {return {x: 1, y: 399}}; - - man1.on(EVENTS.ITERATION, () => { - if (iterated1 > 0 && iterated2 > 0 && org1 === null) { - org1 = man1.organisms.first.val; - org1.jsvm.code.push(0b00001101000000000000000000000000); // onStepDown() - } else if (man2.organisms.size === 2) { - destroy(); - } - if (iterated1 > 10000) {throw 'Error sending organism between Managers'} - iterated1++; + man.run(); }); - man2.on(EVENTS.ITERATION, () => iterated2++); - - server.run(); - man1.run(man2.run); - }); - /** - * The meaning of this test is in checking if one organism from up manager - * will go into the down manager, but there will be another organism. First - * organism should die in this case. - */ - it("Checking moving of organism from one Manager to another 2", (done) => { - const amount = OConfig.orgStartAmount; - const period = OConfig.orgRainMutationPeriod; - const percent = OConfig.orgCloneMutationPercent; - const period1 = OConfig.orgEnergySpendPeriod; - const clone = OConfig.orgClonePeriod; - const height = Config.worldHeight; - const energy = OConfig.orgStartEnergy; - const server = new Server(); - const man1 = new Manager(false); - delete Config.organisms; - const man2 = new Manager(false); - let iterated1 = 0; - let iterated2 = 0; - let freePos = World.prototype.getFreePos; - let org1 = null; - let org2 = null; - let inc = 0; - let doneInc = 0; - const destroy = () => { - man1.destroy(() => { - man2.destroy(() => { - waitEvent(server, SEVENTS.DESTROY, () => server.destroy(), () => { - World.prototype.getFreePos = freePos; - OConfig.orgStartEnergy = energy; - OConfig.orgClonePeriod = clone; - OConfig.orgEnergySpendPeriod = period1; - OConfig.orgCloneMutationPercent = percent; - OConfig.orgRainMutationPeriod = period; - OConfig.orgStartAmount = amount; - Config.worldHeight = height; - done(); + it("Checking moving of organism from one Manager to another", (done) => { + const amount = OConfig.orgStartAmount; + const period = OConfig.orgRainMutationPeriod; + const percent = OConfig.orgCloneMutationPercent; + const period1 = OConfig.orgEnergySpendPeriod; + const width = Config.worldWidth; + const height = Config.worldHeight; + const energy = OConfig.orgStartEnergy; + const max = OConfig.orgMaxOrgs; + const server = new Server(); + Config.worldWidth = 400; + Config.worldHeight = 400; + const man1 = new Manager(false); + deletePluginConfigs(); + const man2 = new Manager(false); + let iterated1 = 0; + let iterated2 = 0; + let freePos = World.prototype.getFreePos; + let org1 = null; + const destroy = () => { + man1.destroy(() => { + man2.destroy(() => { + waitEvent(server, SEVENTS.DESTROY, () => server.destroy(), () => { + World.prototype.getFreePos = freePos; + OConfig.orgStartEnergy = energy; + OConfig.orgEnergySpendPeriod = period1; + OConfig.orgCloneMutationPercent = percent; + OConfig.orgRainMutationPeriod = period; + OConfig.orgStartAmount = amount; + Config.worldWidth = width; + Config.worldHeight = height; + OConfig.orgMaxOrgs = max; + done(); + }); }); }); + }; + + OConfig.orgStartAmount = 1; + OConfig.orgRainMutationPeriod = 0; + OConfig.orgCloneMutationPercent = 0; + OConfig.orgEnergySpendPeriod = 0; + OConfig.orgStartEnergy = 10000; + OConfig.orgMaxOrgs = 2; + World.prototype.getFreePos = () => {return [399, 1]}; + + man1.on(EVENTS.LOOP, () => { + if (iterated1 > 0 && iterated2 > 0 && org1 === null) { + org1 = man1.organisms.first.val; + org1.vm.insertLine(); + org1.vm.updateLine(0, 0b00001011000000000000000000000000); // onStepRight() + } else if (man2.organisms.size === 2) { + destroy(); + } + if (iterated1 > 10000) {throw 'Error sending organism between Managers'} + iterated1++; }); - }; - - OConfig.orgStartAmount = 1; - OConfig.orgRainMutationPeriod = 0; - OConfig.orgCloneMutationPercent = 0; - OConfig.orgEnergySpendPeriod = 0; - OConfig.orgClonePeriod = 0; - Config.worldHeight = 400; - OConfig.orgStartEnergy = 10000; - World.prototype.getFreePos = () => {return inc++ === 0 && {x: 1, y: 399} || {x: 1, y: 0}}; - - man1.on(EVENTS.ITERATION, () => { - if (iterated1 > 0 && iterated2 > 0 && org1 === null && org2 !== null) { - org1 = man1.organisms.first.val; - org1.jsvm.code.push(0b00001101000000000000000000000000); // onStepDown() - man1.on(EVENTS.STEP_OUT, () => { - expect(doneInc < 3).toBe(true); - ++doneInc; + man2.on(EVENTS.LOOP, () => iterated2++); + + waitEvent(server, server.EVENTS.RUN, () => server.run(), () => man1.run(() => man2.run())); + }); + /** + * The meaning of this test is in checking if one organism from left manager + * will go to the right manager, but there will be another organism. First + * organism should die in this case. + */ + it("Checking moving of organism from one Manager to another 2", (done) => { + const amount = OConfig.orgStartAmount; + const period = OConfig.orgRainMutationPeriod; + const percent = OConfig.orgCloneMutationPercent; + const period1 = OConfig.orgEnergySpendPeriod; + const height = Config.worldHeight; + const energy = OConfig.orgStartEnergy; + const max = OConfig.orgMaxOrgs; + const server = new Server(); + Config.worldHeight = 400; + Config.worldWidth = 400; + const man1 = new Manager(false); + deletePluginConfigs(); + const man2 = new Manager(false); + let iterated1 = 0; + let iterated2 = 0; + let freePos = World.prototype.getFreePos; + let org1 = null; + let org2 = null; + let inc = 0; + let doneInc = 0; + const destroy = () => { + man1.destroy(() => { + man2.destroy(() => { + waitEvent(server, SEVENTS.DESTROY, () => server.destroy(), () => { + World.prototype.getFreePos = freePos; + OConfig.orgStartEnergy = energy; + OConfig.orgEnergySpendPeriod = period1; + OConfig.orgCloneMutationPercent = percent; + OConfig.orgRainMutationPeriod = period; + OConfig.orgStartAmount = amount; + Config.worldHeight = height; + OConfig.orgMaxOrgs = max; + done(); + }); + }); }); - man2.on(EVENTS.STEP_IN, () => { - ++doneInc; + }; + + OConfig.orgStartAmount = 1; + OConfig.orgRainMutationPeriod = 0; + OConfig.orgCloneMutationPercent = 0; + OConfig.orgEnergySpendPeriod = 0; + OConfig.orgStartEnergy = 10000; + OConfig.orgMaxOrgs = 1; + World.prototype.getFreePos = () => {return inc++ === 0 && [399, 1] || [0, 1]}; + + man1.on(EVENTS.LOOP, () => { + if (iterated1 > 0 && iterated2 > 0 && org1 === null && org2 !== null) { + org1 = man1.organisms.first.val; + org1.vm.code.push(0b00001011000000000000000000000000); // onStepRight() + man1.on(EVENTS.STEP_OUT, () => { + expect(doneInc < 3).toBe(true); + ++doneInc; + }); + man2.on(EVENTS.STEP_IN, () => { + ++doneInc; + expect(man1.organisms.size).toBe(1); + expect(man1.organisms.first.val.x).toBe(0); + }); + } else if (org1 !== null && org2 !== null && doneInc === 2) { expect(man1.organisms.size).toBe(1); - expect(man1.organisms.first.val.y).toBe(0); - }); - } else if (org1 !== null && org2 !== null && doneInc === 2) { - expect(man1.organisms.size).toBe(1); - expect(man1.organisms.first.val.y).toBe(0); - expect(man2.organisms.size).toBe(1); - expect(man2.organisms.first.val.y).toBe(0); - destroy(); - doneInc++; - } - if (iterated1 > 10000) {throw 'Error sending organism between Managers'} - iterated1++; - }); - man2.on(EVENTS.ITERATION, () => { - !iterated2 && (org2 = man2.organisms.first.val); - iterated2++; - }); + expect(man1.organisms.first.val.x).toBe(0); + expect(man2.organisms.size).toBe(1); + expect(man2.organisms.first.val.x).toBe(0); + destroy(); + doneInc++; + } + if (iterated1 > 10000) {throw 'Error sending organism between Managers'} + iterated1++; + }); + man2.on(EVENTS.LOOP, () => { + !iterated2 && (org2 = man2.organisms.first.val); + iterated2++; + }); - server.run(); - man1.run(man2.run); - }); + waitEvent(server, server.EVENTS.RUN, () => server.run(), () => man1.run(() => man2.run())); + }); + it('Tests organism moving from client of one server to client of other server', (done) => { + const log = console.log; + const ocfg = new ConfigHelper(OConfig); + const cfg = new ConfigHelper(Config); + const scfg = new ConfigHelper(SConfig); + const freePos = World.prototype.getFreePos; + let iterated1 = 0; + let iterated2 = 0; + let org1 = null; + const destroy = () => { + man1.destroy(() => { + man2.destroy(() => { + waitEvent(server1, SEVENTS.DESTROY, () => server1.destroy(), () => { + waitEvent(server2, SEVENTS.DESTROY, () => server2.destroy(), () => { + World.prototype.getFreePos = freePos; + ocfg.reset(); + scfg.reset(); + cfg.reset(); + console.log = log; + done(); + }); + }); + }); + }); + }; + + console.log = () => {}; + scfg.set('upHost', SERVER_HOST); + scfg.set('rightHost', SERVER_HOST); + scfg.set('downHost', SERVER_HOST); + scfg.set('leftHost', SERVER_HOST); + ocfg.set('codeIterationsPerOnce', 1); + scfg.set('modeDistributed', true); + scfg.set('maxConnections', 1); + scfg.set('port', 3000); + scfg.set('rightPort', 3001); + const server1 = new Server(); // left server + scfg.set('port', 3001); + scfg.set('leftPort', 3000); + scfg.set('rightPort', 1001); + const server2 = new Server(); // right server + cfg.set('worldWidth', 10); + cfg.set('worldHeight', 10); + cfg.set('serverPort', 3000); + cfg.set('serverHost', SERVER_HOST); + const man1 = new Manager(false); + deletePluginConfigs(); + cfg.set('serverPort', 3001); + const man2 = new Manager(false); + ocfg.set('orgStartAmount', 1); + ocfg.set('orgRainMutationPeriod', 0); + ocfg.set('orgCloneMutationPercent',0); + ocfg.set('orgEnergySpendPeriod', 0); + ocfg.set('orgStartEnergy', 10000); + cfg.set('worldCyclical', false); + World.prototype.getFreePos = () => {return [9, 1]}; + + man1.on(EVENTS.LOOP, () => { + if (iterated1 > 0 && iterated2 > 0 && org1 === null) { + expect(man2.organisms.size).toBe(1); + org1 = man1.organisms.first.val; + org1.vm.code.push(0b00001011000000000000000000000000); // onStepRight() + } else if (man2.organisms.size === 2) { + destroy(); + } + if (iterated1 > 10000) {throw 'Error sending organism between Servers'} + iterated1++; + }); + man2.on(EVENTS.LOOP, () => iterated2++); - it("Testing hundred managers and one server", (done) => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 35000; - const maxCons = SConfig.maxConnections; - const server = new Server(); - const mans = []; - const CLIENTS = 100; - const width = Config.worldWidth; - const height = Config.worldHeight; - let amount = 0; - let waitObj = {done: false}; - let man; - - Config.worldWidth = 100; - Config.worldHeight = 100; - SConfig.maxConnections = CLIENTS; - server.run(); - for (let i = 0; i < CLIENTS; i++) { - delete Config.organisms; - mans.push(man = new Manager(false)); - man.run(() => ++amount === CLIENTS && (waitObj.done = true)); - } - wait(waitObj, () => { - amount = 0; - server.on(server.EVENTS.CLOSE, () => ++amount === CLIENTS && (waitObj.done = true)); - for (let i = 0; i < CLIENTS; i++) {mans[i].destroy()} - wait(waitObj, () => { - waitEvent(server, server.EVENTS.DESTROY, () => server.destroy(), () => { - SConfig.maxConnections = maxCons; - Config.worldWidth = width; - Config.worldHeight = height; - done(); + waitEvent(server1, server1.EVENTS.RUN, () => server1.run(), () => { + waitEvent(server2, server2.EVENTS.RUN, () => server2.run(), () => { + man1.run(() => man2.run()); }); }); - }, 31000); + }); + }); - it("Testing run/stop/run manager and one server", (done) => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 35000; - const maxCons = SConfig.maxConnections; - const server = new Server(); - const CLIENTS = 100; - const width = Config.worldWidth; - const height = Config.worldHeight; - let amount = 0; - let waitObj = {done: false}; - let count = 0; - let man1; - let man2; - let oldId; - - Config.worldWidth = 100; - Config.worldHeight = 100; - SConfig.maxConnections = CLIENTS; - man1 = new Manager(false); - delete Config.organisms; - man2 = new Manager(false); - - server.run(); - man1.run(() => ++count === 2 && (waitObj.done = true)); - man2.run(() => ++count === 2 && (waitObj.done = true)); - wait(waitObj, () => { - man1.stop(() => { - expect(man1.clientId).toBe(null); - oldId = man1.clientId; - man1.run(() => { - expect(man1.clientId).not.toBe(null); + + describe('Client/Server tests', () => { + let originalTimeout; + beforeEach(() => { + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; + }); + afterEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout); + + it("Testing hundred managers and one server", (done) => { + const log = console.log; + const maxCons = SConfig.maxConnections; + const server = new Server(); + const mans = []; + const CLIENTS = 100; + const width = Config.worldWidth; + const height = Config.worldHeight; + let amount = 0; + let waitObj = {done: false}; + let man; + + console.log = () => {}; + Config.worldWidth = 100; + Config.worldHeight = 100; + SConfig.maxConnections = CLIENTS; + + waitEvent(server, server.EVENTS.RUN, () => server.run(), () => { + for (let i = 0; i < CLIENTS; i++) { + deletePluginConfigs(); + mans.push(man = new Manager(false)); + man.run(() => ++amount === CLIENTS && (waitObj.done = true)); + } + wait(waitObj, () => { amount = 0; - server.on(server.EVENTS.CLOSE, () => ++amount === 2 && (waitObj.done = true)); - man1.destroy(); - man2.destroy(); + server.on(server.EVENTS.CLOSE, () => ++amount === CLIENTS && (waitObj.done = true)); + for (let i = 0; i < CLIENTS; i++) {mans[i].destroy()} wait(waitObj, () => { waitEvent(server, server.EVENTS.DESTROY, () => server.destroy(), () => { SConfig.maxConnections = maxCons; Config.worldWidth = width; Config.worldHeight = height; + console.log = log; done(); }); }); - }); + }, 61000); }); }); - }); - - it("Tests many connections/disconnections of Manager to the server", (done) => { - jasmine.DEFAULT_TIMEOUT_INTERVAL = 35000; - const maxCons = SConfig.maxConnections; - const server = new Server(); - const CLIENTS = 16; - const width = Config.worldWidth; - const height = Config.worldHeight; - let waitObj = {done: false}; - let amount = 0; - let count = 0; - let man1; - let man2; - const cb = () => { - man1.stop(() => { - expect(man1.clientId).toBe(null); - man1.run(() => { - expect(man1.clientId).not.toBe(null); - if (++amount < 10) { - cb(); - return; - } - man1.destroy(); - man2.destroy(); - amount = 0; - server.on(server.EVENTS.CLOSE, () => ++amount === 2 && (waitObj.done = true)); - wait(waitObj, () => { - waitEvent(server, server.EVENTS.DESTROY, () => server.destroy(), () => { - SConfig.maxConnections = maxCons; - Config.worldWidth = width; - Config.worldHeight = height; - done(); + it("Testing run/stop/run manager and one server", (done) => { + const maxCons = SConfig.maxConnections; + const server = new Server(); + const CLIENTS = 100; + const width = Config.worldWidth; + const height = Config.worldHeight; + let amount = 0; + let waitObj = {done: false}; + let count = 0; + const onDone = () => ++count === 2 && (waitObj.done = true); + let man1; + let man2; + let oldId; + + Config.worldWidth = 100; + Config.worldHeight = 100; + SConfig.maxConnections = CLIENTS; + man1 = new Manager(false); + deletePluginConfigs(); + man2 = new Manager(false); + + testQ(done, + [server, SEVENTS.RUN, () => server.run(), () => {man1.run(onDone); man2.run(onDone)}], + [waitObj], + [man1, EVENTS.STOP, () => man1.stop(), () => {expect(man1.clientId).toBe(null); oldId = man1.clientId}], + [man1, EVENTS.RUN, () => man1.run(), () => {expect(man1.clientId).not.toBe(null); amount = 0; waitObj.done = false}], + [server, SEVENTS.CLOSE, () => man1.destroy(), emp], + [server, SEVENTS.CLOSE, () => man2.destroy(), emp], + [server, SEVENTS.DESTROY, () => server.destroy(), () => { + SConfig.maxConnections = maxCons; + Config.worldWidth = width; + Config.worldHeight = height; + }] + ); + }); + it("Tests many connections/disconnections of Manager to the server", (done) => { + const maxCons = SConfig.maxConnections; + const server = new Server(); + const CLIENTS = 16; + const width = Config.worldWidth; + const height = Config.worldHeight; + let waitObj = {done: false}; + let amount = 0; + let count = 0; + let man1; + let man2; + const cb = () => { + man1.stop(() => { + expect(man1.clientId).toBe(null); + man1.run(() => { + expect(man1.clientId).not.toBe(null); + if (++amount < 10) { + cb(); + return; + } + man1.destroy(); + man2.destroy(); + amount = 0; + server.on(server.EVENTS.CLOSE, () => ++amount === 2 && (waitObj.done = true)); + wait(waitObj, () => { + waitEvent(server, server.EVENTS.DESTROY, () => server.destroy(), () => { + SConfig.maxConnections = maxCons; + Config.worldWidth = width; + Config.worldHeight = height; + done(); + }); }); }); }); + }; + + Config.worldWidth = 10; + Config.worldHeight = 10; + SConfig.maxConnections = CLIENTS; + man1 = new Manager(false); + deletePluginConfigs(); + man2 = new Manager(false); + + waitEvent(server, server.EVENTS.RUN, () => server.run(), () => { + man1.run(() => ++count === 2 && (waitObj.done = true)); + man2.run(() => ++count === 2 && (waitObj.done = true)); + wait(waitObj, cb); }); - }; - - Config.worldWidth = 10; - Config.worldHeight = 10; - SConfig.maxConnections = CLIENTS; - man1 = new Manager(false); - delete Config.organisms; - man2 = new Manager(false); - - server.run(); - man1.run(() => ++count === 2 && (waitObj.done = true)); - man2.run(() => ++count === 2 && (waitObj.done = true)); - wait(waitObj, cb); + }); }); +// it('Tests organism moving back from client of near server', (done) => { +// const ocfg = new ConfigHelper(OConfig); +// const cfg = new ConfigHelper(Config); +// const scfg = new ConfigHelper(SConfig); +// const freePos = World.prototype.getFreePos; +// let iterated1 = 0; +// let iterated2 = 0; +// let org1 = null; +// let destroyFlag = false; +// let stepInFlag = false; +// let stepInBack = false; +// const destroy = () => { +// man1.destroy(() => { +// man2.destroy(() => { +// waitEvent(server1, SEVENTS.DESTROY, () => server1.destroy(), () => { +// waitEvent(server2, SEVENTS.DESTROY, () => server2.destroy(), () => { +// World.prototype.getFreePos = freePos; +// ocfg.reset(); +// scfg.reset(); +// cfg.reset(); +// done(); +// }); +// }); +// }); +// }); +// }; +// +// scfg.set('upHost', SERVER_HOST); +// scfg.set('rightHost', SERVER_HOST); +// scfg.set('downHost', SERVER_HOST); +// scfg.set('leftHost', SERVER_HOST); +// ocfg.set('codeIterationsPerOnce', 1); +// scfg.set('modeDistributed', true); +// scfg.set('maxConnections', 1); +// scfg.set('port', 3000); +// scfg.set('rightPort', 3001); +// const server1 = new Server(); // up server +// scfg.set('port', 3001); +// scfg.set('leftPort', 3000); +// scfg.set('rightPort', 1001); +// const server2 = new Server(); // down server +// cfg.set('worldWidth', 10); +// cfg.set('worldHeight', 10); +// cfg.set('serverPort', 3000); +// cfg.set('serverHost', SERVER_HOST); +// const man1 = new Manager(false); +// deletePluginConfigs(); +// cfg.set('serverPort', 3001); +// const man2 = new Manager(false); +// ocfg.set('orgStartAmount', 1); +// ocfg.set('orgRainMutationPeriod', 0); +// ocfg.set('orgCloneMutationPercent',0); +// ocfg.set('orgEnergySpendPeriod', 0); +// ocfg.set('orgStartEnergy', 10000); +// cfg.set('worldCyclical', false); +// World.prototype.getFreePos = () => {return [0, 1]}; +// +// man1.on(EVENTS.LOOP, () => { +// if (iterated1 > 0 && iterated2 > 0 && org1 === null) { +// expect(man2.organisms.size).toBe(1); +// org1 = man1.organisms.first.val; +// org1.vm.code.push(0b00001011000000000000000000000000); // onStepRight() +// man1.on(EVENTS.KILL, () => destroyFlag = true); +// man1.on(EVENTS.STEP_IN, () => stepInBack = true); +// man2.on(EVENTS.STEP_IN, () => stepInFlag = true); +// } else if (destroyFlag && stepInFlag && stepInBack) { +// destroyFlag = false; +// destroy(); +// } +// if (iterated1 > 10000) {throw 'Error sending organism between Servers'} +// iterated1++; +// }); +// man2.on(EVENTS.LOOP, () => iterated2++); +// +// waitEvent(server1, server1.EVENTS.RUN, () => server1.run(), () => { +// waitEvent(server2, server2.EVENTS.RUN, () => server2.run(), () => { +// man1.run(man2.run); +// }); +// }); +// }); +// +// it('Tests organism moving back from client of near server (with no clients)', (done) => { +// const ocfg = new ConfigHelper(OConfig); +// const cfg = new ConfigHelper(Config); +// const scfg = new ConfigHelper(SConfig); +// const freePos = World.prototype.getFreePos; +// let iterated1 = 0; +// let org1 = null; +// let destroyFlag = false; +// let stepInBack = false; +// const destroy = () => { +// man1.destroy(() => { +// waitEvent(server1, SEVENTS.DESTROY, () => server1.destroy(), () => { +// waitEvent(server2, SEVENTS.DESTROY, () => server2.destroy(), () => { +// World.prototype.getFreePos = freePos; +// ocfg.reset(); +// scfg.reset(); +// cfg.reset(); +// done(); +// }); +// }); +// }); +// }; +// +// scfg.set('upHost', SERVER_HOST); +// scfg.set('rightHost', SERVER_HOST); +// scfg.set('downHost', SERVER_HOST); +// scfg.set('leftHost', SERVER_HOST); +// ocfg.set('codeIterationsPerOnce', 1); +// scfg.set('modeDistributed', true); +// scfg.set('maxConnections', 1); +// scfg.set('port', 3000); +// scfg.set('rightPort', 3001); +// const server1 = new Server(); // up server +// scfg.set('port', 3001); +// scfg.set('leftPort', 3000); +// scfg.set('rightPort', 1001); +// const server2 = new Server(); // down server +// cfg.set('worldWidth', 10); +// cfg.set('worldHeight', 10); +// cfg.set('serverPort', 3000); +// cfg.set('serverHost', SERVER_HOST); +// const man1 = new Manager(false); +// ocfg.set('orgStartAmount', 1); +// ocfg.set('orgMaxOrgs', 1); +// ocfg.set('orgRainMutationPeriod', 0); +// ocfg.set('orgCloneMutationPercent',0); +// ocfg.set('orgEnergySpendPeriod', 0); +// ocfg.set('orgStartEnergy', 10000); +// cfg.set('worldCyclical', false); +// World.prototype.getFreePos = () => {return [5, 1]}; +// +// man1.on(EVENTS.LOOP, () => { +// if (iterated1 > 0 && org1 === null) { +// org1 = man1.organisms.first.val; +// org1.vm.code.push(0b00001011000000000000000000000000); // onStepRight() +// man1.on(EVENTS.KILL, () => destroyFlag = true); +// man1.on(EVENTS.STEP_IN, () => stepInBack = true); +// } else if (destroyFlag && stepInBack) { +// stepInBack = false; +// expect(man1.organisms.size).toBe(1); +// destroy(); +// } +// if (iterated1 > 10000) {throw 'Error sending organism between Servers'} +// iterated1++; +// }); +// +// waitEvent(server1, server1.EVENTS.RUN, () => server1.run(), () => { +// waitEvent(server2, server2.EVENTS.RUN, () => server2.run(), () => man1.run()); +// }); +// }); +// +// it("Checking moving of organism through three Managers", (done) => { +// const ocfg = new ConfigHelper(OConfig); +// const cfg = new ConfigHelper(Config); +// const server = new Server(); +// cfg.set('worldWidth', 10); +// cfg.set('worldHeight', 10); +// const man1 = new Manager(false); +// deletePluginConfigs(); +// const man2 = new Manager(false); +// deletePluginConfigs(); +// const man3 = new Manager(false); +// let freePos = World.prototype.getFreePos; +// let waitObj = {done: false}; +// let i = 0; +// const destroy = () => { +// man1.destroy(() => { +// man2.destroy(() => { +// man3.destroy(() => { +// waitEvent(server, SEVENTS.DESTROY, () => server.destroy(), () => { +// World.prototype.getFreePos = freePos; +// ocfg.reset(); +// cfg.reset(); +// done(); +// }); +// }); +// }); +// }); +// }; +// +// ocfg.set('orgStartAmount', 1); +// ocfg.set('orgRainMutationPeriod', 0); +// ocfg.set('orgCloneMutationPercent', 0); +// ocfg.set('orgEnergySpendPeriod', 0); +// ocfg.set('orgStartEnergy', 10000); +// World.prototype.getFreePos = () => {return [5, ++i === 1 ? 1 : 2]}; +// +// testQ(done, +// [server, SEVENTS.RUN, () => server.run(), () => {man1.run(() => man2.run(() => man3.run(() => waitObj.done = true)))}], +// [waitObj], +// [man1, EVENTS.LOOP, emp, () => man1.organisms.first.val.vm.code.push(0b00001011000000000000000000000000)], // onStepRight() +// [man3, EVENTS.STEP_IN, emp, () => destroy()] +// ); +// }); }); \ No newline at end of file diff --git a/client/src/manager/plugins/Config.js b/client/src/manager/plugins/Config.js index de63ebb..9dafbe4 100644 --- a/client/src/manager/plugins/Config.js +++ b/client/src/manager/plugins/Config.js @@ -3,12 +3,13 @@ * * @author flatline */ -const Api = require('./../../share/Config').api; +const Api = require('./../../share/Config').api; +const Helper = require('./../../../../common/src/Helper'); class Config { constructor(manager) { - manager.api.setConfig = Api.set.bind(Api); - manager.api.getConfig = Api.get.bind(Api); + Helper.setApi(manager.api, 'setConfig', Api.set.bind(Api), 'Sets configuration value by name. Namespaces are also supported. Example: \'setConfig(\'organisms.orgMaxOrgs\', 500)\'. Opposite to getConfig()'); + Helper.setApi(manager.api, 'getConfig', Api.get.bind(Api), 'Returns specified config value. First parameter is a namespace (optional) and config name. For example, to get maximum amount of organisms in current instance/world type: man.api.getConfig(\'organisms.orgMaxOrgs\'). Example of organism related configs you may find here (https://github.com/tmptrash/construct/blob/master/client/src/manager/plugins/status/charts/Config.js). Other configuration parameters are located in files with name Config.js'); } } diff --git a/client/src/manager/plugins/Energy.js b/client/src/manager/plugins/Energy.js index 488313f..938057a 100644 --- a/client/src/manager/plugins/Energy.js +++ b/client/src/manager/plugins/Energy.js @@ -3,10 +3,10 @@ * * @author flatline */ -const Helper = require('./../../../../common/src/Helper'); -const Config = require('./../../share/Config').Config; -const Console = require('./../../share/Console'); -const EVENTS = require('./../../share/Events').EVENTS; +const Helper = require('./../../../../common/src/Helper'); +const Config = require('./../../share/Config').Config; +const Organism = require('./../../manager/plugins/organisms/Organism').Organism; +const EVENTS = require('./../../share/Events').EVENTS; class Energy { constructor(manager) { @@ -23,44 +23,66 @@ class Energy { } _onIteration(counter) { - if (counter % Config.worldEnergyCheckPeriod !== 0 || Config.worldEnergyCheckPeriod === 0) {return} - if (counter === 0) { - this._updateEnergy(Config.worldEnergyDots, Config.worldEnergyInDot); - return; + if (Config.worldEnergyCheckPeriod === 0 || counter % Config.worldEnergyCheckPeriod !== 0) {return} + + let energy = this._getEnergyPercent(); + + this._manager.fire(EVENTS.WORLD_ENERGY, energy); + if (energy > Config.worldEnergyMinPercent) {return} + + const maxEnergy = Config.worldEnergyMaxPercent * Config.worldWidth * Config.worldHeight; + let amount = 0; + let attempts = 0; + while (amount < maxEnergy && attempts < 100) { + const startAmount = amount; + amount = this._addEnergyBlock(amount, maxEnergy); + if (amount === startAmount) { + attempts++; + } else { + attempts = 0; + } } - let energy = 0; - const world = this._manager.world; - const width = Config.worldWidth; + } + + _addEnergyBlock(amount, maxEnergy) { + const width = Config.worldWidth; const height = Config.worldHeight; + const color = Organism.getColor(Config.worldEnergyColorIndex); + let block = Config.worldEnergyBlockSize; + const world = this._manager.world; + let x = Helper.rand(width); + let y = Helper.rand(height); - for (let x = 0; x < width; x++) { - for (let y = 0; y < height; y++) { - if (world.getDot(x, y) > 0) {energy++} + for (let i = 0; i < block; i++) { + x = x + Helper.rand(3) - 1; + y = y + Helper.rand(3) - 1; + if (x < 0 || x >= width || y < 0 || y >= height) { + return amount + } + if (world.isFree(x, y)) { + world.setDot(x, y, color); + if (++amount > maxEnergy) { + return amount + } } } - if (energy * 100 / (width * height) <= Config.worldEnergyCheckPercent) { - this._updateEnergy(Config.worldEnergyDots, Config.worldEnergyInDot); - } + return amount; } - _updateEnergy(dotAmount, energyInDot) { + _getEnergyPercent() { + let energy = 0; const world = this._manager.world; const width = Config.worldWidth; const height = Config.worldHeight; - const rand = Helper.rand; - let x; - let y; - Console.info('Creating random energy'); - for (let dot = 0; dot < dotAmount; dot++) { - x = rand(width); - y = rand(height); - if (world.getDot(x, y) < 1) { - world.setDot(x, y, energyInDot); + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + if (world.getDot(x, y) > 0) {++energy} } } - this._manager.fire(EVENTS.UPDATE_ENERGY); + + return energy / (width * height); } } diff --git a/client/src/manager/plugins/Status.js b/client/src/manager/plugins/Status.js deleted file mode 100644 index 0a2305b..0000000 --- a/client/src/manager/plugins/Status.js +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Shows console status of application - * - * @author flatline - */ -const EVENTS = require('./../../share/Events').EVENTS; -const Config = require('./../../share/Config').Config; - -const GREEN = 'color: #00aa00'; -const RED = 'color: #aa0000'; -// TODO: move this value to Status plugin config -const PERIOD = 10000; - -class Status { - constructor(manager) { - this._manager = manager; - this._stamp = 0; - this._ips = 0; - this._energy = 0; - this._codeSize = 0; - this._runLines = 1; - this._changes = 0; - this._fitness = 0; - this._times = 0; - this._oldValues = [0, 0, 0]; - this._speed = [0, 0, 0]; - - manager.on(EVENTS.IPS, this._onIps.bind(this)); - manager.on(EVENTS.ORGANISM, this._onOrganism.bind(this)); - } - - _onIps(ips, orgs) { - const stamp = Date.now(); - - this._onBeforeIps(ips, orgs); - if (stamp - this._stamp < PERIOD) {return} - - const times = this._times || 1; - const times_1 = (times - 1) || 1; - const realIps = this._ips / times; - const orgAmount = orgs.size || 1; - const sips = `ips:${realIps.toFixed(realIps < 10 ? 2 : 0)}`.padEnd(10); - const slps = this._format(this._runLines / times, 'lps', orgAmount, 0, 14, 1, false, false); - const sorgs = this._format(orgAmount, 'org', orgAmount, 0, 10, 1, false, false); - const senergy = this._format(this._energy / times, 'nrg', orgAmount, 0, 14, 1, false); - const siq = this._format(this._speed[0] / times_1, 'iq', orgAmount, 3, 13, 1000); - const schanges = this._format(this._speed[1] / times_1, 'che', orgAmount, 3, 12, 100000); - const sfit = this._format(this._speed[2] / times_1, 'fit', orgAmount, 3, 14); - const scode = this._format(this._codeSize / times, 'cod', orgAmount, 1, 12, 1, false); - - console.log(`%c${sips}${slps}${sorgs}%c${siq}${senergy}${schanges}${sfit}${scode}`, GREEN, RED); - this._manager.hasView && this._manager.canvas.text(5, 15, sips); - this._onAfterIps(stamp); - } - - _format(value, name, orgs, fixed, pad, coef = 1, lines = true, perOrg = true) { - orgs = perOrg ? orgs : 1; - lines = lines ? this._runLines : 1; - return `${name}:${(((value / orgs) / lines) * coef).toFixed(fixed)}`.padEnd(pad); - } - - _onOrganism(org, lines) { - this._runLines += lines; - } - - _onBeforeIps(ips, orgs) { - let item = orgs.first; - let energy = 0; - let codeSize = 0; - let changes = 0; - let fitness = 0; - let org; - - while(item && (org = item.val)) { - energy += org.energy; - codeSize += org.jsvm.size; - changes += org.changes; - fitness += org.fitness(); - item = item.next; - } - - if (this._oldValues) { - const olds = this._oldValues; - this._speed[0] += energy - olds[0]; - this._speed[1] += changes - olds[1]; - this._speed[2] += fitness - olds[2]; - } - - this._ips += ips; - this._energy += energy; - this._codeSize += codeSize; - this._changes += changes; - this._fitness += fitness; - this._setOldValues(energy, changes, fitness); - - this._times++; - } - - _onAfterIps(stamp) { - this._ips = 0; - this._energy = 0; - this._codeSize = 0; - this._changes = 0; - this._fitness = 0; - this._runLines = 0; - this._times = 0; - this._stamp = stamp; - this._oldValues = null; - this._speed = [0, 0, 0]; - } - - _setOldValues(energy, changes, fitness) { - this._oldValues = [energy, changes, fitness]; - } -} - -module.exports = Status; \ No newline at end of file diff --git a/client/src/manager/plugins/Stones.js b/client/src/manager/plugins/Stones.js new file mode 100644 index 0000000..344b717 --- /dev/null +++ b/client/src/manager/plugins/Stones.js @@ -0,0 +1,74 @@ +/** + * Manager's plugin, which add stones to the world + * + * @author flatline + */ +const Helper = require('./../../../../common/src/Helper'); +const Config = require('./../../share/Config').Config; +const Organism = require('./../../manager/plugins/organisms/Organism').Organism; +const OBJECT_TYPES = require('./../../view/World').OBJECT_TYPES; + +const STONE_BLOCK_SIZE = 300; +// +// We have to add stone type to global types storage +// +OBJECT_TYPES.TYPE_STONE = -(Object.keys(OBJECT_TYPES).length + 1); + +class Stones { + constructor(manager) { + this._manager = manager; + this._onLoopCb = this._onLoop.bind(this); + + Helper.override(manager, 'onLoop', this._onLoopCb); + } + + destroy() { + Helper.unoverride(this._manager, 'onLoop', this._onLoopCb); + this._manager = null; + this._onLoopCb = null; + } + + _onLoop(counter) { + if (counter > 1 || Config.worldStonesPercent === .0) {return} + + const stones = Config.worldStonesPercent * Config.worldWidth * Config.worldHeight; + let amount = 0; + let attempts = 0; + while (amount < stones && attempts < 100) { + const startAmount = amount; + amount = this._addStoneBlock(amount, stones); + if (startAmount === amount) { + attempts++; + } else { + attempts = 0; + } + } + } + + _addStoneBlock(amount, stones) { + const width = Config.worldWidth; + const height = Config.worldHeight; + const color = Organism.getColor(Config.worldStoneColorIndex); + const man = this._manager; + const world = man.world; + const stone = OBJECT_TYPES.TYPE_STONE; + let x = Helper.rand(width); + let y = Helper.rand(height); + + for (let i = 0; i < STONE_BLOCK_SIZE; i++) { + x = x + Helper.rand(3) - 1; + y = y + Helper.rand(3) - 1; + if (x < 0 || x >= width || y < 0 || y >= height) {return amount} + if (world.isFree(x, y)) { + if (world.setDot(x, y, color)) { + man.positions[x][y] = stone; + if (++amount >= stones) {return amount} + } + } + } + + return amount; + } +} + +module.exports = Stones; \ No newline at end of file diff --git a/client/src/manager/plugins/backup/Backup.js b/client/src/manager/plugins/backup/Backup.js index 97d7637..61691f6 100644 --- a/client/src/manager/plugins/backup/Backup.js +++ b/client/src/manager/plugins/backup/Backup.js @@ -61,12 +61,11 @@ class Backup { x : org.x, y : org.y, mutationProbs : org.mutationProbs, - cloneMutationPercent: org.cloneMutationPercent, mutationPeriod : org.mutationPeriod, mutationPercent : org.mutationPercent, color : org.color, - vars : org.jsvm.vars, - code : org.jsvm.cloneByteCode() + vars : org.vm.vars, + code : org.vm.cloneByteCode() }); cur = cur.next; } diff --git a/client/src/manager/plugins/client/Client.js b/client/src/manager/plugins/client/Client.js index 266621c..3ad4baf 100644 --- a/client/src/manager/plugins/client/Client.js +++ b/client/src/manager/plugins/client/Client.js @@ -2,48 +2,35 @@ * Manager's plugin. Implements WebSocket client logic. Work in pair with * server/src/server/Server class. Activates current manager on a server * side and run it. - * TODO: this plugin should listen organisms moves outside of the world - * TODO: and send appropriate requests - * TODO: we have to use events in this class * * @author flatline */ -const Config = require('./../../../share/Config').Config; -const Helper = require('./../../../../../common/src/Helper'); -const TYPES = require('./../../../../../common/src/net/Requests').TYPES; -const Console = require('./../../../share/Console'); -const Connection = require('./../../../../../common/src/net/Connection').Connection; -const EVENTS = require('./../../../../../common/src/net/Connection').EVENTS; -const Plugins = require('./Plugins'); -const GEVENTS = require('./../../../share/Events').EVENTS; -// -// In browser we use browser's native WS implementation. On node.js -// we use implementation of 'ws' library -// -const WS = Config.modeNodeJs ? require('ws') : window.WebSocket; -// TODO: should be moved to local config -const PLUGINS = [ +const Config = require('./../../../share/Config').Config; +const Helper = require('./../../../../../common/src/Helper'); +const TYPES = require('./../../../../../common/src/net/Requests').TYPES; +const Console = require('./../../../share/Console'); +const BaseClient = require('./../../../../../common/src/net/Client').Client; +const EVENTS = require('./../../../../../common/src/net/Client').EVENTS; +const Plugins = require('./Plugins'); +const GEVENTS = require('./../../../share/Events').EVENTS; + +const PLUGINS = [ 'src/plugins/Request', 'src/manager/plugins/client/plugins/Api', - 'src/manager/plugins/client/plugins/Async' + 'src/plugins/AsyncClient' ]; const EVENTS_LEN = Object.keys(EVENTS).length; -const OPEN = EVENTS_LEN; -const GET_ID = EVENTS_LEN + 1; +const GET_ID = EVENTS_LEN; const CLIENT_EVENTS = Object.assign({ - OPEN, GET_ID }, EVENTS); -const CLIENT_EVENTS_LEN = Object.keys(CLIENT_EVENTS).length; -class Client extends Connection { - // TODO: rename to parent +class Client extends BaseClient { constructor(manager) { - super(CLIENT_EVENTS_LEN); + super(Config.serverHost, Config.serverPort, Config.MODE_NODE_JS, CLIENT_EVENTS); this.EVENTS = CLIENT_EVENTS; - // TODO: rename to _parent this._manager = manager; this._onStepOutCb = this._onStepOut.bind(this); this._runCb = this.run.bind(this); @@ -60,37 +47,26 @@ class Client extends Connection { run() { if (this.active) {return} - this._client = this._createWebSocket(); - this._client.onerror = this.onError.bind(this); - this._client.onclose = this.onClose.bind(this); - this._client.onopen = this.onOpen.bind(this); + super.run(); this._manager.on(GEVENTS.STEP_OUT, this._onStepOutCb); } stop() { - this.active && this._client.close(); + super.stop(); this._manager.off(GEVENTS.STEP_OUT, this._onStepOutCb); } - // TODO: rename to parent() get manager() {return this._manager} - get socket() {return this._client} destroy() { - this.stop(); - if (this._client) { - this._client.onclose = null; - this._client.onmessage = null; - this._client.onerror = null; - } + super.destroy(); + Helper.unoverride(this._manager, 'run', this._runCb); Helper.unoverride(this._manager, 'stop', this._stopCb); - this._runCb = null; - this._stopCb = null; - this._manager = null; - this._plugins = null; - - super.destroy(); + this._runCb = null; + this._stopCb = null; + this._manager = null; + this._plugins = null; } /** @@ -101,9 +77,9 @@ class Client extends Connection { */ onClose(event) { super.onClose(event); - this.active = false; - this._manager.clientId = null; Console.warn(`Client "${this._manager.clientId}" has disconnected by reason: ${this.closeReason}`); + this._manager.clientId = null; + this._manager.resetActive(); } /** @@ -112,13 +88,12 @@ class Client extends Connection { * @override */ onOpen(event) { - this.active = true; - this._client.onmessage = this.onMessage.bind(this, this._client); + super.onOpen(event); // // First we send request to get unique clientId from server. It // also means that this client is active and ready to run // - this.request(this._client, TYPES.REQ_GET_ID, (type, clientId) => { + this.request(this.socket, TYPES.REQ_GET_ID, (type, clientId) => { if (type !== TYPES.RES_GET_ID_OK) { Console.error(`Unable to get unique client id from server. Response type: ${type}`); return; @@ -127,24 +102,12 @@ class Client extends Connection { this.fire(GET_ID, clientId); Console.info(`Client id "${clientId}" obtained from the server`); }); - this.fire(OPEN, event); Console.info('Connection with Server has opened'); } - _createWebSocket() { - let ws = null; - try { - ws = new WS(`${Config.serverHost}:${Config.serverPort}`); - } catch (e) { - Console.error(e.message); - } - - return ws; - } - _onStepOut(x, y, dir, org) { - this.request(this._client, TYPES.REQ_MOVE_ORG, this._manager.clientId, x, y, dir, org.serialize()); + this.request(this.socket, TYPES.REQ_MOVE_ORG, this._manager.clientId, x, y, dir, org.serialize()); } } -module.exports = {Client, EVENTS: CLIENT_EVENTS}; +module.exports = {Client, EVENTS: CLIENT_EVENTS}; \ No newline at end of file diff --git a/client/src/manager/plugins/client/ClientSpec.js b/client/src/manager/plugins/client/ClientSpec.js index cc889ec..1ee6171 100644 --- a/client/src/manager/plugins/client/ClientSpec.js +++ b/client/src/manager/plugins/client/ClientSpec.js @@ -9,12 +9,11 @@ describe("client/src/manager/plugins/Client", () => { let api = require('./../../../share/Config').api; let Console = require('./../../../share/Console'); let SConsole = require('./../../../../../server/src/share/Console'); - const Api = require('./../../../../../server/src/server/plugins/Api'); - const Request = require('./../../../../../common/src/plugins/Request'); const waitEvent = THelper.waitEvent; const wait = THelper.wait; const host = Config.serverHost; - let isNodeJs; + const port = SConfig.port; + const maxConns = SConfig.maxConnections; let Client; let CEVENTS; let Server; @@ -25,14 +24,14 @@ describe("client/src/manager/plugins/Client", () => { let serror; let swarn; let sinfo; + let dist; beforeAll(() => { - // - // These two lines set modeNodeJs mode to Node.js as running environment - // - isNodeJs = Config.modeNodeJs; - Config.modeNodeJs = true; Config.serverHost = 'ws://127.0.0.1'; + dist = SConfig.modeDistributed; + SConfig.modeDistributed = false; + SConfig.port = Config.serverPort; + SConfig.maxConnections = 100; Client = require('./Client').Client; CEVENTS = require('./Client').EVENTS; Server = require('./../../../../../server/src/server/Server').Server; @@ -54,7 +53,6 @@ describe("client/src/manager/plugins/Client", () => { SConsole.info = () => {}; }); afterAll(() => { - api.set('modeNodeJs', isNodeJs); SConsole.error = serror; SConsole.warn = swarn; SConsole.info = sinfo; @@ -64,6 +62,9 @@ describe("client/src/manager/plugins/Client", () => { Console.info = info; Config.serverHost = host; + SConfig.modeDistributed = dist; + SConfig.port = port; + SConfig.maxConnections = maxConns; }); it("Checking client creation without server", (done) => { @@ -77,6 +78,7 @@ describe("client/src/manager/plugins/Client", () => { stop() {} get clientId() {return this._clientId} set clientId(id) {this._clientId = id} + resetActive() {} } const man = new Man0(); const client = new Client(man); @@ -97,6 +99,7 @@ describe("client/src/manager/plugins/Client", () => { stop() {} get clientId() {return this._clientId} set clientId(id) {this._clientId = id} + resetActive() {} } const man = new Man(); const server = new Server(SConfig.port); @@ -129,6 +132,7 @@ describe("client/src/manager/plugins/Client", () => { stop() {} get clientId() {return this._clientId} set clientId(id) {this._clientId = id; id && ++count === 2 && (waitObj.done = true)} + resetActive() {} } const man1 = new Man1(); const man2 = new Man1(); diff --git a/client/src/manager/plugins/client/Config.js b/client/src/manager/plugins/client/Config.js deleted file mode 100644 index 30fd847..0000000 --- a/client/src/manager/plugins/client/Config.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Configuration of Client plugin - * - * @author flatline - */ -const Config = { -}; - -module.exports = Config; \ No newline at end of file diff --git a/client/src/manager/plugins/client/plugins/Api.js b/client/src/manager/plugins/client/plugins/Api.js index 919d289..2628eef 100644 --- a/client/src/manager/plugins/client/plugins/Api.js +++ b/client/src/manager/plugins/client/plugins/Api.js @@ -7,14 +7,13 @@ const TYPES = require('./../../../../../../common/src/net/Requests').TYPES; const BaseApi = require('./../../../../../../common/src/net/Api'); const EVENTS = require('./../../../../share/Events').EVENTS; -const Console = require('./../../../../share/Console'); class Api extends BaseApi { constructor(client) { super(client); this.api[TYPES.REQ_MOVE_ORG] = this._stepIn.bind(this); - this.api[TYPES.RES_MOVE_ERR] = this._stepIn.bind(this); + this.api[TYPES.REQ_MOVE_ORG_BACK] = this._stepInBack.bind(this); this.api[TYPES.REQ_SET_NEAR_ACTIVE] = this._setActive.bind(this); } @@ -26,16 +25,32 @@ class Api extends BaseApi { /** * Is called if organism is move in from other Manager (world) * @param {String} reqId Unique request id + * @param {String} clientId Unique client id within current server * @param {Number} x Current org X position * @param {Number} y Current org Y position * @param {Number} dir Direction of moving * @param {String} orgJson Organism's serialized json - * @param {String|null} errMsg Error message * @api */ - _stepIn(reqId, x, y, dir, orgJson, errMsg = null) { - this.parent.manager.fire(EVENTS.STEP_IN, x, y, orgJson); - errMsg && Console.warn(errMsg); + _stepIn(reqId, clientId, x, y, dir, orgJson) { + const ret = {ret: true}; + this.parent.manager.fire(EVENTS.STEP_IN, x, y, orgJson, ret); + this.parent.response(this.sock, ret.ret ? TYPES.RES_MOVE_OK : TYPES.RES_MOVE_ERR, reqId, clientId); + } + + /** + * Is called if organism is moved back from other Manager (world) + * @param {String} reqId Unique request id + * @param {String} clientId Unique client id within current server + * @param {Number} x Current org X position + * @param {Number} y Current org Y position + * @param {Number} dir Direction of moving + * @param {String} orgJson Organism's serialized json + * @api + */ + _stepInBack(reqId, clientId, x, y, dir, orgJson) { + const ret = {ret: true}; + this.parent.manager.fire(EVENTS.STEP_IN, x, y, orgJson, ret); } /** @@ -50,10 +65,6 @@ class Api extends BaseApi { _setActive(reqId, dir, active) { this.parent.manager.activeAround[dir] = active; } - - _request(type, ...params) { - return this.parent.request(this.parent.socket, type, this.parent.manager.clientId, ...params); - } } module.exports = Api; \ No newline at end of file diff --git a/client/src/manager/plugins/ips/Config.js b/client/src/manager/plugins/ips/Config.js index a9e6a20..a57def9 100644 --- a/client/src/manager/plugins/ips/Config.js +++ b/client/src/manager/plugins/ips/Config.js @@ -5,11 +5,11 @@ */ const Config = { /** - * {Boolean} Shows/Hides IPS value on the left top corner of a canvas + * {Boolean} Shows/Hides title values on the left top corner of a canvas */ show : true, /** - * {Number} Period in milliseconds, which is used to update IPS + * {Number} Period in milliseconds, which is used to update titles */ periodMs: 1000 }; diff --git a/client/src/manager/plugins/ips/Ips.js b/client/src/manager/plugins/ips/Ips.js index ad2d94b..f183eff 100644 --- a/client/src/manager/plugins/ips/Ips.js +++ b/client/src/manager/plugins/ips/Ips.js @@ -6,43 +6,48 @@ * {Boolean} show Shows/Hides IPS value at the top-left corner * {Number} periodMs Period of milliseconds, which is user for checking * IPS value. It's possible to increase it to reduce amount of - * requests and additional jsvm in main loop + * requests and additional vm in main loop * * @author flatline */ const Configurable = require('./../../../../../common/src/Configurable'); const Helper = require('./../../../../../common/src/Helper'); const Config = require('./../../../share/Config').Config; -const EVENTS = require('./../../../share/Events').EVENTS; const IpsConfig = require('./Config'); +const EVENTS = require('./../../../share/Events').EVENTS; class Ips extends Configurable { constructor(manager) { super(manager, {Config, cfg: IpsConfig}, {show: ['_show', 'Shows IPS of the world']}); this._stamp = Date.now(); + this._ips = 0; + this._onLoopCb = this._onLoop.bind(this); this._onIterationCb = this._onIteration.bind(this); + Helper.override(manager, 'onLoop', this._onLoopCb); Helper.override(manager, 'onIteration', this._onIterationCb); } destroy() { + Helper.unoverride(this.parent, 'onLoop', this._onLoopCb); Helper.unoverride(this.parent, 'onIteration', this._onIterationCb); + this._onLoopCb = null; this._onIterationCb = null; super.destroy(); } - _onIteration(counter, stamp) { + _onLoop(counter, stamp) { if (!this.cfg.show) {return} - const ts = stamp - this._stamp; + const ts = stamp - this._stamp; if (ts < this.cfg.periodMs) {return} - const man = this.parent; - let ips = man.codeRuns / (man.organisms.size || 1) / (ts / 1000); - man.fire(EVENTS.IPS, ips, man.organisms); - man.codeRuns = 0; - this._stamp = stamp; + this.parent.fire(EVENTS.IPS, this._ips / (ts / 1000)); + this._ips = 0; + this._stamp = stamp; } + _onIteration() {this._ips++} + _show(show = true) { this.cfg.show = show; } diff --git a/client/src/manager/plugins/organisms/Config.js b/client/src/manager/plugins/organisms/Config.js index 7b5fb92..931954f 100644 --- a/client/src/manager/plugins/organisms/Config.js +++ b/client/src/manager/plugins/organisms/Config.js @@ -5,154 +5,159 @@ */ const Config = { /** - * {Number} Maximum value of mutation period in iterations. It's used - * in mutations of org.mutationPeriod parameter - */ - ORG_MAX_MUTATION_PERIOD: 10000, - /** - * {Number} Index of first color of organism. After maximum color this - * color will be applied + * {Number} Max value, which we may use in orgMutationProbs array. We may use + * range: [0...ORG_MUTATION_PROBS_MAX_VAL] including these values */ - ORG_FIRST_COLOR: 1, + ORG_MUTATION_PROBS_MAX_VAL: 100, /** - * {Number} Maximum color index of organism + * {Array} Array of operators weights in energy equivalent. Every value of this + * array is bind to special operator run on VM. The same sequence should be + * implemented. See Operators.operators getter for details. Values may be float. */ - ORG_MAX_COLOR: Number.MAX_SAFE_INTEGER, + orgOperatorWeights: [ + .0001, .0001, .00001, .00000001, .0001, // var, const, if, loop, operator, + .00001, 2, 2, 2, 2, // lookAt, eatLeft, eatRight, eatUp, eatDown, + 2, 2, 2, 2, // stepLeft, stepRight, stepUp, stepDown, + .001, .001, // fromMem, toMem, + .0001, .0001, // myX, myY, + .001, .001, .001, .001 // checkLeft, checkRight, checkUp, checkDown + ], /** * {Array} Probabilities which used, when mutator decides what to do: - * add, change, delete code line inside the jsvm; change amount of + * add, change, delete code line inside the vm; change amount of * mutations or change mutations period... Depending on these * values, organism may have different strategies of living. * For example: if add value is bigger then del and change, - * then jsvm size will be grow up all the time. If del value is + * then vm size will be grow up all the time. If del value is * bigger then other, then it will be decreased to zero lines - * of jsvm and organism will die. + * of vm and organism will die. * Format: [ - * add - Probability of adding new line to the code - * change - Probability of changing existing line of code - * delete - Probability of deleting a line of code - * small-change - Probability of "small change" - change part of one code line - * clone - Probability for sharing of energy percent on clone - * copy - Probability of copying of code lines inside it's own code - * period - Probability of period of organism mutations - * amount - Probability of amount of mutations per period - * probs - Probability of change one of probability coefficient in this array - * clonePeriod - Probability of change clone energy percent value + * change - Probability of changing existing line of code + * delete - Probability of deleting a line of code + * small-change - Probability of "small change" - change part of one code line + * period - Probability of period of organism mutations + * amount - Probability of amount of mutations per period + * probs - Probability of change one of probability coefficient in this array + * add - Probability of adding new line to the code + * copy - Probability of copying of code lines inside it's own code * ] */ - orgMutationProbs: [50,80,10,100,1,10,10,10,10,10], + orgMutationProbs: [5,1,90,1,1,1,20,1], /** - * {Number} Max value, which we may use in orgMutationProbs array. We may use - * range: [0...orgMutationProbsMaxValue] including these values + * {Boolean} If turned on, then organism will be responsible for changing + * mutations probabilities. Otherwise these probabilities will be constant */ - orgMutationProbsMaxValue: 100, + orgMutationProbsPerOrg: true, /** - * {Number} Percent of mutations from jsvm size, which will be applied to - * organism after cloning. Should be <= 1.0 (1.0 === 100%) + * {Number} Minimum age for cloning. Before that, cloning is impossible. It should + * be less then orgAlivePeriod config */ - orgCloneMutationPercent: 0.01, + orgCloneMinAge: 3000, /** - * {Number} Amount of iterations between cloning. Set it to 0 to turn it off + * {Number} Minimum energy for cloning */ - orgClonePeriod: 100, + orgCloneMinEnergy: 20000000, /** - * {Number} Amount of iterations, after which crossover will be applied - * to random organisms. May be set to 0 to turn crossover off + * {Boolean} If true, then random organism will be killed after new one has + * cloned and amount of organisms is greater then orgMaxOrgs config. false + * mean, that new organism will not be cloned, if amount of organisms is >= + * orgMaxOrgs config. */ - orgCrossoverPeriod: 0, + orgKillOnClone: true, + /** + * {Number} Amount of iterations between tournament. During tournament one + * organism (looser) will be killed + */ + orgTournamentPeriod: 0, /** * {Number} Amount of iterations within organism's life loop, after that we * do mutations according to orgRainMutationPercent config. If 0, then - * mutations will be disabled. Should be less then ORGANISM_MAX_MUTATION_PERIOD + * mutations wilsabled. Should be less then ORGANISM_MAX_MUTATION_PERIOD */ - orgRainMutationPeriod: 10000, + orgRainMutationPeriod: 100, /** * {Number} Percent of mutations from code size. 0 is a possible value if * we want to disable mutations. Should be less then 1.0 (1.0 === 100%) */ - orgRainMutationPercent: 0.02, + orgRainMutationPercent: 0.01, /** - * {Number} Amount of organisms we have to create on program start + * {Boolean} Turn this flag on to give organism a possibility to choose his + * own mutations period and percent. false - mean, that these values will be + * constant for all organisms */ - orgStartAmount: 100, + orgRainPerOrg: true, /** - * {Number} Amount of energy for first organisms. They are like Adam and - * Eve. It means that these empty (without jsvm) organism were created - * by operator and not by evolution. - */ - orgStartEnergy: 10000, - /** - * {Number} Begin color of "empty" organism (organism without code). Color - * should be set in HEX-RGB mode. Example: 0xRRGGBB + * {Number} Amount of iterations, after which crossover will be applied + * to random organisms. May be set to 0 to turn crossover off */ - orgStartColor: 0xFF0000, + orgCrossoverPeriod: 4000, /** - * {Number} Amount of iterations within organism's life loop, after that we decrease - * some amount of energy. If 0, then energy decreasing will be disabled. + * {Number} Period of iterations for creation of random organisms. Set it to 0 + * to turn off this feature */ - orgEnergySpendPeriod: 50, + orgRandomOrgPeriod: 5000, /** * {Number} Amount of iterations when organism is alive. It will die after * this period. If 0, then will not be used and organism may leave forever */ orgAlivePeriod: 0, /** - * {Number} This value means the period between organism codeSizes, which - * affects energy grabbing by the system. For example: we have two - * organisms: org1.energy = 10, org2.energy = 10, org1.codeSize = 6, - * org2.codeSize = 9, OConfig.orgGarbagePeriod = 5. It means that - * during energy grabbing by the system org1 and org2 will spend the - * same amount of energy - 1 unit. This is because the period goes - * from 1..5, 6..10,... and both organisms are in the same period. In - * simple words, if your size is between 0-20, then 1 unit of energy will - * be grabbed from you. If your size is between 21-40, then 2 units of - * energy will be grabbed from you and so on... - */ - orgGarbagePeriod: 20, - /** - * {Number} Size of organism stack (internal memory) - */ - orgMemSize: 64, - /** - * {Number} Percent of energy, which will be given to the child + * {Number} Size of organism stack (internal memory) in bits. Real amount of + * organism's internal memory will be 2^orgMemBits. Example: if orgMemBits=3, + * then memory=2^3 === 8 numbers */ - orgCloneEnergyPercent: 0.5, + orgMemBits: 8, /** * {Number} Percent of energy, which will be minused from organism after * stepping from one instance to another. */ - orgStepEnergySpendPercent: 0.0, + orgStepEnergySpendPercent: .0, + /** + * {Number} Percent from orgMaxOrgs config, which is used for crossing borders + * between clients/Managers. Entire amount of organisms within one client = + * orgMaxOrgs + orgMaxOrgs * orgStepOverflowPercent. Without this config organisms + * try to rich the limit of organisms on one client and it's impossible to cross + * the border (every organism will be returned back, but there will not be free + * space at that time). In this situation crossing organism just die during crossing. + */ + orgStepOverflowPercent: 0.2, /** * {Number} Maximum amount of organisms in a world. If some organism will * try to clone itself, when entire amount of organisms are equal * this value, the cloning will not happen. */ - orgMaxOrgs: 300, + orgMaxOrgs: 1000, /** - * {Number} If organism reach this limit of amount of jsvm lines, then codeSizeCoef - * will be used during it's energy grabbing by system. We use this approach, - * because our CPU's are slow and organisms with big codes are very slow. But - * it's possible for organisms to go outside the limit by inventing new - * effective mechanisms of energy obtaining. + * {Number} Amount of organisms we have to create on program start */ - codeMaxSize: 150, + orgStartAmount: 1000, /** - * {Number} Amount of bits per one variable. It affects maximum value, - * which this variable may contain. This value shouldn't be less then 2. + * {Number} Amount of energy for first organisms. They are like Adam and + * Eve. It means that these empty (without vm) organism were created + * by operator and not by evolution. + */ + orgStartEnergy: 90000000, + /** + * {Number} Amount of bits for storing a numeric constant inside byte code */ - codeBitsPerVar: 2, + codeConstBits: 16, /** * {Number} The value from -X/2 to X/2, which is used for setting * default value, while organism is delivering. So, if the value is - * 1000, then ragne will be: -500..500 + * 1000, then range will be: -500..500 */ - codeVarInitRange: 1000, + codeVarInitRange: 500, /** * {Number} This value is amount of code lines, which will be run for one - * organism without interruption by one JSVM. Set this value to value bigger + * organism without interruption by one VM. Set this value to value bigger * then codeMaxSize, then entire code of organism will be run */ - codeYieldPeriod: 10, + codeYieldPeriod: 5, + /** + * {Number} Amount of bits per one variable. It affects maximum value, + * which this variable may contain. This value shouldn't be less then 2. + */ + codeBitsPerVar: 4, /** * {Number} Amount of bits for storing operator. This is first XX bits * in a number. @@ -162,12 +167,20 @@ const Config = { * {Number} Amount of bits, which stores maximum block length. Under block * length we mean maximum amount of lines in one block like if, for,... */ - codeBitsPerBlock: 4, + codeBitsPerBlock: 10, /** * {Number} Amount of iterations between calls to V8 event loop. See * Manager._initLoop(), Manager.run() methods for details. */ - codeIterationsPerOnce: 50, + codeIterationsPerOnce: 100, + /** + * {Number} If organism reach this limit of amount of vm lines, then codeSizeCoef + * will be used during it's energy grabbing by system. We use this approach, + * because our CPU's are slow and organisms with big codes are very slow. But + * it's possible for organisms to go outside the limit by inventing new + * effective mechanisms of energy obtaining. + */ + codeMaxSize: 300 }; module.exports = Config; \ No newline at end of file diff --git a/client/src/manager/plugins/organisms/Mutator.js b/client/src/manager/plugins/organisms/Mutator.js index c447f37..e7152a5 100644 --- a/client/src/manager/plugins/organisms/Mutator.js +++ b/client/src/manager/plugins/organisms/Mutator.js @@ -10,129 +10,135 @@ * @author flatline */ const EVENTS = require('./../../../share/Events').EVENTS; -const Config = require('./../../../share/Config').Config; const OConfig = require('./../../../manager/plugins/organisms/Config'); const Helper = require('./../../../../../common/src/Helper'); -const Organism = require('./dos/Organism'); -const Num = require('./../../../jsvm/Num'); +const Num = require('./../../../vm/Num'); -const VAR_BITS_OFFS = Num.VAR_BITS_OFFS - 1; -const VARS = Num.VARS; -const MAX_VAR = Num.MAX_VAR; +const ADD_MUTAION_INDEX = 6; +/** + * {Number} Default alive period, which is used if orgAlivePeriod + * config is set to zero + */ +const MAX_ALIVE_PERIOD = 10000; class Mutator { - constructor(manager) { - this._manager = manager; - this._MUTATION_TYPES = [ - this._onAdd, - this._onChange, - this._onDel, - this._onSmallChange, - this._onClone, - this._onCopy, - this._onPeriod, - this._onAmount, - this._onProbs, - this._onCloneEnergyPercent - ]; - - manager.on(EVENTS.ORGANISM, this._onOrganism.bind(this)); - manager.on(EVENTS.CLONE, this._onCloneOrg.bind(this)); + static _onChange(org) { + const vm = org.vm; + vm.updateLine(Helper.rand(vm.size), Num.rand()); + org.changes += Num.MAX_BITS; } - destroy() { - this._manager = null; - this._MUTATION_TYPES = null; + static _onDel(org) { + org.vm.removeLine(); + org.changes += Num.MAX_BITS; } - _onOrganism(org) { - if (org.iterations % org.mutationPeriod === 0 && OConfig.orgRainMutationPeriod > 0 && org.mutationPeriod > 0 && org.alive) { - this._mutate(org, false); + /** + * Operator type or one variable may mutate + * @param {Organism} org + */ + static _onSmallChange(org) { + const rand = Helper.rand; + const vm = org.vm; + const index = rand(vm.size); + const rnd = rand(2); + // + // Toggles operator bits only + // + if (rnd === 0) { + vm.updateLine(index, Num.setOperator(vm.getLine(index), rand(vm.operators.operators.length))); + org.changes += Num.BITS_PER_OPERATOR; + // + // Toggles specified bit, except operator bits + // + } else { + vm.updateLine(index, vm.getLine(index) ^ (1 << rand(Num.VAR_BITS_OFFS - 1))); + org.changes++; } } - _onCloneOrg(parent, child) { - if (child.energy > 0 && OConfig.orgCloneMutationPercent > 0) {this._mutate(child)} + static _onMutationPeriod(org) { + if (!OConfig.orgRainPerOrg) {return} + org.mutationPeriod = OConfig.orgAlivePeriod < 1 ? Helper.rand(MAX_ALIVE_PERIOD) + 1 : Helper.rand(OConfig.orgAlivePeriod - 1) + 1; + org.changes++; } - _mutate(org, clone = true) { - const jsvm = org.jsvm; - const probIndex = Helper.probIndex; - const mTypes = this._MUTATION_TYPES; - const maxSize = OConfig.codeMaxSize; - let mutations = Math.round(jsvm.size * (clone ? org.cloneMutationPercent : org.mutationPercent)) || 1; - let type; - - for (let i = 0; i < mutations; i++) { - if (jsvm.size > maxSize) { - mutations = i; - break; - } - type = jsvm.size < 1 ? 0 : probIndex(org.mutationProbs); - mTypes[type](org); - } - org.changes += mutations; - this._manager.fire(EVENTS.MUTATIONS, org, mutations, clone); - - return mutations; + static _onMutationPercent(org) { + if (!OConfig.orgRainPerOrg) {return} + org.mutationPercent = Math.random(); + org.changes++; } - _onAdd(org) { - org.jsvm.size <= OConfig.codeMaxSize && org.jsvm.insertLine(); + static _onProbs(org) { + if (!OConfig.orgMutationProbsPerOrg) {return} + org.mutationProbs[Helper.rand(org.mutationProbs.length)] = Helper.rand(OConfig.ORG_MUTATION_PROBS_MAX_VAL) || 1; + org.changes++; } - _onChange(org) { - const jsvm = org.jsvm; - jsvm.updateLine(Helper.rand(jsvm.size), Num.get()); + static _onAdd(org) { + org.vm.insertLine(); + org.changes += Num.MAX_BITS; } - _onDel(org) { - org.jsvm.removeLine(); + static _onCopy(org) { + org.changes += (org.vm.copyLines() * Num.MAX_BITS); } - /** - * Operator type or one variable may mutate - * @param {Organism} org - * @private - */ - _onSmallChange(org) { - const rand = Helper.rand; - const jsvm = org.jsvm; - const index = rand(jsvm.size); - const rnd = rand(3); - - if (rnd === 0) { - jsvm.updateLine(index, Num.setOperator(jsvm.getLine(index), rand(jsvm.operators.operators.length))); - } else if (rnd === 1) { - jsvm.updateLine(index, Num.setVar(jsvm.getLine(index), rand(VARS), rand(MAX_VAR))); - } else { - // toggle specified bit - jsvm.updateLine(index, jsvm.getLine(index) ^ (1 << rand(VAR_BITS_OFFS))); - } - } + constructor(manager, owner) { + this._manager = manager; + this._owner = owner; + this._MUTATION_TYPES = [ + Mutator._onChange, + Mutator._onDel, + Mutator._onSmallChange, + Mutator._onMutationPeriod, + Mutator._onMutationPercent, + Mutator._onProbs, + Mutator._onAdd, + Mutator._onCopy + ]; - _onClone(org) { - org.cloneMutationPercent = Math.random(); - } + this._onOrganismCb = this._onOrganism.bind(this); - _onCopy(org) { - org.jsvm.copyLines(); + Helper.override(owner, 'onOrganism', this._onOrganismCb); } - _onPeriod(org) { - org.mutationPeriod = Helper.rand(OConfig.ORG_MAX_MUTATION_PERIOD); + destroy() { + Helper.unoverride(this._owner, 'onOrganism', this._onOrganismCb); + this._onOrganismCb = null; + this._manager = null; + this._owner = null; + this._MUTATION_TYPES = null; } - _onAmount(org) { - org.mutationPercent = Math.random(); + _onOrganism(org) { + if (org.iterations % org.mutationPeriod !== 0 || org.iterations < 1 || OConfig.orgRainMutationPeriod === 0 || OConfig.orgRainMutationPercent === 0.0 || org.mutationPeriod === 0 || org.energy < 1) {return} + this._mutate(org); } - _onProbs(org) { - org.mutationProbs[Helper.rand(org.mutationProbs.length)] = Helper.rand(OConfig.orgMutationProbsMaxValue) || 1; - } + /** + * IMPORTANT: mutations should be applied only after last line of organism's code + * has interpreted + * @param {Organism} org Current organism + */ + _mutate(org) { + const vm = org.vm; + const probIndex = Helper.probIndex; + const mTypes = this._MUTATION_TYPES; + const maxSize = OConfig.codeMaxSize; + let mutations = ((vm.size * org.mutationPercent + .5) << 0) || 1; + let type; - _onCloneEnergyPercent(org) { - org.cloneEnergyPercent = Math.random(); + for (let i = 0; i < mutations; i++) { + // + // If we reach code size maximum, then only change and delete + // mutations may be applied to organism's code + // + type = vm.size < 1 ? ADD_MUTAION_INDEX : probIndex(org.mutationProbs); + if (vm.size >= maxSize && type >= ADD_MUTAION_INDEX) {mutations = i; break} + mTypes[type](org); + } + this._manager.fire(EVENTS.MUTATIONS, org, mutations); } } diff --git a/client/src/manager/plugins/organisms/Organism.js b/client/src/manager/plugins/organisms/Organism.js index 590f828..c1a7ff2 100644 --- a/client/src/manager/plugins/organisms/Organism.js +++ b/client/src/manager/plugins/organisms/Organism.js @@ -5,17 +5,48 @@ * TODO: - * @author flatline */ -const Observer = require('./../../../../../common/src/Observer'); -const Helper = require('./../../../../../common/src/Helper'); -const Config = require('./../../../share/Config').Config; -const OConfig = require('./../../../manager/plugins/organisms/Config'); -const EVENTS = require('./../../../share/Events').EVENTS; -const EVENT_AMOUNT = require('./../../../share/Events').EVENT_AMOUNT; -const JSVM = require('./../../../jsvm/JSVM'); +const _fill = require('lodash/fill'); +const Observer = require('./../../../../../common/src/Observer'); +const Helper = require('./../../../../../common/src/Helper'); +const OConfig = require('./../../../manager/plugins/organisms/Config'); +const EVENT_AMOUNT = require('./../../../share/Events').EVENT_AMOUNT; +const VM = require('./../../../vm/VM'); -const IS_NUM = Helper.isNumeric; +const DESTROY = 0; +const CLONE = 1; +const KILL_NO_ENERGY = 2; +const KILL_AGE = 3; +const ITERATION = 4; +const ORG_EVENTS = { + DESTROY, + CLONE, + KILL_NO_ENERGY, + KILL_AGE, + ITERATION +}; + +const MAX_COLORS = 4000; +const UPDATE_COLOR_PERIOD = 50; class Organism extends Observer { + /** + * Returns color by index. Index may be increased without limit + * @param {Number} index Color index. Starts from 0 till Number.MAX_VALUE + * @returns {Number} RGB value + */ + static getColor(index) { + // + // Maximum possible colors for this value is MAX_COLORS + // + const frequency = 0.0005; + + const r = Math.sin(frequency * index ) * 127 + 128; + const g = Math.sin(frequency * index + 2) * 127 + 128; + const b = Math.sin(frequency * index + 4) * 127 + 128; + + return r << 16 | g << 8 | b; + } + /** * Is called before every run. Should return true, if everything * is okay and we don't need to interrupt running. If true, then @@ -26,6 +57,7 @@ class Organism extends Observer { /** * Is called as a running body (main) method + * @return {Number} Amount of run lines * @abstract */ onRun() {} @@ -36,71 +68,70 @@ class Organism extends Observer { * @param {String} id Unique identifier of organism * @param {Number} x Unique X coordinate * @param {Number} y Unique Y coordinate - * @param {Boolean} alive true if organism is alive * @param {Object} item Reference to the Queue item, where * this organism is located - * @param {Function} codeEndCb Callback, which is called at the - * end of every code iteration. * @param {Function} operatorCls Class of operators * @param {Organism} parent Parent organism if cloning is needed */ - constructor(id, x, y, alive, item, codeEndCb, operatorCls, parent = null) { + constructor(id, x, y, item, operatorCls, parent = null) { super(EVENT_AMOUNT); - this._codeEndCb = codeEndCb; this._operatorCls = operatorCls; if (parent === null) {this._create()} else {this._clone(parent)} - - this._id = id; - this._x = x; - this._y = y; - this._changes = 1; - this._alive = alive; - this._item = item; - this._iterations = 0; - this._fnId = 0; + this._id = id; + this._x = x; + this._y = y; + this._iterations = -1; + this._changes = 0; + this._item = item; + this._energyChanges = 0; + this._fnId = 0; } get id() {return this._id} get x() {return this._x} get y() {return this._y} - get alive() {return this._alive} get item() {return this._item} get iterations() {return this._iterations} get changes() {return this._changes} get mutationProbs() {return this._mutationProbs} get mutationPeriod() {return this._mutationPeriod} get mutationPercent() {return this._mutationPercent} - get cloneMutationPercent() {return this._cloneMutationPercent} get energy() {return this._energy} + get startEnergy() {return this._startEnergy} get color() {return this._color} get mem() {return this._mem} - get cloneEnergyPercent() {return this._cloneEnergyPercent} get posId() {return Helper.posId(this._x, this._y)} set x(newX) {this._x = newX} set y(newY) {this._y = newY} - set cloneMutationPercent(m) {this._cloneMutationPercent = m} set mutationPeriod(m) {this._mutationPeriod = m} set mutationPercent(p) {this._mutationPercent = p} - set cloneEnergyPercent(p) {this._cloneEnergyPercent = p} - set energy(e) {this._energy = e} - set changes(c) { - this._changes = c; - this._updateColor(c); - } + set energy(e) {if (this.vm !== null) { this._energy = e; ++this._energyChanges % UPDATE_COLOR_PERIOD === 0 && this._updateColor()}} + set startEnergy(e) {this._startEnergy = e} + set changes(c) {this._changes = c} /** - * Runs one code iteration and returns + * Runs one code iteration (amount of lines set in Config.codeYieldPeriod) and returns + * organism destroy state * @return {Boolean} false means that organism was destroyed */ run() { this._iterations++; if (this.onBeforeRun() === false) {return true} - this.onRun(); - return this.alive && this._updateDestroy() && this._updateEnergy(); + const lines = this._energy > 0 ? this.onRun() : 0; + this._updateEnergy(); + if (this._energy > 0) { + this._updateClone(); + if (this._energy > 0) { + this.fire(ITERATION, lines, this); + this._energy > 0 && this._updateAge(); + } + } + + return true; } /** @@ -108,23 +139,21 @@ class Organism extends Observer { * @return {String} JSON string */ serialize() { - let json = { + let json = { id : this._id, x : this._x, y : this._y, changes : this._changes, - alive : this._alive, // 'item' will be added after insertion iterations : this._iterations, fnId : this._fnId, - jsvm : this.jsvm.serialize(), + vm : this.vm.serialize(), energy : this._energy, + startEnergy : this._startEnergy, color : this._color, mutationProbs : this._mutationProbs, - cloneMutationPercent: this._cloneMutationPercent, mutationPeriod : this._mutationPeriod, mutationPercent : this._mutationPercent, - cloneEnergyPercent : this._cloneEnergyPercent, mem : this.mem.slice() }; @@ -143,106 +172,108 @@ class Organism extends Observer { this._x = json.x; this._y = json.y; this._changes = json.changes; - this._alive = json.alive; // 'item' will be added after insertion this._iterations = json.iterations; this._fnId = json.fnId; - this.jsvm.unserialize(json.jsvm); + this.vm.unserialize(json.vm); this._energy = json.energy; + this._startEnergy = json.startEnergy; this._color = json.color; this._mutationProbs = json.mutationProbs; - this._cloneMutationPercent = json.cloneMutationPercent; this._mutationPeriod = json.mutationPeriod; this._mutationPercent = json.mutationPercent; - this._cloneEnergyPercent = json.cloneEnergyPercent; this._mem = json.mem.slice(); } - grabEnergy(amount) { - if (!IS_NUM(amount)) {return true} - const noEnergy = (this._energy -= amount) < 1; - noEnergy && this.destroy(); - return !noEnergy; - } - + // TODO: describe fitness in details fitness() { - return Math.abs(OConfig.codeMaxSize - this.jsvm.size) * this._energy * this._changes; + // TODO: check these variants + //return (OConfig.codeMaxSize + 1 - this.vm.size) * (this._energy - this._startEnergy) * (this._changes || 1); + //return (OConfig.codeMaxSize + 1 - this.vm.size) * (this._energy - this._startEnergy) * (this._changes || 1); + //return (OConfig.codeMaxSize + 1 - this.vm.size) * (this._energy - this._startEnergy); + //return (OConfig.codeMaxSize + 1 - this.vm.size) * ((this._energy - this._startEnergy) / this._iterations); + return this._energy / (this.vm.size || 1); } destroy() { - this.fire(EVENTS.DESTROY, this); - this._alive = false; - this._energy = 0; - this._item = null; - this._mem = null; - this.jsvm && this.jsvm.destroy(); - this.jsvm = null; - this._codeEndCb = null; + if (this.vm === null) {return} + this.fire(DESTROY, this); + this._energy = 0; + this._startEnergy = 0; + this._item = null; + this._mem = null; + this._mutationProbs = null; + this.vm && this.vm.destroy(); + this.vm = null; + this._operatorCls = null; + this._iterations = -1; super.destroy(); } - _updateColor(changes) { - if ((this._color += changes) > OConfig.ORG_MAX_COLOR) { - this._color = OConfig.ORG_FIRST_COLOR; - } - } - _create() { - this.jsvm = new JSVM(this._codeEndCb.bind(this, this), this, this._operatorCls); + this.vm = new VM(this, this._operatorCls, OConfig.orgOperatorWeights); this._energy = OConfig.orgStartEnergy; - this._color = OConfig.orgStartColor; + this._startEnergy = OConfig.orgStartEnergy; + this._color = Organism.getColor(MAX_COLORS); this._mutationProbs = OConfig.orgMutationProbs.slice(); - this._cloneMutationPercent = OConfig.orgCloneMutationPercent; this._mutationPeriod = OConfig.orgRainMutationPeriod; this._mutationPercent = OConfig.orgRainMutationPercent; - this._cloneEnergyPercent = OConfig.orgCloneEnergyPercent; - this._mem = []; + this._mem = new Array(Math.pow(2, OConfig.orgMemBits)); + + _fill(this._mem, 0); } _clone(parent) { - this.jsvm = new JSVM(this._codeEndCb.bind(this, this), this, this._operatorCls, parent.jsvm); + this.vm = new VM(this, this._operatorCls, OConfig.orgOperatorWeights, parent.vm); this._energy = parent.energy; + this._startEnergy = parent.energy; this._color = parent.color; this._mutationProbs = parent.mutationProbs.slice(); - this._cloneMutationPercent = parent.cloneMutationPercent; this._mutationPeriod = parent.mutationPeriod; this._mutationPercent = parent.mutationPercent; - this._cloneEnergyPercent = parent.cloneEnergyPercent; this._mem = parent.mem.slice(); } + _updateColor() { + this._color = Organism.getColor(OConfig.orgAlivePeriod === 0 ? MAX_COLORS : this._iterations * (MAX_COLORS / OConfig.orgAlivePeriod)); + } + + _updateClone() { + if (this._iterations > OConfig.orgCloneMinAge && this._energy > OConfig.orgCloneMinEnergy) {this.fire(CLONE, this)} + } + /** - * Checks if organism need to be killed/destroyed, because of age or zero energy - * @return {Boolean} false means that organism was destroyed. - * @private + * Checks if organism need to be killed, because of age + * @return {Boolean} false means that organism was killed. */ - _updateDestroy() { + _updateAge() { const alivePeriod = OConfig.orgAlivePeriod; - const needDestroy = this._energy < 1 || this._iterations >= alivePeriod && alivePeriod > 0; + const needDestroy = this._iterations >= alivePeriod && alivePeriod > 0; - needDestroy && this.destroy(); + if (needDestroy) { + this.fire(KILL_AGE, this); + this.destroy(); + } return !needDestroy; } /** - * This is how our system grabs an energy= require(organism if it's age is - * divided into OConfig.orgEnergySpendPeriod. + * This method destroys organisms with zero energy * @return {Boolean} false means that organism was destroyed. - * @private */ _updateEnergy() { - if (this._iterations % OConfig.orgEnergySpendPeriod !== 0 || OConfig.orgEnergySpendPeriod === 0) {return true} - const codeSize = this.jsvm.size; - let grabSize = Math.floor(codeSize / OConfig.orgGarbagePeriod); - - if (grabSize < 1) {grabSize = 1} - grabSize = Math.min(this._energy, grabSize); - this.fire(EVENTS.GRAB_ENERGY, grabSize); + // + // this.vm === null means, that organism has already destroyed + // + if (this._energy < 1 && this.vm) { + this.fire(KILL_NO_ENERGY, this); + this.destroy(); + } - return this.grabEnergy(grabSize); + return true; } } -module.exports = Organism; \ No newline at end of file +module.exports = {EVENTS: ORG_EVENTS, Organism}; \ No newline at end of file diff --git a/client/src/manager/plugins/organisms/Organisms.js b/client/src/manager/plugins/organisms/Organisms.js index 25e52d6..28e0631 100644 --- a/client/src/manager/plugins/organisms/Organisms.js +++ b/client/src/manager/plugins/organisms/Organisms.js @@ -12,10 +12,14 @@ const Config = require('./../../../../src/share/Config').Config; const OConfig = require('./Config'); const Console = require('./../../../../src/share/Console'); const EVENTS = require('./../../../../src/share/Events').EVENTS; -const Backup = require('./../backup/Backup'); +const ORG_EVENTS = require('./../../../../src/manager/plugins/organisms/Organism').EVENTS; +//const Backup = require('./../backup/Backup'); const Mutator = require('./Mutator'); - -const RAND_OFFS = 4; +const Num = require('./../../../vm/Num'); +/** + * {Number} Random range for selection of random organism from a Queue + */ +const RAND_RANGE = 5; // TODO: inherit this class from Configurable class Organisms extends Configurable { @@ -28,26 +32,6 @@ class Organisms extends Configurable { */ compare(org1, org2) {} - /** - * Is called every time after organism's code was run - * @param {Organism} org - * @abstract - */ - onOrganism(org) {} - - /** - * Is called after moving of organism is done. Updates Manager.positions - * map with a new position of organism - * @param {Number} x1 Start X position - * @param {Number} y1 Start Y position - * @param {Number} x2 End X position - * @param {Number} y2 End Y position - * @param {Organism} org Organism, which is moving - * @returns {Boolean} - * @abstract - */ - onAfterMove(x1, y1, x2, y2, org) {} - /** * Is called before cloning of organism * @param {Organism} org @@ -63,20 +47,6 @@ class Organisms extends Configurable { */ onClone(org, child) {} - /** - * Is called after organism has created - * @param {Organism} org - * @abstract - */ - onAfterCreateOrg(org) {} - - /** - * Is called after organism has killed - * @param {Organism} org Killed organism - * @abstract - */ - onAfterKillOrg(org) {} - /** * Creates one instance of organism. You have to override this * method in your child class @@ -87,15 +57,24 @@ class Organisms extends Configurable { createEmptyOrg(...args) {} constructor(manager) { - super(manager, {Config, cfg: OConfig}, {getAmount: ['_apiGetAmount', 'Shows amount of organisms within current Client(Manager)']}); + super(manager, {Config, cfg: OConfig}, { + getAmount : ['_apiGetAmount', 'Shows amount of organisms within current Client(Manager)'], + getOrganism: ['_apiGetOrganism', 'Returns organism instance by id or int\'s index in a Queue'] + }); this.organisms = manager.organisms; - this.manager = manager; this.randOrgItem = this.organisms.first; - this._mutator = new Mutator(manager); - this._onIterationCb = this.onIteration.bind(this); + this.positions = manager.positions; + this.world = manager.world; + + this._mutator = new Mutator(manager, this); + this._maxEnergy = 0; + this._oldMaxEnergy = 0; + this._onIterationCb = this._onIteration.bind(this); + this._onLoopCb = this._onLoop.bind(this); this.reset(); Helper.override(manager, 'onIteration', this._onIterationCb); + Helper.override(manager, 'onLoop', this._onLoopCb); } destroy() { @@ -103,91 +82,78 @@ class Organisms extends Configurable { let org; while (item && (org = item.val)) {org.destroy(); item = item.next} - Helper.unoverride(this.manager, 'onIteration', this._onIterationCb); + Helper.unoverride(this.parent, 'onIteration', this._onIterationCb); + Helper.unoverride(this.parent, 'onLoop', this._onLoopCb); this._mutator.destroy(); this._mutator = null; - this.manager = null; this._onIterationCb = null; + this._onLoopCb = null; super.destroy(); } /** - * Override of Manager.onIteration() method. Is called on every - * iteration of main loop. The counter is an analog of time. - * @param {Number} counter Value of main loop counter. - * @param {Number} stamp Time stamp of current iteration - * @private + * Is called at the end of run() method. Updates maxEnergy value for population + * @param {Organism} org Current organism */ - onIteration(counter, stamp) { - let item = this.organisms.first; - let org; - - while (item && (org = item.val)) { - org.run(); - this.onOrganism(org); - item = item.next; + onOrganism(org) { + if (org.energy > this._oldMaxEnergy) {this._oldMaxEnergy = org.energy} + if (org === this.organisms.last.val) { + this._maxEnergy = this._oldMaxEnergy; + this._oldMaxEnergy = 0; } - - this.updateClone(counter); - this.updateCrossover(counter); - this.updateCreate(); } addOrgHandlers(org) { - org.on(EVENTS.DESTROY, this._onKillOrg.bind(this)); + org.on(ORG_EVENTS.DESTROY, this._onDestroyOrg.bind(this)); + org.on(ORG_EVENTS.KILL_NO_ENERGY, this._onKillNoEnergyOrg.bind(this)); + org.on(ORG_EVENTS.KILL_AGE, this._onKillAgeOrg.bind(this)); + org.on(ORG_EVENTS.ITERATION, this._onIterationOrg.bind(this)); + org.on(ORG_EVENTS.CLONE, this._onCloneOrg.bind(this)); } - /** - * Cloning parents are chosen according to tournament principle - * @param {Number} counter Current counter - * @returns {boolean} - * @private - */ - updateClone(counter) { - const orgs = this.organisms; - const needClone = counter % OConfig.orgClonePeriod === 0 && OConfig.orgClonePeriod !== 0; - let orgAmount = orgs.size; - if (!needClone || orgAmount < 1) {return false} - let org1 = this.getRandOrg(); - let org2 = this.getRandOrg(); - if (!org1.alive && !org2.alive) {return false} - - let tmpOrg = this._tournament(org1, org2); - if (tmpOrg === org2) {[org1, org2] = [org2, org1]} - - if (orgAmount >= OConfig.orgMaxOrgs) {org2.destroy()} - if (org1.alive) {this._clone(org1)} - - return true; + reset() { + this._orgId = 0; } - updateCrossover(counter) { - const orgs = this.organisms; - const orgAmount = orgs.size; - const needCrossover = counter % OConfig.orgCrossoverPeriod === 0 && OConfig.orgCrossoverPeriod !== 0; - if (!needCrossover || orgAmount < 1) {return false} + move(x1, y1, x2, y2, org) { + const world = this.world; + if (world.isFree(x2, y2) === false) {return false} - let org1 = this._tournament(); - let org2 = this._tournament(); - let winner = this._tournament(org1, org2); - let looser = winner === org1 ? org2 : org1; + org.x = x2; + org.y = y2; + world.setDot(x2, y2, org.color); - if (looser.alive) { - this._crossover(winner, looser); - } + if (x1 === x2 && y1 === y2) {return false} + world.setDot(x1, y1, 0); + this.positions[x1][y1] = 0; + this.positions[x2][y2] = org; return true; } - updateCreate() { - if (this.organisms.size < 1) { - this._createPopulation(); - } + createOrg(x, y, parent = null) { + if (x === -1) {return false} + this._randOrg(); + const item = this.organisms.addAfter(this.randOrgItem, null); + let org = this.createEmptyOrg(++this._orgId + '', x, y, item, parent); + + item.val = org; + this.addOrgHandlers(org); + this.world.setDot(x, y, org.color); + this.positions[x][y] = org; + this.parent.fire(EVENTS.BORN_ORGANISM, org); + //Console.info(org.id, ' born'); + + return item; } - getRandOrg() { - const offs = Helper.rand(RAND_OFFS); + /** + * Returns random organism of current population + * @return {Organism|null} + */ + _randOrg() { + const offs = Helper.rand(RAND_RANGE) + 1; let item = this.randOrgItem; for (let i = 0; i < offs; i++) { @@ -199,109 +165,220 @@ class Organisms extends Configurable { return (this.randOrgItem = item).val; } - reset() { - this._orgId = 0; - this._maxEnergy = 0; - } - - move(x1, y1, x2, y2, org) { - let moved = false; - const world = this.manager.world; + /** + * Override of Manager.onIteration() method. Is called on every + * iteration of main loop. The counter is an analog of time. + * @param {Number} counter Value of main loop counter. + * @param {Number} stamp Time stamp of current iteration + */ + _onIteration(counter, stamp) { + let item = this.organisms.first; + let org; - if (world.isFree(x2, y2) === false) {return false} - if (x1 !== x2 || y1 !== y2) {moved = true; world.setDot(x1, y1, 0)} - world.setDot(x2, y2, org.color); - this.onAfterMove(x1, y1, x2, y2, org); + while (org = item && item.val) { + org.run(); + this.onOrganism(org); + item = item.next; + } - return moved; + this._updateTournament(counter); + this._updateRandomOrgs(counter); + this._updateCrossover(counter); } - createOrg(pos, parent = null) { - const orgs = this.organisms; - if (orgs.size >= OConfig.orgMaxOrgs || pos === false) {return false} - orgs.add(null); - let last = orgs.last; - let org = this.createEmptyOrg(++this._orgId + '', pos.x, pos.y, true, last, this._onCodeEnd.bind(this), parent); - - last.val = org; - this.addOrgHandlers(org); - this.move(pos.x, pos.y, pos.x, pos.y, org); - this.onAfterCreateOrg(org); - this.manager.fire(EVENTS.BORN_ORGANISM, org); - //Console.info(org.id, ' born'); - - return true; + _onLoop() { + this._updateCreate(); } _tournament(org1 = null, org2 = null) { - org1 = org1 || this.getRandOrg(); - org2 = org2 || this.getRandOrg(); + org1 = org1 || this._randOrg(); + org2 = org2 || this._randOrg(); - if (!org1.alive && !org2.alive) {return false} - if ((org2.alive && !org1.alive) || this.compare(org2, org1)) { + if (org1.energy < 1 && org2.energy < 1) {return false} + if ((org2.energy > 0 && org1.energy < 1) || this.compare(org2, org1)) { return org2; } return org1; } - _clone(org) { + _clone(org, isCrossover = false) { if (this.onBeforeClone(org) === false) {return false} - let pos = this.manager.world.getNearFreePos(org.x, org.y); - if (pos === false || this.createOrg(pos, org) === false) {return false} - let child = this.organisms.last.val; + let x; + let y; + let item; + [x, y] = this.world.getNearFreePos(org.x, org.y); + if (x === -1 || (item = this.createOrg(x, y, org)) === false) {return false} + let child = item.val; this.onClone(org, child); - this.manager.fire(EVENTS.CLONE, org, child); + if (org.energy < 1 || child.energy < 1) {return false} + this.parent.fire(EVENTS.CLONE, org, child, isCrossover); - return true; + return item; } - _crossover(winner, looser) { - this._clone(winner); - const orgs = this.organisms; - let child = orgs.last.val; + _crossover(org1, org2) { + const item = this._clone(org1, true); + if (item === false) {return false} + let child = item.val; - if (child.alive && looser.alive) { - child.changes += child.jsvm.crossover(looser.jsvm); - if (orgs.size >= OConfig.orgMaxOrgs) {looser.destroy()} + if (child.energy > 0 && org2.energy > 0) { + child.changes += (Math.abs(child.vm.crossover(org2.vm)) * Num.MAX_BITS); + return true; } + + return false; } _createPopulation() { - const world = this.manager.world; + const world = this.world; this.reset(); - for (let i = 0; i < OConfig.orgStartAmount; i++) { - this.createOrg(world.getFreePos()); + for (let i = 0, len = OConfig.orgStartAmount; i < len; i++) { + this.createOrg(...world.getFreePos()); } Console.info('Population has created'); } - _onCodeEnd(org, lines) { - this.manager.codeRuns++; - this.manager.fire(EVENTS.ORGANISM, org, lines); + /** + * API method, which will be added to Manager.api interface + * @api + * @return {Number} Amount of organisms within current Manager + */ + _apiGetAmount() { + return this.parent.organisms.size; } - _onKillOrg(org) { + /** + * Return organism instance by id or it's index in a Queue + * @param {Number|String} index Index or id + * @return {Organism} Organism instance or null + * @api + */ + _apiGetOrganism(index) { + if (Helper.isNumeric(index)) { + return this.organisms.get(index); + } + + let item = this.organisms.first; + let org; + + while (org = item && item.val) { + if (org.id === index) {return org} + item = item.next; + } + + return null; + } + + _onDestroyOrg(org) { if (this.randOrgItem === org.item) { if ((this.randOrgItem = org.item.next) === null) { this.randOrgItem = this.organisms.first; } } this.organisms.del(org.item); - this.manager.world.setDot(org.x, org.y, 0); - this.onAfterKillOrg(org); - this.manager.fire(EVENTS.KILL_ORGANISM, org); + this.world.setDot(org.x, org.y, 0); + this.positions[org.x][org.y] = 0; + this.parent.fire(EVENTS.KILL, org); //Console.info(org.id, ' die'); } + _onKillNoEnergyOrg(org) {this._parent.fire(EVENTS.KILL_NO_ENERGY, org)} + _onKillAgeOrg(org) {this._parent.fire(EVENTS.KILL_AGE, org)} + _onIterationOrg(lines, org) { + this._parent.codeRuns += lines; + this._parent.fire(EVENTS.CODE_RUN, lines, org); + } + + _onCloneOrg(org) { + //const maxOrgs = OConfig.orgMaxOrgs; + //const orgAmount = this.organisms.size; + + //if (OConfig.orgKillOnClone && orgAmount >= maxOrgs) {this._killInTour()} + //if (orgAmount >= maxOrgs && (OConfig.orgKillOnClone || Math.random() <= (org.energy / org.vm.size) / this._maxEnergy)) {this._randOrg().destroy()} + // if (this.organisms.size >= OConfig.orgMaxOrgs && Math.random() <= ((org.energy / 10000000000000) * (org.iterations / OConfig.orgAlivePeriod))) { + // this._randOrg().destroy(); + // } + //if (this.organisms.size < maxOrgs) {this._clone(org)} + //if (this.organisms.size >= maxOrgs && Math.random() <= (org.energy / org.vm.size) / this._maxEnergy) {this._randOrg().destroy()} + // + // This is very important part of application! Cloning should be available only if + // amount of organisms is less then maximum or if current organism has ate other just + // now (and free one slot in Organisms.organisms queue). It's not a good idea to + // kill organisms here with small amount of energy or support more energetic + // organisms before cloning. They should kill each other to have a possibility + // to clone them. + // + if (OConfig.orgKillOnClone && this.organisms.size >= OConfig.orgMaxOrgs) { + const randOrg = this._randOrg(); + if (randOrg !== org && Math.random() <= org.iterations / OConfig.orgAlivePeriod) {randOrg.destroy()} + } + if (this.organisms.size < OConfig.orgMaxOrgs) {this._clone(org)} + } + + _killInTour() { + let org1 = this._randOrg(); + let org2 = this._randOrg(); + if (org1.energy < 1 || org2.energy < 1 || org1 === org2 || this.organisms.size < 1) {return false} + const winner = this._tournament(org1, org2); + if (winner === false) {return false} + + if (winner === org2) {[org1, org2] = [org2, org1]} + this.parent.fire(EVENTS.KILL_TOUR, org2); + org2.destroy(); + + return true; + } + /** - * API method, which will be added to Manager.api interface - * @return {Number} Amount of organisms within current Manager + * Does tournament between two random organisms and kill looser. In general + * this function is a natural selection in our system. + * @param {Number} counter Current value of global iterations counter + * @returns {Boolean} true - tournament occurred, false - otherwise */ - _apiGetAmount() { - return this.manager.organisms.size; + _updateTournament(counter) { + const period = OConfig.orgTournamentPeriod; + return counter % period === 0 && this.organisms.size > 0 || period !== 0 && this._killInTour(); + } + + _updateRandomOrgs(counter) { + if (counter % OConfig.orgRandomOrgPeriod !== 0 || OConfig.orgRandomOrgPeriod === 0 || this.organisms.size < 1) {return false} + const vm = this._randOrg().vm; + const size = Helper.rand(vm.size) + 1; + const pos = Helper.rand(vm.size - size); + + vm.generate(pos, size); + + return true; + } + + _updateCrossover(counter) { + const orgAmount = this.organisms.size; + if (counter % OConfig.orgCrossoverPeriod !== 0 || OConfig.orgCrossoverPeriod === 0 || orgAmount < 1) {return false} + // + // We have to have a possibility to crossover not only with best + // organisms, but with low fit also + // + let org1 = Helper.rand(2) === 0 ? this._tournament() : this._randOrg(); + let org2 = Helper.rand(2) === 0 ? this._tournament() : this._randOrg(); + + if (org1 === false || org2 === false || org1.energy < 1 || org2.energy < 1) {return false} + this._crossover(org1, org2); + if (orgAmount + 1 >= OConfig.orgMaxOrgs) { + const winner = this._tournament(org1, org2); + if (winner === false) {return false} + if (winner === org2) {[org1, org2] = [org2, org1]} + this.parent.fire(EVENTS.KILL_OVERFLOW, org2); + org2.destroy(); + } + + return true; + } + + _updateCreate() { + if (this.organisms.size < 1) {this._createPopulation()} } } diff --git a/client/src/manager/plugins/organisms/dos/Code2String.js b/client/src/manager/plugins/organisms/dos/Code2String.js index 5e34bf6..139b0f4 100644 --- a/client/src/manager/plugins/organisms/dos/Code2String.js +++ b/client/src/manager/plugins/organisms/dos/Code2String.js @@ -5,56 +5,50 @@ * * @author flatline */ -const Num = require('./../../../../jsvm/Num'); - +const Num = require('./../../../../vm/Num'); +const OConfig = require('./../Config'); +const Helper = require('./../../../../../../common/src/Helper'); /** * {Function} Just a shortcuts */ -const VAR0 = Num.getVar; -const VAR1 = (n) => Num.getVar(n, 1); -const VAR2 = (n) => Num.getVar(n, 2); -const BITS_AFTER_THREE_VARS = Num.BITS_PER_OPERATOR + Num.BITS_PER_VAR * 3; -const BITS_FOR_NUMBER = 16; -const HALF_OF_VAR = Num.MAX_VAR / 2; +const FOUR_BITS = 4; +const CONDITION_BITS = 2; class Code2String { constructor(manager) { this._manager = manager; /** * {Object} These operator handlers should return string representation - * of numeric based byte jsvm. + * of numeric based byte vm. */ - this._OPERATORS_CB = { - 0 : this._onVar.bind(this), - //1: this._onFunc.bind(this), - 1 : this._onCondition.bind(this), - 2 : this._onLoop.bind(this), - 3 : this._onOperator.bind(this), - 4 : this._onNot.bind(this), - //5 : this._onPi.bind(this), - //6 : this._onTrig.bind(this), - 5 : this._onLookAt.bind(this), - 6 : this._onEatLeft.bind(this), - 7 : this._onEatRight.bind(this), - 8 : this._onEatUp.bind(this), - 9 : this._onEatDown.bind(this), - 10: this._onStepLeft.bind(this), - 11: this._onStepRight.bind(this), - 12: this._onStepUp.bind(this), - 13: this._onStepDown.bind(this), - 14: this._onFromMem.bind(this), - 15: this._onToMem.bind(this), - 16: this._onMyX.bind(this), - 17: this._onMyY.bind(this), - 18: this._onCheckLeft.bind(this), - 19: this._onCheckRight.bind(this), - 20: this._onCheckUp.bind(this), - 21: this._onCheckDown.bind(this) - }; - this._OPERATORS_CB_LEN = Object.keys(this._OPERATORS_CB).length; + this._OPERATORS_CB = [ + this._onVar.bind(this), + this._onConst.bind(this), + this._onCondition.bind(this), + this._onLoop.bind(this), + this._onOperator.bind(this), + this._onLookAt.bind(this), + this._onEatLeft.bind(this), + this._onEatRight.bind(this), + this._onEatUp.bind(this), + this._onEatDown.bind(this), + this._onStepLeft.bind(this), + this._onStepRight.bind(this), + this._onStepUp.bind(this), + this._onStepDown.bind(this), + this._onFromMem.bind(this), + this._onToMem.bind(this), + this._onMyX.bind(this), + this._onMyY.bind(this), + this._onCheckLeft.bind(this), + this._onCheckRight.bind(this), + this._onCheckUp.bind(this), + this._onCheckDown.bind(this) + ]; + this._OPERATORS_CB_LEN = this._OPERATORS_CB.length; /** * {Array} Available conditions for if operator. Amount should be - * the same like (1 << BITS_PER_VAR) + * the same like (1 << Num.BITS_PER_VAR) */ this._CONDITIONS = ['<', '>', '==', '!=']; /** @@ -63,23 +57,29 @@ class Code2String { this._OPERATORS = [ '+', '-', '*', '/', '%', '&', '|', '^', '>>', '<<', '>>>', '<', '>', '==', '!=', '<=' ]; - //this._TRIGS = ['sin', 'cos', 'tan', 'abs']; /** * {Array} Contains closing bracket offset for "if", "loop",... operators */ - this._offsets = []; + this._offsets = [0]; + + Num.init(this._OPERATORS_CB_LEN); - Num.setOperatorAmount(this._OPERATORS_CB_LEN); + this._BITS_AFTER_ONE_VAR = Num.BITS_PER_OPERATOR + Num.BITS_PER_VAR; + this._BITS_AFTER_TWO_VARS = Num.BITS_PER_OPERATOR + Num.BITS_PER_VAR * 2; + this._BITS_AFTER_THREE_VARS = Num.BITS_PER_OPERATOR + Num.BITS_PER_VAR * 3; // // API of the Manager for accessing outside. (e.g. from Console) // - manager.api.formatCode = (code) => this.format(code); + Helper.setApi(manager.api, 'formatCode', (code) => this.format(code), 'Converts byte code array into human readable JavaScript based code. This function is low level. For using it you have to get organism\'s virtual machine reference and then use it\'s code property. For example: man.api.formatCode(man.api.organisms.getOrganism(\'128\').vm.code). This example will find organism with id \'128\' and shows his byte code.'); } destroy() { this._OPERATORS_CB = null; this._CONDITIONS = null; this._OPERATORS = null; + this._offsets = null; + delete this._manager.api.formatCode; + this._manager = null; } format(code, separator = '\n') { @@ -87,155 +87,144 @@ class Code2String { const operators = this._OPERATORS_CB; const offs = this._offsets; let lines = new Array(len); - let needClose = 0; - + // + // First number always amount of code lines + // + offs.splice(0, offs.length, len); for (let line = 0; line < len; line++) { + lines[line] = operators[Num.getOperator(code[line])](code[line], line); // // We found closing bracket '}' of some loop and have to add // it to output code array // - if (line === offs[offs.length - 1]) { - while (offs.length > 0 && offs[offs.length - 1] === line) { - offs.pop(); - needClose++; - } - } - lines[line] = operators[Num.getOperator(code[line])](code[line], line, len); - if (needClose > 0) { - for (let i = 0; i < needClose; i++) { - lines[line] = '}' + lines[line]; - } - needClose = 0; + while (offs.length > 1 && line === offs[offs.length - 1]) { + line = offs.pop(); + lines[line] += '}'; } } // // All closing brackets st the end of JS script // const length = lines.length - 1; - for (let i = 0; i < offs.length; i++) { + for (let i = 1; i < offs.length; i++) { lines[length] += '}'; } - offs.length = 0; return js_beautify(lines.join(separator), {indent_size: 4}); } /** * Parses variable operator. Format: let = const|number. Num bits format: - * BITS_PER_OPERATOR bits - operator id - * BITS_PER_VAR bits - destination var index - * BITS_PER_VAR bits - assign type (const (half of bits) or variable (half of bits)) - * BITS_PER_VAR bits - variable index or all bits till the end for constant + * Num.BITS_PER_OPERATOR bits - operator id + * Num.BITS_PER_VAR bits - destination var index + * Num.BITS_PER_VAR bits - assign type (const (half of bits) or variable (half of bits)) + * Num.BITS_PER_VAR bits - variable index or all bits till the end for constant * - * @param {Num} num Packed into number jsvm line - * @return {String} Parsed jsvm line string + * @param {Num} num Packed into number vm line + * @return {String} Parsed vm line string */ _onVar(num) { - const var1 = VAR1(num); - const isConst = VAR2(num) >= HALF_OF_VAR; - - return `v${VAR0(num)}=${isConst ? Num.getBits(num, BITS_AFTER_THREE_VARS, BITS_FOR_NUMBER) : ('v' + var1)}`; + return `v${Num.getVar0(num)}=${'v' + Num.getVar1(num)}`; } - _onFunc(num) { - return ''; + _onConst(num) { + return `v${Num.getVar0(num)}=${Num.getBits(num, this._BITS_AFTER_ONE_VAR, OConfig.codeConstBits)}`; } - _onCondition(num, line, lines) { - const val3 = Num.getBits(num, BITS_AFTER_THREE_VARS, Num.BITS_OF_TWO_VARS); - this._offsets.push(this._getOffs(line, lines, val3)); - return `if(v${VAR0(num)}${this._CONDITIONS[VAR2(num)]}v${VAR1(num)}){`; - } + _onCondition(num, line) { + const cond = Num.getBits(num, this._BITS_AFTER_TWO_VARS, CONDITION_BITS); + const blockOffs = Num.getBits(num, this._BITS_AFTER_TWO_VARS + CONDITION_BITS, OConfig.codeBitsPerBlock); - _onLoop(num, line, lines) { - const var0 = VAR0(num); - const val3 = Num.getBits(num, BITS_AFTER_THREE_VARS, Num.BITS_OF_TWO_VARS); - this._offsets.push(this._getOffs(line, lines, val3)); - return `for(v${var0}=v${VAR1(num)};v${var0} 0 && offset >= offsets[length - 1]) { - return offsets[length - 1]; - } - - return offset; + _getOffs(line, offs) { + const offsets = this._offsets || [0]; + return line + offs > offsets[offsets.length - 1] ? offsets[offsets.length - 1] : line + offs; } } diff --git a/client/src/manager/plugins/organisms/dos/Operators.js b/client/src/manager/plugins/organisms/dos/Operators.js index 9589f18..e99548c 100644 --- a/client/src/manager/plugins/organisms/dos/Operators.js +++ b/client/src/manager/plugins/organisms/dos/Operators.js @@ -1,49 +1,70 @@ /** - * Digital Organisms Script - (DOS) is a simple language for JSVM. + * Digital Organisms Script - (DOS) is a simple language for VM. * This file contains all available operators implementation. For example: * for, if, variable declaration, steps, eating etc... User may override * this class for own needs and change operator list to custom. * * @author flatline */ -const DIR = require('./../../../../../../common/src/Directions').DIR; const Helper = require('./../../../../../../common/src/Helper'); const EVENTS = require('./../../../../../src/share/Events').EVENTS; -const Config = require('./../../../../../src/share/Config').Config; const OConfig = require('./../Config'); -const Operators = require('./../../../../../src/jsvm/Operators'); -const Num = require('./../../../../../src/jsvm/Num'); +const Operators = require('./../../../../vm/Operators'); +const Num = require('./../../../../vm/Num'); +/** + * {Function} Is created to speed up this function call. constants are run + * much faster, then Helper.normalize() + */ +const IN_WORLD = Helper.inWorld; +const IS_FINITE = Number.isFinite; +const IS_NAN = Number.isNaN; /** * {Function} Just a shortcuts */ -const VAR0 = Num.getVar; -const VAR1 = (n) => Num.getVar(n, 1); -const VAR2 = (n) => Num.getVar(n, 2); -const BITS_AFTER_THREE_VARS = Num.BITS_PER_OPERATOR + Num.BITS_PER_VAR * 3; const FOUR_BITS = 4; -const BLOCK_MAX_LEN = OConfig.codeBitsPerBlock; -const BITS_FOR_NUMBER = 16; -const IS_NUM = Helper.isNumeric; -const HALF_OF_VAR = Num.MAX_VAR / 2; const CONDITION_BITS = 2; +const MAX_VAL = Number.MAX_VALUE; +/** + * {Array} Available conditions for if operator. Amount should be + * the same like (1 << Num.BITS_PER_VAR) + */ +const CONDITIONS = [(a,b)=>aa>b, (a,b)=>a===b, (a,b)=>a!==b]; +/** + * {Array} Available operators for math calculations + */ +const OPERATORS = [ + (a,b) => {const v=a+b; return IS_FINITE(v)?v:MAX_VAL}, + (a,b) => {const v=a-b; return IS_FINITE(v)?v:-MAX_VAL}, + (a,b) => {const v=a*b; return IS_FINITE(v)?v:MAX_VAL}, + (a,b) => {const v=a/b; return IS_FINITE(v)?v:MAX_VAL}, + (a,b) => {const v=a%b; return IS_NAN(v)?0:v}, + (a,b) => a&b, + (a,b) => a|b, + (a,b) => a^b, + (a,b) => a>>b, + (a,b) => a< a>>>b, + (a,b) => +(a +(a>b), + (a,b) => +(a===b), + (a,b) => +(a!==b), + (a,b) => +(a<=b) +]; class OperatorsDos extends Operators { constructor(offs, vars, obs) { super(offs, vars, obs); /** - * {Object} These operator handlers should return string, which - * will be added to the final string script for evaluation. + * {Object} These operator handlers should return next script line + * number VM should step to */ this._OPERATORS_CB = [ this.onVar.bind(this), - //this.onFunc.bind(this), + this.onConst.bind(this), this.onCondition.bind(this), this.onLoop.bind(this), this.onOperator.bind(this), - this.onNot.bind(this), - //this.onPi.bind(this), - //this.onTrig.bind(this), this.onLookAt.bind(this), this.onEatLeft.bind(this), this.onEatRight.bind(this), @@ -63,91 +84,113 @@ class OperatorsDos extends Operators { this.onCheckDown.bind(this) ]; /** - * {Array} Available conditions for if operator. Amount should be - * the same like (1 << BITS_PER_VAR) - */ - this._CONDITIONS = [(a,b)=>aa>b, (a,b)=>a===b, (a,b)=>a!==b]; - /** - * {Array} Available operators for math calculations + * {Object} Reusable object to pass it as a parameter to this.fire(..., ret) */ - this._OPERATORS = [ - (a,b)=>a+b, (a,b)=>a-b, (a,b)=>a*b, (a,b)=>a/b, (a,b)=>a%b, (a,b)=>a&b, (a,b)=>a|b, (a,b)=>a^b, (a,b)=>a>>b, (a,b)=>a<a>>>b, (a,b)=>+(a+(a>b), (a,b)=>+(a===b), (a,b)=>+(a!==b), (a,b)=>+(a<=b) - ]; - //this._TRIGS = [(a)=>Math.sin(a), (a)=>Math.cos(a), (a)=>Math.tan(a), (a)=>Math.abs(a)]; + this._ret = {ret: 0}; // // We have to set amount of available operators for correct // working of mutations of operators. // - Num.setOperatorAmount(this._OPERATORS_CB.length); + Num.init(this._OPERATORS_CB.length); + + this._BITS_AFTER_ONE_VAR = Num.BITS_PER_OPERATOR + Num.BITS_PER_VAR; + this._BITS_AFTER_TWO_VARS = Num.BITS_PER_OPERATOR + Num.BITS_PER_VAR * 2; + this._BITS_AFTER_THREE_VARS = Num.BITS_PER_OPERATOR + Num.BITS_PER_VAR * 3; } destroy() { super.destroy(); this._OPERATORS_CB = null; - this._CONDITIONS = null; - this._OPERATORS = null; - //this._TRIGS = null; + this._ret = null; } get operators() {return this._OPERATORS_CB} /** - * Parses variable operator. Format: var = number|var. 'num' bits format: - * TODO: + * Handler of variable assignment operator. 'xx' means, that amount of + * bits depends on configuration. '...' means, that all other bits are + * ignored. Example: + * bits : 8 xx xx + * number: 00000000 00 01... + * desc : var v0 v1 + * string: v0 = v1 * - * @param {Num} num Packed into number jsvm line - * @param {Number} line Current line in jsvm - * @return {Number} Parsed jsvm line string + * @param {Number} num One bit packed byte code row + * @param {Number} line Current line in DOS code + * @return {Number} Next line number to proceed */ onVar(num, line) { - const vars = this.vars; - vars[VAR0(num)] = VAR2(num) < HALF_OF_VAR ? Num.getBits(num, BITS_AFTER_THREE_VARS, BITS_FOR_NUMBER) : vars[VAR1(num)]; - return line + 1; + this.vars[Num.getVar0(num)] = this.vars[Num.getVar1(num)]; + return ++line; } - //onFunc(num, line) { - // return line + 1; - //} + /** + * Handler of numeric constant assignment operator. 'xx' means, that amount of + * bits depends on configuration. '...' means, that all other bits are + * ignored. Example: + * bits : 8 xx xx + * number: 00000001 00 01... + * desc : const v0 1 + * string: v0 = 1 + * + * @param {Number} num One bit packed byte code row + * @param {Number} line Current line in DOS code + * @return {Number} Next line number to proceed + */ + onConst(num, line) { + this.vars[Num.getVar0(num)] = Num.getBits(num, this._BITS_AFTER_ONE_VAR, OConfig.codeConstBits); + return ++line; + } - onCondition(num, line, org, lines) { - const val3 = Num.getBits(num, BITS_AFTER_THREE_VARS, BLOCK_MAX_LEN); - const offs = this._getOffs(line, lines, val3); - const cond = VAR2(num) >>> (OConfig.codeBitsPerVar - CONDITION_BITS); + /** + * Handler of 'if' operator. 'xx' means, that amount of bits depends on + * configuration. '...' means, that all other bits are + * ignored. Offset of closing bracket means row number after, which this + * bracket will be added. Offset of closing bracket is calculating using + * formula: line + offs. Example: + * bits : 8 xx xx 2 xx + * number: 00000010 00 01 00 00... + * desc : if v0 v1 < } + * string: if (v0 < v1) {} + * + * @param {Number} num One bit packed byte code row + * @param {Number} line Current line in DOS code + * @return {Number} Next line number to proceed + */ + onCondition(num, line) { + const cond = Num.getBits(num, this._BITS_AFTER_TWO_VARS, CONDITION_BITS); + const offs = this._getOffs(line, Num.getBits(num, this._BITS_AFTER_TWO_VARS + CONDITION_BITS, OConfig.codeBitsPerBlock)); - if (this._CONDITIONS[cond](this.vars[VAR0(num)], this.vars[VAR1(num)])) { - return line + 1; + if (CONDITIONS[cond](this.vars[Num.getVar0(num)], this.vars[Num.getVar1(num)])) { + this.offs.push(offs, offs); + return ++line; } return offs; } /** - * for(v0=v1; v0 0) { - org.x = ret.x; - org.y = ret.y; + onFromMem(num, line, org) { + if (Num.getBits(num, this._BITS_AFTER_TWO_VARS, 1)) { + const offs = (this.vars[Num.getVar1(num)] + .5) << 0; + this.vars[Num.getVar0(num)] = org.mem[offs >= org.mem.length || offs < 0 ? 0 : offs]; + return ++line; } - return ret.ret; + this.vars[Num.getVar0(num)] = org.mem[Num.getBits(num, this._BITS_AFTER_TWO_VARS + 1, OConfig.orgMemBits)]; + return ++line; + } + onToMem(num, line, org) { + if (Num.getBits(num, this._BITS_AFTER_TWO_VARS, 1)) { + const offs = (this.vars[Num.getVar0(num)] + .5) << 0; + org.mem[offs >= org.mem.length || offs < 0 ? 0 : offs] = this.vars[Num.getVar1(num)]; + return ++line; + } + + org.mem[Num.getBits(num, this._BITS_AFTER_TWO_VARS + 1, OConfig.orgMemBits)] = this.vars[Num.getVar0(num)]; + return ++line; + } + + onMyX(num, line, org) {this.vars[Num.getVar0(num)] = org.x; return ++line} + onMyY(num, line, org) {this.vars[Num.getVar0(num)] = org.y; return ++line} + + onCheckLeft(num, line, org) { + this.obs.fire(EVENTS.CHECK_AT, org.x - 1, org.y, this._ret); + this.vars[Num.getVar0(num)] = this._ret.ret; + return ++line; + } + onCheckRight(num, line, org) { + this.obs.fire(EVENTS.CHECK_AT, org.x + 1, org.y, this._ret); + this.vars[Num.getVar0(num)] = this._ret.ret; + return ++line; + } + onCheckUp(num, line, org) { + this.obs.fire(EVENTS.CHECK_AT, org.x, org.y - 1, this._ret); + this.vars[Num.getVar0(num)] = this._ret.ret; + return ++line; + } + onCheckDown(num, line, org) { + this.obs.fire(EVENTS.CHECK_AT, org.x, org.y + 1, this._ret); + this.vars[Num.getVar0(num)] = this._ret.ret; + return ++line; + } + + _step(org, x1, y1, x2, y2, step) { + this.obs.fire(EVENTS.STEP, org, x1, y1, x2, y2); + return org.x === x2 && org.y === y2 ? step : 0; } /** - * Returns offset for closing bracket of blocked operators like + * Returns offset for closing bracket of block operators like * "if", "for" and so on. These operators shouldn't overlap each * other. for example: * @@ -269,21 +336,12 @@ class OperatorsDos extends Operators { * So it's possible to set it to one of 1...3. So we change it in * real time to fix the overlap problem. * @param {Number} line Current line index - * @param {Number} lines Amount of lines * @param {Number} offs Local offset of closing bracket we want to set * @returns {Number} - * @private */ - _getOffs(line, lines, offs) { - let offset = line + offs < lines ? line + offs + 1 : lines; - const offsets = this.offs; - const length = offsets.length; - - if (length > 0 && offset >= offsets[length - 1]) { - return offsets[length - 1]; - } - - return offset; + _getOffs(line, offs) { + const offsets = this.offs || [0]; + return line + offs > offsets[offsets.length - 1] ? offsets[offsets.length - 1] : line + offs + 1; } } diff --git a/client/src/manager/plugins/organisms/dos/OperatorsSpec.js b/client/src/manager/plugins/organisms/dos/OperatorsSpec.js index ab5069f..16da5a2 100644 --- a/client/src/manager/plugins/organisms/dos/OperatorsSpec.js +++ b/client/src/manager/plugins/organisms/dos/OperatorsSpec.js @@ -1,1047 +1,1440 @@ +const _fill = require('lodash/fill'); + describe("client/src/organism/OperatorsDos", () => { - let OperatorsDos = require('./Operators'); - let Helper = require('./../../../../../../common/src/Helper'); - let Observer = require('./../../../../../../common/src/Observer'); - let EVENTS = require('./../../../../share/Events').EVENTS; - let EVENT_AMOUNT = require('./../../../../share/Events').EVENT_AMOUNT; - let Config = require('./../../../../share/Config').Config; - let OConfig = require('./../../organisms/Config'); - let api = require('./../../../../share/Config').api; - let cbpv = null; - - beforeEach(() => {cbpv = OConfig.codeBitsPerVar;api.set('codeBitsPerVar', 2)}); - afterEach(() => api.set('codeBitsPerVar', cbpv)); - - it("Checking onVar() method", () => { - let ops = new OperatorsDos([], [0, 0, 0, 0], new Observer()); - - expect(ops.onVar(0x00ffffff, 0)).toEqual(1); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 0).toEqual(true); - expect(ops.vars[2] === 0).toEqual(true); - expect(ops.vars[3] === 0).toEqual(true); - expect(ops.onVar(0x000fffff, 0)).toEqual(1); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 0).toEqual(true); - expect(ops.vars[2] === 0).toEqual(true); - expect(ops.vars[3] === 0).toEqual(true); - expect(ops.onVar(0x0000ffff, 0)).toEqual(1); - expect(ops.vars[0] === 0x3fff).toEqual(true); - expect(ops.vars[1] === 0).toEqual(true); - expect(ops.vars[2] === 0).toEqual(true); - expect(ops.vars[3] === 0).toEqual(true); - - ops.destroy(); + const OConfig = require('./../../organisms/Config'); + const THelper = require('./../../../../../../common/tests/Helper'); + const cbpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 2; + const OperatorsDos = require('./Operators'); + const EVENTS = require('./../../../../share/Events').EVENTS; + const Config = require('./../../../../share/Config').Config; + const OrganismDos = require('./../../organisms/dos/Organism'); + const ConfigHelper = require('./../../../../../../common/tests/Config'); + + afterAll(() => OConfig.codeBitsPerVar = cbpv); + + describe('Checks creation, destroy and public API', () => { + it('Checking instance creation', () => { + let offs = []; + let vars = []; + let obs = {}; + let ops = new OperatorsDos(offs, vars, obs); + expect(ops.offs).toBe(offs); + expect(ops.vars).toBe(vars); + expect(ops.obs).toBe(obs); + ops.destroy(); + }); + it('Checking destroy', () => { + let offs = []; + let vars = []; + let obs = {}; + let ops = new OperatorsDos(offs, vars, obs); + ops.destroy(); + expect(ops.offsets).not.toBe(offs); + expect(ops.vars).not.toBe(vars); + expect(ops.obs).not.toBe(obs); + }); + it('Checking operators getter', () => { + let offs = []; + let vars = []; + let obs = {}; + let ops = new OperatorsDos(offs, vars, obs); + expect(Array.isArray(ops.operators)).toBe(true); + expect(ops.operators.length > 0).toBe(true); + ops.destroy(); + }); }); - // it("Checking onVar() method with 3 bits per var config", () => { - // let ops = new OperatorsDos([], [0, 1, 2, 3], new Observer()); - // let bpv = OConfig.codeBitsPerVar; - // - // OConfig.codeBitsPerVar = 4; - // expect(ops.onVar(0x00ffffff, 0)).toEqual(1); - // expect(ops.vars[0] === 0).toEqual(true); - // expect(ops.vars[1] === 1).toEqual(true); - // expect(ops.vars[2] === 2).toEqual(true); - // expect(ops.vars[3] === 3).toEqual(true); - // expect(ops.onVar(0x000fffff, 0)).toEqual(1); - // expect(ops.vars[0] === 3).toEqual(true); - // expect(ops.vars[1] === 1).toEqual(true); - // expect(ops.vars[2] === 2).toEqual(true); - // expect(ops.vars[3] === 3).toEqual(true); - // // expect(ops.onVar(0x0000ffff, 0)).toEqual(1); - // // expect(ops.vars[0] === 0x3fff).toEqual(true); - // // expect(ops.vars[1] === 0).toEqual(true); - // // expect(ops.vars[2] === 0).toEqual(true); - // // expect(ops.vars[3] === 0).toEqual(true); - // - // OConfig.codeBitsPerVar = bpv; - // ops.destroy(); - // }); - - it("Checking onVar() method 2", () => { - let ops = new OperatorsDos([], [1, 2, 3, 4], new Observer()); - - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 2).toEqual(true); - expect(ops.vars[2] === 3).toEqual(true); - expect(ops.vars[3] === 4).toEqual(true); - expect(ops.onVar(0x001fffff, 0)).toEqual(1); - expect(ops.vars[0] === 2).toEqual(true); - expect(ops.vars[1] === 2).toEqual(true); - expect(ops.vars[2] === 3).toEqual(true); - expect(ops.vars[3] === 4).toEqual(true); - - ops.destroy(); + describe('onVar() method', () => { + let org; + let ops; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + + it("Checking variables working", () => { + expect(ops.onVar(0x00dfffff, 0)).toEqual(1); // 0xd === 0b1101, var3 = var1 + expect(ops.vars).toEqual([0, 1, 2, 1]); + expect(ops.onVar(0x000fffff, 0)).toEqual(1); // 0x0 === 0b0000, var0 = var0 + expect(ops.vars).toEqual([0, 1, 2, 1]); + expect(ops.onVar(0x006fffff, 0)).toEqual(1); // 0x6 === 0b0110, var1 = var2 + expect(ops.vars).toEqual([0, 2, 2, 1]); + expect(ops.onVar(0x00ffffff, 0)).toEqual(1); // 0xf === 0b1111, var3 = var3 + expect(ops.vars).toEqual([0, 2, 2, 1]); + }); + + it("Checking onVar() method with 3 bits per var config", () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 3; + let ops1 = new OperatorsDos([], [0, 1, 2, 3, 4, 5, 6, 7], org); + + expect(ops1.onVar(0x00ffffff, 0)).toEqual(1); // 0xff === 0b[111111]11, var7 = var7 + expect(ops1.vars).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); + expect(ops1.onVar(0x005dffff, 0)).toEqual(1); // 0x5d === 0b[010111]01, var2 = var7 + expect(ops1.vars).toEqual([0, 1, 7, 3, 4, 5, 6, 7]); + expect(ops1.onVar(0x005fffff, 0)).toEqual(1); // 0x5f === 0b[010111]11, var2 = var7 + expect(ops1.vars).toEqual([0, 1, 7, 3, 4, 5, 6, 7]); + expect(ops1.onVar(0x0000ffff, 0)).toEqual(1); // 0x00 === 0b[000000]00, var0 = var0 + expect(ops1.vars).toEqual([0, 1, 7, 3, 4, 5, 6, 7]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); + + it('Checking line increase', () => { + expect(ops.onVar(0x000fffff, 0)).toEqual(1); // 0x0 === 0b0000, var0 = var0 + expect(ops.onVar(0x000fffff, 1)).toEqual(2); // 0x0 === 0b0000, var0 = var0 + expect(ops.onVar(0x000fffff, 100)).toEqual(101); // 0x0 === 0b0000, var0 = var0 + }); }); - it("Checking onCondition() method", () => { - let ops = new OperatorsDos([], [0, 1, 2, 3], new Observer()); + describe('onConst() method', () => { + let org; + let ops; + let codeConstBits = OConfig.codeConstBits; + OConfig.codeConstBits = 16; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + afterAll (() => OConfig.codeConstBits = codeConstBits); + + it("Checking different constant values", () => { + expect(ops.onConst(0x01dfffff, 0)).toEqual(1); // 0xdffff === 0b[11][0111111111111111]11, var3 = 0x7fff + expect(ops.vars).toEqual([0, 1, 2, 0x7fff]); + expect(ops.onConst(0x010fffff, 0)).toEqual(1); // 0x0ffff === 0b[00][0011111111111111]11, var0 = 0x3fff + expect(ops.vars).toEqual([0x3fff, 1, 2, 0x7fff]); + expect(ops.onConst(0x01000000, 0)).toEqual(1); // 0x00000 === 0b[00][0000000000000000]00, var0 = 0x0000 + expect(ops.vars).toEqual([0, 1, 2, 0x7fff]); + }); - expect(ops.onCondition(0x01ffffff, 0, {}, 0)).toEqual(0); //if(v3!=v3)'); - expect(ops.onCondition(0x011fffff, 0, {}, 0)).toEqual(1); //if(v0!=v1)'); - expect(ops.onCondition(0x011abfff, 0, {}, 0)).toEqual(0); //if(v0==v1)'); - expect(ops.onCondition(0x01ffffff, 0, {}, 0)).toEqual(0); //if(v3!=v3)'); - expect(ops.onCondition(0x011fffff, 0, {}, 2)).toEqual(1); //if(v0!=v1)'); - expect(ops.onCondition(0x01ffffff, 0, {}, 2)).toEqual(2); //if(v3!=v3)'); - expect(ops.onCondition(0x01ffffff, 0, {}, 20)).toEqual(16); //if(v3!=v3)'); + it("Checking onConst() method with 3 bits per var config", () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 3; + let ops1 = new OperatorsDos([], [0, 1, 2, 3, 4, 5, 6, 7], org); - ops.destroy(); - }); + expect(ops1.onConst(0x01ffffff, 0)).toEqual(1); // 0xfffff === 0b[111][1111111111111111]1, var7 = 0xffff + expect(ops1.vars).toEqual([0, 1, 2, 3, 4, 5, 6, 0xffff]); + expect(ops1.onConst(0x015dffff, 0)).toEqual(1); // 0x5dfff === 0b[010][1110111111111111]1, var2 = 0xefff + expect(ops1.vars).toEqual([0, 1, 0xefff, 3, 4, 5, 6, 0xffff]); + expect(ops1.onConst(0x0100ffff, 0)).toEqual(1); // 0x00fff === 0b[000][0000011111111111]1, var0 = 0x07ff + expect(ops1.vars).toEqual([0x07ff, 1, 0xefff, 3, 4, 5, 6, 0xffff]); - it("Checking onLoop() method", () => { - let ops = new OperatorsDos([], [0, 1, 2, 3], new Observer()); - - expect(ops.onLoop(0x02ffffff, 0, {}, 2)).toEqual(2); //for(v3=v3;v3 { - let ops = new OperatorsDos([], [0, 1, 2, 3], new Observer()); - - expect(ops.onLoop(0x028fffff, 0, {}, 20)).toEqual(1); //for(v2=v0;v2 { - let ops = new OperatorsDos([], [0, 1, 2, 3], new Observer()); - - expect(ops.onLoop(0x0287ffff, 0, {}, 15)).toEqual(1); //for(v2=v0;v2 { + expect(ops.onConst(0x01ffffff, 0)).toEqual(1); // 0xfffff === 0b[111][1111111111111111]1, var7 = 0xffff + expect(ops.onConst(0x015dffff, 1)).toEqual(2); // 0x5dfff === 0b[010][1110111111111111]1, var2 = 0xefff + expect(ops.onConst(0x0100ffff, 700)).toEqual(701); // 0x00fff === 0b[000][0000011111111111]1, var0 = 0x07ff + }); }); - it("Checking onOperator() method", () => { - let ops = new OperatorsDos([], [0, 3, 1, 3], new Observer()); + describe('onCondition() method', () => { + let org; + let ops; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + + it("Checking conditions", () => { + expect(ops.onCondition(0x02ffffff, 0)).toEqual(1); //if(v3!==v3); + ops.offsets = [1]; + expect(ops.onCondition(0x021fffff, 0)).toEqual(1); //if(v0!==v1); + ops.offsets = [1]; + expect(ops.onCondition(0x021abfff, 0)).toEqual(1); //if(v0===v1); + ops.offsets = [1]; + expect(ops.onCondition(0x0213ffff, 0)).toEqual(1); //if(v0 < v1); + }); - expect(ops.onOperator(0x031a3fff, 0, {}, 1)).toEqual(1); //'v0=v1>>v2'; - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 3).toEqual(true); - expect(ops.vars[2] === 1).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); + it('Checking closing bracket offset', () => { + ops.offsets = [2]; + expect(ops.onCondition(0x02ffffff, 0)).toEqual(2); //if(v3!==v3); + ops.offsets = [1]; + expect(ops.onCondition(0x02ffffff, 0)).toEqual(1); //if(v3!==v3); + ops.offsets = [1]; + expect(ops.onCondition(0x0213ffff, 0)).toEqual(1); //if(v0 < v1); + }); - ops.destroy(); - }); - it("Checking onOperator() method 2", () => { - let ops = new OperatorsDos([], [0, 3, 1, 3], new Observer()); + it("Checking onCondition() method with 3 bits per var config", () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 3; + let ops1 = new OperatorsDos([], [0, 1, 2, 3, 4, 5, 6, 7], org); - expect(ops.onOperator(0x036c7fff, 1, {}, 1)).toEqual(2); //'v1=v2-v3'; - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === -2).toEqual(true); - expect(ops.vars[2] === 1).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); + ops.offsets = [2]; + expect(ops.onCondition(0x02ffffff, 0)).toEqual(2); //if(v3!==v3); + ops.offsets = [2]; + expect(ops.onCondition(0x021fffff, 0)).toEqual(1); //if(v0!==v7); - ops.destroy(); + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onOperator() method 3", () => { - let ops = new OperatorsDos([], [0, 3, 1, 3], new Observer()); - expect(ops.onOperator(0x036fffff, 3, 1)).toEqual(4); //'v1=v2<=v3'; - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 1).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); + describe('onLoop() method', () => { + let org; + let ops; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + + it("Checking conditions", () => { + expect(ops.onCondition(0x03ffffff, 0)).toEqual(1); //while(v3!==v3); + ops.offsets = [1]; + expect(ops.onCondition(0x031fffff, 0)).toEqual(1); //while(v0!==v1); + ops.offsets = [1]; + expect(ops.onCondition(0x031abfff, 0)).toEqual(1); //while(v0===v1); + ops.offsets = [1]; + expect(ops.onCondition(0x0313ffff, 0)).toEqual(1); //while(v0 < v1); + }); - ops.destroy(); - }); - it("Checking onOperator() method 3", () => { - let ops = new OperatorsDos([], [0, 3, 1, 3], new Observer()); + it('Checking closing bracket offset', () => { + ops.offsets = [2]; + expect(ops.onCondition(0x03ffffff, 0)).toEqual(2); //while(v3!==v3); + ops.offsets = [1]; + expect(ops.onCondition(0x03ffffff, 0)).toEqual(1); //while(v3!==v3); + ops.offsets = [1]; + expect(ops.onCondition(0x0313ffff, 0)).toEqual(1); //while(v0 < v1); + }); - expect(ops.onOperator(0x03ffffff, 7, {}, 1)).toEqual(8); //'v3=v3<=v3'; - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 3).toEqual(true); - expect(ops.vars[2] === 1).toEqual(true); - expect(ops.vars[3] === 1).toEqual(true); + it("Checking onLoop() method with 3 bits per var config", () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 3; + let ops1 = new OperatorsDos([], [0, 1, 2, 3, 4, 5, 6, 7], org); - ops.destroy(); - }); + ops.offsets = [2]; + expect(ops.onCondition(0x03ffffff, 0)).toEqual(2); //while(v3!==v3); + ops.offsets = [2]; + expect(ops.onCondition(0x031fffff, 0)).toEqual(1); //while(v0!==v7); - it("Checking onNot() method", () => { - let ops = new OperatorsDos([], [0, 1, 2, 3], new Observer()); - - expect(ops.onNot(0x041fffff, 0, {}, 1)).toEqual(1); //'v0=!v1'; - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(ops.onNot(0x046fffff, 1, {}, 1)).toEqual(2); //'v1=!v2'; - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 0).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(ops.onNot(0x04ffffff, 2, {}, 1)).toEqual(3); //'v3=!v3'; - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 0).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 0).toEqual(true); - }); - // - // it("Checking onPi() method", () => { - // let ops = new OperatorsDos([]); - // - // expect(ops.onPi(0x053fffff), 0, 1).toEqual('v0=pi'); - // expect(ops.onPi(0x057fffff), 0, 1).toEqual('v1=pi'); - // expect(ops.onPi(0x05ffffff), 0, 1).toEqual('v3=pi'); - // }); - // - // it("Checking onTrig() method", () => { - // let ops = new OperatorsDos([]); - // - // expect(ops.onTrig(0x061bffff), 0, 1).toEqual('v0=Math.tan(v1)'); - // expect(ops.onTrig(0x0663ffff), 0, 1).toEqual('v1=Math.sin(v2)'); - // expect(ops.onTrig(0x06ffffff), 0, 1).toEqual('v3=Math.abs(v3)'); - // }); - - it("Checking onLookAt() in a complex way", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - - expect(ops.onLookAt(0x071bffff, 0, {}, 1)).toEqual(1);//v0=org.lookAt(v1,v2); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - - obs.on(EVENTS.GET_ENERGY, (org, x, y, ret) => { - ret.ret = 7; - expect(x === 1 && y === 2).toEqual(true); - }); - expect(ops.onLookAt(0x071bffff, 0, {}, 1)).toEqual(1);//v0=org.lookAt(v1,v2); - expect(ops.vars[0] === 7).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - - obs.clear(); - obs.on(EVENTS.GET_ENERGY, (org, x, y, ret) => { - ret.ret = 8; - expect(x === 2 && y === 3).toEqual(true); - }); - expect(ops.onLookAt(0x076fffff, 1, {}, 1)).toEqual(2);//v1=org.lookAt(v2,v3); - expect(ops.vars[0] === 7).toEqual(true); - expect(ops.vars[1] === 8).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - - obs.clear(); - obs.on(EVENTS.GET_ENERGY, (org, x, y, ret) => { - ret.ret = 9; - expect(x === 3 && y === 3).toEqual(true); - }); - expect(ops.onLookAt(0x07ffffff, 3, {}, 1)).toEqual(4);//v3=org.lookAt(v3,v3); - expect(ops.vars[0] === 7).toEqual(true); - expect(ops.vars[1] === 8).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 9).toEqual(true); - - obs.clear(); - ops.destroy(); + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onEatLeft() method", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - let org = {x:4, y:5, energy:0}; - - obs.on(EVENTS.EAT, (org, x, y, ret) => { - expect(ret.ret === 1).toEqual(true); - ret.ret = 5; - expect(x === 3 && y === 5).toEqual(true); - }); - expect(ops.onEatLeft(0x081fffff, 0, org, 1)).toEqual(1); // v0=org.eatLeft(v1); - expect(ops.vars[0] === 5).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.energy === 5).toEqual(true); - - obs.clear(); - ops.destroy(); - }); - it("Checking onEatLeft() method 2", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - let org = {x:5, y:6, energy:0}; - - obs.on(EVENTS.EAT, (org, x, y, ret) => { - expect(ret.ret === 2).toEqual(true); - ret.ret = 5; - expect(x === 4 && y === 6).toEqual(true); - }); - expect(ops.onEatLeft(0x086fffff, 0, org, 1)).toEqual(1); // v1=org.eatLeft(v2); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 5).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.energy === 5).toEqual(true); - - obs.clear(); - ops.destroy(); - }); - it("Checking onEatLeft() method 3", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - let org = {x:3, y:4, energy:0}; - - obs.on(EVENTS.EAT, (org, x, y, ret) => { - expect(ret.ret === 3).toEqual(true); - ret.ret = 1; - expect(x === 2 && y === 4).toEqual(true); - }); - expect(ops.onEatLeft(0x08ffffff, 0, org, 1)).toEqual(1); // v3=org.eatLeft(v3); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 1).toEqual(true); - expect(org.energy === 1).toEqual(true); - - obs.clear(); - ops.destroy(); - }); + describe('onOperator() method', () => { + let org; + let ops; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + + it("Checking onOperator() method", () => { + expect(ops.onOperator(0x045a3fff, 0)).toEqual(1); //v1=v1>>v2; + expect(ops.vars).toEqual([0, 0, 2, 3]); + expect(ops.onOperator(0x046c7fff, 1)).toEqual(2); //v1=v2-v3; + expect(ops.vars).toEqual([0, -1, 2, 3]); + expect(ops.onOperator(0x046fffff, 3)).toEqual(4); //v1=v2<=v3; + expect(ops.vars).toEqual([0, 1, 2, 3]); + expect(ops.onOperator(0x04ffffff, 7)).toEqual(8); //v3=v3<=v3; + expect(ops.vars).toEqual([0, 1, 2, 1]); + expect(ops.onOperator(0x046d3fff, 0)).toEqual(1); //v1=v2%v3; + expect(ops.vars).toEqual([0, 0, 2, 1]); + + expect(ops.onOperator(0x046c3fff, 0)).toEqual(1); //v1=v2+v3; + expect(ops.vars).toEqual([0, 3, 2, 1]); + expect(ops.onOperator(0x046c7fff, 0)).toEqual(1); //v1=v2-v3; + expect(ops.vars).toEqual([0, 1, 2, 1]); + expect(ops.onOperator(0x046cbfff, 0)).toEqual(1); //v1=v2*v3; + expect(ops.vars).toEqual([0, 2, 2, 1]); + ops.vars = [0, 1, 2, 4]; + expect(ops.onOperator(0x046cffff, 0)).toEqual(1); //v1=v2/v3; + expect(ops.vars).toEqual([0, .5, 2, 4]); + ops.vars = [0, 1, 2, 3]; + expect(ops.onOperator(0x046d3fff, 0)).toEqual(1); //v1=v2%v3; + expect(ops.vars).toEqual([0, 2, 2, 3]); + }); - it("Checking onEatRight() method", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - let org = {x:4, y:5, energy:0}; - - obs.on(EVENTS.EAT, (org, x, y, ret) => { - expect(ret.ret === 1).toEqual(true); - ret.ret = 5; - expect(x === 5 && y === 5).toEqual(true); - }); - expect(ops.onEatRight(0x081fffff, 0, org, 1)).toEqual(1); // v0=org.eatRight(v1); - expect(ops.vars[0] === 5).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.energy === 5).toEqual(true); - - obs.clear(); - ops.destroy(); - }); - it("Checking onEatRight() method 2", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - let org = {x:5, y:6, energy:0}; - - obs.on(EVENTS.EAT, (org, x, y, ret) => { - expect(ret.ret === 2).toEqual(true); - ret.ret = 5; - expect(x === 6 && y === 6).toEqual(true); - }); - expect(ops.onEatRight(0x086fffff, 0, org, 1)).toEqual(1); // v1=org.eatRight(v2); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 5).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.energy === 5).toEqual(true); - - obs.clear(); - ops.destroy(); - }); - it("Checking onEatRight() method 3", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - let org = {x:3, y:4, energy:0}; - - obs.on(EVENTS.EAT, (org, x, y, ret) => { - expect(ret.ret === 3).toEqual(true); - ret.ret = 1; - expect(x === 4 && y === 4).toEqual(true); - }); - expect(ops.onEatRight(0x08ffffff, 0, org, 1)).toEqual(1); // v3=org.eatRight(v3); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 1).toEqual(true); - expect(org.energy === 1).toEqual(true); - - obs.clear(); - ops.destroy(); - }); + it('Checking onOperator() with 4 bits per var', () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); - it("Checking onEatUp() method", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - let org = {x:4, y:5, energy:0}; - - obs.on(EVENTS.EAT, (org, x, y, ret) => { - expect(ret.ret === 1).toEqual(true); - ret.ret = 5; - expect(x === 4 && y === 4).toEqual(true); - }); - expect(ops.onEatUp(0x081fffff, 0, org, 1)).toEqual(1); // v0=org.onEatUp(v1); - expect(ops.vars[0] === 5).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.energy === 5).toEqual(true); - - obs.clear(); - ops.destroy(); - }); - it("Checking onEatRight() method 2", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - let org = {x:5, y:6, energy:0}; - - obs.on(EVENTS.EAT, (org, x, y, ret) => { - expect(ret.ret === 2).toEqual(true); - ret.ret = 5; - expect(x === 5 && y === 5).toEqual(true); - }); - expect(ops.onEatUp(0x086fffff, 0, org, 1)).toEqual(1); // v1=org.onEatUp(v2); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 5).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.energy === 5).toEqual(true); - - obs.clear(); - ops.destroy(); - }); - it("Checking onEatUp() method 3", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - let org = {x:3, y:4, energy:0}; - - obs.on(EVENTS.EAT, (org, x, y, ret) => { - expect(ret.ret === 3).toEqual(true); - ret.ret = 1; - expect(x === 3 && y === 3).toEqual(true); - }); - expect(ops.onEatUp(0x08ffffff, 0, org, 1)).toEqual(1); // v3=org.onEatUp(v3); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 1).toEqual(true); - expect(org.energy === 1).toEqual(true); - - obs.clear(); - ops.destroy(); - }); + expect(ops1.onOperator(0x04ffffff, 0)).toEqual(1); //v15=v15<=v15 + expect(ops1.vars).toEqual([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,1]); + expect(ops1.onOperator(0x046ff0ff, 0)).toEqual(1); //v6=v15+v15 + expect(ops1.vars).toEqual([0,1,2,3,4,5,2,7,8,9,10,11,12,13,14,1]); - it("Checking onEatDown() method", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - let org = {x:4, y:5, energy:0}; - - obs.on(EVENTS.EAT, (org, x, y, ret) => { - expect(ret.ret === 1).toEqual(true); - ret.ret = 5; - expect(x === 4 && y === 6).toEqual(true); - }); - expect(ops.onEatDown(0x081fffff, 0, org, 1)).toEqual(1); // v0=org.onEatDown(v1); - expect(ops.vars[0] === 5).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.energy === 5).toEqual(true); - - obs.clear(); - ops.destroy(); - }); - it("Checking onEatDown() method 2", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - let org = {x:5, y:6, energy:0}; - - obs.on(EVENTS.EAT, (org, x, y, ret) => { - expect(ret.ret === 2).toEqual(true); - ret.ret = 5; - expect(x === 5 && y === 7).toEqual(true); - }); - expect(ops.onEatDown(0x086fffff, 0, org, 1)).toEqual(1); // v1=org.onEatDown(v2); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 5).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.energy === 5).toEqual(true); - - obs.clear(); - ops.destroy(); - }); - it("Checking onEatDown() method 3", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [1, 1, 2, 3], obs); - let org = {x:3, y:4, energy:0}; - - obs.on(EVENTS.EAT, (org, x, y, ret) => { - expect(ret.ret === 3).toEqual(true); - ret.ret = 1; - expect(x === 3 && y === 5).toEqual(true); - }); - expect(ops.onEatDown(0x08ffffff, 0, org, 1)).toEqual(1); // v3=org.onEatDown(v3); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 1).toEqual(true); - expect(org.energy === 1).toEqual(true); - - obs.clear(); - ops.destroy(); - }); + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); - it("Checking onStepLeft() method", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [0, 1, 2, 3], obs); - let org = {x:3, y:4}; - - obs.on(EVENTS.STEP, (org, x1, y1, x2, y2, ret) => { - ret.ret = 1; - ret.x = 2; - ret.y = 4; - expect(x1 === 3 && y1 === 4 && x2 === 2 && y2 === 4).toEqual(true); - }); - expect(ops.onStepLeft(0x081fffff, 0, org, 1)).toEqual(1); // v0=org.stepLeft(); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.x === 2 && org.y === 4).toEqual(true); - - obs.clear(); - ops.destroy(); + it("Checking overflows", () => { + const max = Number.MAX_VALUE; + + ops.vars = [0, 1, max, max]; + expect(ops.onOperator(0x046c3fff, 0)).toEqual(1); //v1=v2+v3; + expect(ops.vars).toEqual([0, max, max, max]); + ops.vars = [0, 1, -max, max]; + expect(ops.onOperator(0x046c7fff, 0)).toEqual(1); //v1=v2-v3; + expect(ops.vars).toEqual([0, -max, -max, max]); + ops.vars = [0, 1, max, max]; + expect(ops.onOperator(0x046cbfff, 0)).toEqual(1); //v1=v2*v3; + expect(ops.vars).toEqual([0, max, max, max]); + ops.vars = [0, 1, max, 0]; + expect(ops.onOperator(0x046cffff, 0)).toEqual(1); //v1=v2/v3; + expect(ops.vars).toEqual([0, max, max, 0]); + ops.vars = [0, 1, 2, 0]; + expect(ops.onOperator(0x046d3fff, 0)).toEqual(1); //v1=v2%v3; + expect(ops.vars).toEqual([0, 0, 2, 0]); + }); }); - it("Checking onStepLeft() method with no free space on the left", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [0, 1, 2, 3], obs); - let org = {x:3, y:4}; - - obs.on(EVENTS.STEP, (org, x1, y1, x2, y2, ret) => { - ret.ret = 0; - ret.x = 3; - ret.y = 4; - expect(x1 === 3 && y1 === 4 && x2 === 2 && y2 === 4).toEqual(true); - }); - expect(ops.onStepLeft(0x081fffff, 0, org, 1)).toEqual(1); // v0=org.stepLeft(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.x === 3 && org.y === 4).toEqual(true); - - obs.clear(); - ops.destroy(); + + describe('onLookAt() method', () => { + let org; + let ops; + const w = Config.worldWidth; + const h = Config.worldHeight; + + beforeEach(() => {Config.worldHeight = Config.worldWidth = 10;org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy(); Config.worldHeight = h; Config.worldWidth = w}); + + it("Checking onLookAt() is found nothing", () => { + org.on(EVENTS.GET_ENERGY, (x, y, ret) => { + expect(x).toBe(2); + expect(y).toBe(3); + ret.ret = 0; + }); + expect(ops.onLookAt(0x056fffff, 0, org)).toEqual(1); //v1=lookAt(v2,v3); + expect(ops.vars).toEqual([0,0,2,3]); + }); + + it("Checking onLookAt() looking outside of the world", () => { + ops.vars = [0, 1, 20, 30]; + expect(ops.onLookAt(0x056fffff, 0, org)).toEqual(1); //v1=lookAt(v2,v3); + expect(ops.vars).toEqual([0,0,20,30]); + + ops.vars = [0, 1, -20, -30]; + expect(ops.onLookAt(0x056fffff, 0, org)).toEqual(1); //v1=lookAt(v2,v3); + expect(ops.vars).toEqual([0,0,-20,-30]); + }); + + it('Checking onLookAt() found an energy', () => { + org.on(EVENTS.GET_ENERGY, (x, y, ret) => { + expect(x).toBe(2); + expect(y).toBe(3); + ret.ret = 13; + }); + expect(ops.onLookAt(0x056fffff, 0, org)).toEqual(1); //v1=lookAt(v2,v3); + expect(ops.vars).toEqual([0,13,2,3]); + }); + + it('Checking onLookAt() with 4 bits per var', () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,7], org); + + org.on(EVENTS.GET_ENERGY, (x, y, ret) => { + expect(x).toBe(7); + expect(y).toBe(7); + ret.ret = 13; + }); + expect(ops1.onLookAt(0x056fffff, 0, org)).toEqual(1); //v6=lookAt(v15,v15); + expect(ops1.vars).toEqual([0,1,2,3,4,5,13,7,8,9,10,11,12,13,14,7]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); + + it('Checking onLookAt() with floating coordinates', () => { + org.on(EVENTS.GET_ENERGY, (x, y, ret) => { + expect(x).toBe(0); + expect(y).toBe(4); + ret.ret = 13; + }); + ops.vars = [0, 1, .1, 3.6]; + expect(ops.onLookAt(0x056fffff, 0, org)).toEqual(1); //v1=lookAt(v2,v3); + expect(ops.vars).toEqual([0,13,.1,3.6]); + }); }); - it("Checking onStepLeft() method 2", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [0, 1, 2, 3], obs); - let org = {x:3, y:4}; - - obs.on(EVENTS.STEP, (org, x1, y1, x2, y2, ret) => { - ret.ret = 1; - ret.x = 2; - ret.y = 4; - expect(x1 === 3 && y1 === 4 && x2 === 2 && y2 === 4).toEqual(true); - }); - expect(ops.onStepLeft(0x086fffff, 0, org, 1)).toEqual(1); // v1=org.stepLeft(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.x === 2 && org.y === 4).toEqual(true); - //expect(ops.onStepLeft(0x08ffffff), 0, org, 1)).toEqual() // v3=org.stepLeft(); - - obs.clear(); - ops.destroy(); + + describe('onEatLeft() method', () => { + let org; + let ops; + const w = Config.worldWidth; + const h = Config.worldHeight; + + beforeEach(() => {Config.worldHeight = Config.worldWidth = 10;org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy(); Config.worldHeight = h; Config.worldWidth = w}); + + it("Checking eating nothing", () => { + ops.vars = [1, 0, 1, 2]; + expect(ops.onEatLeft(0x061fffff, 0, org)).toEqual(1); // v0=eatLeft(v1); + expect(ops.vars).toEqual([0, 0, 1, 2]); + }); + it("Checking eating nothing 2", () => { + org.on(EVENTS.EAT, (org, x, y, ret) => { + expect(ret.ret).toBe(1); + expect(x).toBe(1); + expect(y).toBe(3); + ret.ret = 0; + }); + org.x = 2; + org.y = 3; + expect(ops.onEatLeft(0x061fffff, 0, org)).toEqual(1); // v0=eatLeft(v1); + expect(ops.vars).toEqual([0,1,2,3]); + }); + + it("Checking eating energy", () => { + org.on(EVENTS.EAT, (org, x, y, ret) => { + expect(ret.ret).toBe(1); + expect(x).toBe(1); + expect(y).toBe(3); + ret.ret = 5; + }); + org.x = 2; + org.y = 3; + expect(ops.onEatLeft(0x061fffff, 0, org)).toEqual(1); // v0=eatLeft(v1); + expect(ops.vars).toEqual([5,1,2,3]); + }); + + it('Checking eating with 3bits per var', () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 3; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7], org); + + org.on(EVENTS.EAT, (org, x, y, ret) => { + expect(ret.ret).toBe(4); + expect(x).toBe(1); + expect(y).toBe(3); + ret.ret = 5; + }); + org.x = 2; + org.y = 3; + expect(ops1.onEatLeft(0x0633ffff, 0, org)).toEqual(1); // v1=eatLeft(v4); + expect(ops1.vars).toEqual([0,5,2,3,4,5,6,7]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }) }); - it("Checking onStepLeft() method 2 with no free space on the left", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [0, 1, 2, 3], obs); - let org = {x:3, y:4}; - - obs.on(EVENTS.STEP, (org, x1, y1, x2, y2, ret) => { - ret.ret = 0; - ret.x = 3; - ret.y = 4; - expect(x1 === 3 && y1 === 4 && x2 === 2 && y2 === 4).toEqual(true); - }); - expect(ops.onStepLeft(0x086fffff, 0, org, 1)).toEqual(1); // v1=org.stepLeft(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 0).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.x === 3 && org.y === 4).toEqual(true); - - obs.clear(); - ops.destroy(); + + describe('onEatRight() method', () => { + let org; + let ops; + const w = Config.worldWidth; + const h = Config.worldHeight; + + beforeEach(() => {Config.worldHeight = Config.worldWidth = 10;org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy(); Config.worldHeight = h; Config.worldWidth = w}); + + it("Checking eating nothing", () => { + ops.vars = [1, 0, 1, 2]; + expect(ops.onEatRight(0x071fffff, 0, org)).toEqual(1); // v0=eatRight(v1); + expect(ops.vars).toEqual([0, 0, 1, 2]); + }); + it("Checking eating nothing 2", () => { + org.on(EVENTS.EAT, (org, x, y, ret) => { + expect(ret.ret).toBe(1); + expect(x).toBe(3); + expect(y).toBe(3); + ret.ret = 0; + }); + org.x = 2; + org.y = 3; + expect(ops.onEatRight(0x071fffff, 0, org)).toEqual(1); // v0=eatRight(v1); + expect(ops.vars).toEqual([0,1,2,3]); + }); + + it("Checking eating energy", () => { + org.on(EVENTS.EAT, (org, x, y, ret) => { + expect(ret.ret).toBe(1); + expect(x).toBe(3); + expect(y).toBe(3); + ret.ret = 5; + }); + org.x = 2; + org.y = 3; + expect(ops.onEatRight(0x071fffff, 0, org)).toEqual(1); // v0=eatRight(v1); + expect(ops.vars).toEqual([5,1,2,3]); + }); + + it('Checking eating with 3bits per var', () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 3; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7], org); + + org.on(EVENTS.EAT, (org, x, y, ret) => { + expect(ret.ret).toBe(4); + expect(x).toBe(3); + expect(y).toBe(3); + ret.ret = 5; + }); + org.x = 2; + org.y = 3; + expect(ops1.onEatRight(0x0733ffff, 0, org)).toEqual(1); // v1=eatRight(v4); + expect(ops1.vars).toEqual([0,5,2,3,4,5,6,7]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }) }); - it("Checking onStepLeft() method 3", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [0, 1, 2, 3], obs); - let org = {x:3, y:4}; - - obs.on(EVENTS.STEP, (org, x1, y1, x2, y2, ret) => { - ret.ret = 1; - ret.x = 2; - ret.y = 4; - expect(x1 === 3 && y1 === 4 && x2 === 2 && y2 === 4).toEqual(true); - }); - expect(ops.onStepLeft(0x08ffffff, 0, org, 1)).toEqual(1); // v3=org.stepLeft(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 1).toEqual(true); - expect(org.x === 2 && org.y === 4).toEqual(true); - - obs.clear(); - ops.destroy(); + + describe('onEatUp() method', () => { + let org; + let ops; + const w = Config.worldWidth; + const h = Config.worldHeight; + + beforeEach(() => {Config.worldHeight = Config.worldWidth = 10;org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy(); Config.worldHeight = h; Config.worldWidth = w}); + + it("Checking eating nothing", () => { + ops.vars = [1, 0, 1, 2]; + expect(ops.onEatUp(0x081fffff, 0, org)).toEqual(1); // v0=eatUp(v1); + expect(ops.vars).toEqual([0, 0, 1, 2]); + }); + it("Checking eating nothing 2", () => { + org.on(EVENTS.EAT, (org, x, y, ret) => { + expect(ret.ret).toBe(1); + expect(x).toBe(2); + expect(y).toBe(2); + ret.ret = 0; + }); + org.x = 2; + org.y = 3; + expect(ops.onEatUp(0x081fffff, 0, org)).toEqual(1); // v0=eatUp(v1); + expect(ops.vars).toEqual([0,1,2,3]); + }); + + it("Checking eating energy", () => { + org.on(EVENTS.EAT, (org, x, y, ret) => { + expect(ret.ret).toBe(1); + expect(x).toBe(2); + expect(y).toBe(2); + ret.ret = 5; + }); + org.x = 2; + org.y = 3; + expect(ops.onEatUp(0x081fffff, 0, org)).toEqual(1); // v0=eatUp(v1); + expect(ops.vars).toEqual([5,1,2,3]); + }); + + it('Checking eating with 3bits per var', () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 3; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7], org); + + org.on(EVENTS.EAT, (org, x, y, ret) => { + expect(ret.ret).toBe(4); + expect(x).toBe(2); + expect(y).toBe(2); + ret.ret = 5; + }); + org.x = 2; + org.y = 3; + expect(ops1.onEatUp(0x0833ffff, 0, org)).toEqual(1); // v1=eatUp(v4); + expect(ops1.vars).toEqual([0,5,2,3,4,5,6,7]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }) }); - it("Checking onStepLeft() method 3 with no free space on the left", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [0, 1, 2, 3], obs); - let org = {x:3, y:4}; - - obs.on(EVENTS.STEP, (org, x1, y1, x2, y2, ret) => { - ret.ret = 0; - ret.x = 3; - ret.y = 4; - expect(x1 === 3 && y1 === 4 && x2 === 2 && y2 === 4).toEqual(true); - }); - expect(ops.onStepLeft(0x08ffffff, 0, org, 1)).toEqual(1); // v3=org.stepLeft(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 0).toEqual(true); - expect(org.x === 3 && org.y === 4).toEqual(true); - - obs.clear(); - ops.destroy(); + + describe('onEatDown() method', () => { + let org; + let ops; + const w = Config.worldWidth; + const h = Config.worldHeight; + + beforeEach(() => {Config.worldHeight = Config.worldWidth = 10;org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy(); Config.worldHeight = h; Config.worldWidth = w}); + + it("Checking eating nothing", () => { + ops.vars = [1, 0, 1, 2]; + expect(ops.onEatDown(0x091fffff, 0, org)).toEqual(1); // v0=eatDown(v1); + expect(ops.vars).toEqual([0, 0, 1, 2]); + }); + it("Checking eating nothing 2", () => { + org.on(EVENTS.EAT, (org, x, y, ret) => { + expect(ret.ret).toBe(1); + expect(x).toBe(2); + expect(y).toBe(4); + ret.ret = 0; + }); + org.x = 2; + org.y = 3; + expect(ops.onEatDown(0x091fffff, 0, org)).toEqual(1); // v0=eatDown(v1); + expect(ops.vars).toEqual([0,1,2,3]); + }); + + it("Checking eating energy", () => { + org.on(EVENTS.EAT, (org, x, y, ret) => { + expect(ret.ret).toBe(1); + expect(x).toBe(2); + expect(y).toBe(4); + ret.ret = 5; + }); + org.x = 2; + org.y = 3; + expect(ops.onEatDown(0x091fffff, 0, org)).toEqual(1); // v0=eatDown(v1); + expect(ops.vars).toEqual([5,1,2,3]); + }); + + it('Checking eating with 3bits per var', () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 3; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7], org); + + org.on(EVENTS.EAT, (org, x, y, ret) => { + expect(ret.ret).toBe(4); + expect(x).toBe(2); + expect(y).toBe(4); + ret.ret = 5; + }); + org.x = 2; + org.y = 3; + expect(ops1.onEatDown(0x0933ffff, 0, org)).toEqual(1); // v1=eatDown(v4); + expect(ops1.vars).toEqual([0,5,2,3,4,5,6,7]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }) }); - it("Checking onStepRight() method", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [0, 1, 2, 3], obs); - let org = {x:3, y:4}; - - obs.on(EVENTS.STEP, (org, x1, y1, x2, y2, ret) => { - ret.ret = 1; - ret.x = 4; - ret.y = 4; - expect(x1 === 3 && y1 === 4 && x2 === 4 && y2 === 4).toEqual(true); - }); - expect(ops.onStepRight(0x081fffff, 0, org, 1)).toEqual(1); // v0=org.stepRight(); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.x === 4 && org.y === 4).toEqual(true); - - obs.clear(); - ops.destroy(); + describe('onStepLeft() method', () => { + let org; + let ops; + const w = Config.worldWidth; + const h = Config.worldHeight; + + beforeEach(() => {Config.worldHeight = Config.worldWidth = 10;org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy(); Config.worldHeight = h; Config.worldWidth = w}); + + it("Checking step left", () => { + org.x = 3; + org.y = 4; + org.on(EVENTS.STEP, (org, x1, y1, x2, y2) => { + org.x = x2; + org.y = y2; + expect(x1 === 3 && y1 === 4 && x2 === 2 && y2 === 4).toBe(true); + }); + expect(ops.onStepLeft(0x0a1fffff, 0, org)).toEqual(1); // v0=stepLeft(); + expect(ops.vars).toEqual([2,1,2,3]); + expect(org.x).toBe(2); + expect(org.y).toBe(4); + }); + + it("Checking step left with no free space on the left", () => { + org.x = 3; + org.y = 4; + org.on(EVENTS.STEP, (org, x1, y1, x2, y2) => { + expect(x1 === 3 && y1 === 4 && x2 === 2 && y2 === 4).toBe(true); + }); + expect(ops.onStepLeft(0x0a1fffff, 0, org)).toEqual(1); // v0=stepLeft(); + expect(ops.vars).toEqual([0,1,2,3]); + expect(org.x).toBe(3); + expect(org.y).toBe(4); + }); + + it("Checking step left with 4 bits per var", () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); + + org.x = 3; + org.y = 4; + org.on(EVENTS.STEP, (org, x1, y1, x2, y2) => { + org.x = x2; + org.y = y2; + expect(x1 === 3 && y1 === 4 && x2 === 2 && y2 === 4).toBe(true); + }); + expect(ops1.onStepLeft(0x0a1fffff, 0, org)).toEqual(1); // v1=stepLeft(); + expect(ops1.vars).toEqual([0,2,2,3,4,5,6,7,8,9,10,11,12,13,14,15]); + expect(org.x).toBe(2); + expect(org.y).toBe(4); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onStepRight() method with no free space on the left", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [0, 1, 2, 3], obs); - let org = {x:3, y:4}; - - obs.on(EVENTS.STEP, (org, x1, y1, x2, y2, ret) => { - ret.ret = 0; - ret.x = 3; - ret.y = 4; - expect(x1 === 3 && y1 === 4 && x2 === 4 && y2 === 4).toEqual(true); - }); - expect(ops.onStepRight(0x081fffff, 0, org, 1)).toEqual(1); // v0=org.stepRight(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.x === 3 && org.y === 4).toEqual(true); - - obs.clear(); - ops.destroy(); + + describe('onStepRight() method', () => { + let org; + let ops; + const w = Config.worldWidth; + const h = Config.worldHeight; + + beforeEach(() => {Config.worldHeight = Config.worldWidth = 10;org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy(); Config.worldHeight = h; Config.worldWidth = w}); + + it("Checking step right", () => { + org.x = 3; + org.y = 4; + org.on(EVENTS.STEP, (org, x1, y1, x2, y2) => { + org.x = x2; + org.y = y2; + expect(x1 === 3 && y1 === 4 && x2 === 4 && y2 === 4).toBe(true); + }); + expect(ops.onStepRight(0x0a1fffff, 0, org)).toEqual(1); // v0=stepRight(); + expect(ops.vars).toEqual([4,1,2,3]); + expect(org.x).toBe(4); + expect(org.y).toBe(4); + }); + + it("Checking step right with no free space on the right", () => { + org.x = 3; + org.y = 4; + org.on(EVENTS.STEP, (org, x1, y1, x2, y2) => { + expect(x1 === 3 && y1 === 4 && x2 === 4 && y2 === 4).toBe(true); + }); + expect(ops.onStepRight(0x0a1fffff, 0, org)).toEqual(1); // v0=stepRight(); + expect(ops.vars).toEqual([0,1,2,3]); + expect(org.x).toBe(3); + expect(org.y).toBe(4); + }); + + it("Checking step right with 4 bits per var", () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); + + org.x = 3; + org.y = 4; + org.on(EVENTS.STEP, (org, x1, y1, x2, y2) => { + org.x = x2; + org.y = y2; + expect(x1 === 3 && y1 === 4 && x2 === 4 && y2 === 4).toBe(true); + }); + expect(ops1.onStepRight(0x0a1fffff, 0, org)).toEqual(1); // v1=stepRight(); + expect(ops1.vars).toEqual([0,4,2,3,4,5,6,7,8,9,10,11,12,13,14,15]); + expect(org.x).toBe(4); + expect(org.y).toBe(4); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onStepRight() method 2", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [0, 1, 2, 3], obs); - let org = {x:3, y:4}; - - obs.on(EVENTS.STEP, (org, x1, y1, x2, y2, ret) => { - ret.ret = 1; - ret.x = 4; - ret.y = 4; - expect(x1 === 3 && y1 === 4 && x2 === 4 && y2 === 4).toEqual(true); - }); - expect(ops.onStepRight(0x086fffff, 0, org, 1)).toEqual(1); // v1=org.stepRight(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.x === 4 && org.y === 4).toEqual(true); - - obs.clear(); - ops.destroy(); + + describe('onStepUp() method', () => { + let org; + let ops; + const w = Config.worldWidth; + const h = Config.worldHeight; + + beforeEach(() => {Config.worldHeight = Config.worldWidth = 10;org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy(); Config.worldHeight = h; Config.worldWidth = w}); + + it("Checking step up", () => { + org.x = 3; + org.y = 4; + org.on(EVENTS.STEP, (org, x1, y1, x2, y2) => { + org.x = x2; + org.y = y2; + expect(x1 === 3 && y1 === 4 && x2 === 3 && y2 === 3).toBe(true); + }); + expect(ops.onStepUp(0x0a1fffff, 0, org)).toEqual(1); // v0=stepUp(); + expect(ops.vars).toEqual([3,1,2,3]); + expect(org.x).toBe(3); + expect(org.y).toBe(3); + }); + + it("Checking step up with no free space on above", () => { + org.x = 3; + org.y = 4; + org.on(EVENTS.STEP, (org, x1, y1, x2, y2) => { + expect(x1 === 3 && y1 === 4 && x2 === 3 && y2 === 3).toBe(true); + }); + expect(ops.onStepUp(0x0a1fffff, 0, org)).toEqual(1); // v0=stepUp(); + expect(ops.vars).toEqual([0,1,2,3]); + expect(org.x).toBe(3); + expect(org.y).toBe(4); + }); + + it("Checking step up with 4 bits per var", () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); + + org.x = 3; + org.y = 4; + org.on(EVENTS.STEP, (org, x1, y1, x2, y2) => { + org.x = x2; + org.y = y2; + expect(x1 === 3 && y1 === 4 && x2 === 3 && y2 === 3).toBe(true); + }); + expect(ops1.onStepUp(0x0a1fffff, 0, org)).toEqual(1); // v1=stepUp(); + expect(ops1.vars).toEqual([0,3,2,3,4,5,6,7,8,9,10,11,12,13,14,15]); + expect(org.x).toBe(3); + expect(org.y).toBe(3); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onStepRight() method 2 with no free space on the left", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [0, 1, 2, 3], obs); - let org = {x:3, y:4}; - - obs.on(EVENTS.STEP, (org, x1, y1, x2, y2, ret) => { - ret.ret = 0; - ret.x = 3; - ret.y = 4; - expect(x1 === 3 && y1 === 4 && x2 === 4 && y2 === 4).toEqual(true); - }); - expect(ops.onStepRight(0x086fffff, 0, org, 1)).toEqual(1); // v1=org.stepRight(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 0).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(org.x === 3 && org.y === 4).toEqual(true); - - obs.clear(); - ops.destroy(); + + describe('onStepDown() method', () => { + let org; + let ops; + const w = Config.worldWidth; + const h = Config.worldHeight; + + beforeEach(() => {Config.worldHeight = Config.worldWidth = 10;org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy(); Config.worldHeight = h; Config.worldWidth = w}); + + it("Checking step down", () => { + org.x = 3; + org.y = 4; + org.on(EVENTS.STEP, (org, x1, y1, x2, y2) => { + org.x = x2; + org.y = y2; + expect(x1 === 3 && y1 === 4 && x2 === 3 && y2 === 5).toBe(true); + }); + expect(ops.onStepDown(0x0a1fffff, 0, org)).toEqual(1); // v0=stepDown(); + expect(ops.vars).toEqual([5,1,2,3]); + expect(org.x).toBe(3); + expect(org.y).toBe(5); + }); + + it("Checking step down with no free space below", () => { + org.x = 3; + org.y = 4; + org.on(EVENTS.STEP, (org, x1, y1, x2, y2) => { + expect(x1 === 3 && y1 === 4 && x2 === 3 && y2 === 5).toBe(true); + }); + expect(ops.onStepDown(0x0a1fffff, 0, org)).toEqual(1); // v0=stepDown(); + expect(ops.vars).toEqual([0,1,2,3]); + expect(org.x).toBe(3); + expect(org.y).toBe(4); + }); + + it("Checking step down with 4 bits per var", () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); + + org.x = 3; + org.y = 4; + org.on(EVENTS.STEP, (org, x1, y1, x2, y2) => { + org.x = x2; + org.y = y2; + expect(x1 === 3 && y1 === 4 && x2 === 3 && y2 === 5).toBe(true); + }); + expect(ops1.onStepDown(0x0a1fffff, 0, org)).toEqual(1); // v1=stepDown(); + expect(ops1.vars).toEqual([0,5,2,3,4,5,6,7,8,9,10,11,12,13,14,15]); + expect(org.x).toBe(3); + expect(org.y).toBe(5); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onStepRight() method 3", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [0, 1, 2, 3], obs); - let org = {x:3, y:4}; - - obs.on(EVENTS.STEP, (org, x1, y1, x2, y2, ret) => { - ret.ret = 1; - ret.x = 4; - ret.y = 4; - expect(x1 === 3 && y1 === 4 && x2 === 4 && y2 === 4).toEqual(true); - }); - expect(ops.onStepRight(0x08ffffff, 0, org, 1)).toEqual(1); // v3=org.stepRight(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 1).toEqual(true); - expect(org.x === 4 && org.y === 4).toEqual(true); - - obs.clear(); - ops.destroy(); + + describe('onFromMem() method', () => { + let org; + let ops; + let mbits; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + beforeAll(() => {mbits = OConfig.orgMemBits; OConfig.orgMemBits = 2}); + afterAll(() => OConfig.orgMemBits = mbits); + + it("Checking getting value by constant", () => { + org.mem.splice(0, org.mem.length, ...[1,2,3,4]); + expect(ops.onFromMem(0x0b10ffff, 0, org)).toEqual(1); //v0=fromMem(); + expect(ops.vars).toEqual([1,1,2,3]); + + org.mem.splice(0, org.mem.length, ...[0,1,2,3]); + expect(ops.onFromMem(0x0b50ffff, 0, org)).toEqual(1); //v1=fromMem(); + expect(ops.vars).toEqual([1,0,2,3]); + }); + + it("Checking getting value by variable value", () => { + org.mem.splice(0, org.mem.length, ...[1,2,3,4]); + expect(ops.onFromMem(0x0b1fffff, 0, org)).toEqual(1); //v0=fromMem(); + expect(ops.vars).toEqual([2,1,2,3]); + + org.mem.splice(0, org.mem.length, ...[0,1,2,3]); + expect(ops.onFromMem(0x0b58ffff, 0, org)).toEqual(1); //v1=fromMem(); + expect(ops.vars).toEqual([2,1,2,3]); + }); + + it("Checking getting value by variable floating value", () => { + ops.vars.splice(0, ops.vars.length, ...[.1,3.2,.3,.4]); + org.mem.splice(0, org.mem.length, ...[1,2,3,4]); + expect(ops.onFromMem(0x0b1fffff, 0, org)).toEqual(1); //v0=fromMem(); + expect(ops.vars).toEqual([4,3.2,.3,.4]); + + ops.vars.splice(0, ops.vars.length, ...[.1,3.2,.3,.4]); + org.mem.splice(0, org.mem.length, ...[0,1,2,3]); + expect(ops.onFromMem(0x0b58ffff, 0, org)).toEqual(1); //v1=fromMem(); + expect(ops.vars).toEqual([.1,3,.3,.4]); + }); + + it("Checking getting value by variable value with 4 bits per var", () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); + + org.mem.splice(0, org.mem.length, ...[1,2,3,4]); + expect(ops1.onFromMem(0x0b0fffff, 0, org)).toEqual(1); //v1=fromMem(); + expect(ops1.vars).toEqual([1,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]); + + org.mem.splice(0, org.mem.length, ...[0,7,2,3]); + expect(ops1.onFromMem(0x0b51ffff, 0, org)).toEqual(1); //v5=fromMem(); + expect(ops1.vars).toEqual([1,1,2,3,4,7,6,7,8,9,10,11,12,13,14,15]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onStepRight() method 3 with no free space on the left", () => { - let obs = new Observer(EVENT_AMOUNT); - let ops = new OperatorsDos([], [0, 1, 2, 3], obs); - let org = {x:3, y:4}; - - obs.on(EVENTS.STEP, (org, x1, y1, x2, y2, ret) => { - ret.ret = 0; - ret.x = 3; - ret.y = 4; - expect(x1 === 3 && y1 === 4 && x2 === 4 && y2 === 4).toEqual(true); - }); - expect(ops.onStepRight(0x08ffffff, 0, org, 1)).toEqual(1); // v3=org.stepRight(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 0).toEqual(true); - expect(org.x === 3 && org.y === 4).toEqual(true); - - obs.clear(); - ops.destroy(); + + describe('onToMem() method', () => { + let org; + let ops; + let mbits; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([1], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + beforeAll(() => {mbits = OConfig.orgMemBits; OConfig.orgMemBits = 2}); + afterAll(() => OConfig.orgMemBits = mbits); + + it("Checking setting value by constant", () => { + org.mem.splice(0, org.mem.length, ...[1,2,3,4]); + expect(ops.onToMem(0x0b17ffff, 0, org)).toEqual(1); //toMem(v0, 3); + expect(org.mem).toEqual([1,2,3,0]); + }); + + it("Checking setting value by variable value", () => { + org.mem.splice(0, org.mem.length, ...[0,2,3,4]); + expect(ops.onToMem(0x0b1fffff, 0, org)).toEqual(1); //toMem(v0, v0); + expect(org.mem).toEqual([1,2,3,4]); + }); + + it("Checking setting value by variable value with 4 bits per var", () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); + + org.mem.splice(0, org.mem.length, ...[0,2,3,4]); + expect(ops1.onToMem(0x0b1fffff, 0, org)).toEqual(1); //toMem(v0, v0); + expect(org.mem).toEqual([0,15,3,4]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onFromMem() method", () => { - let org = {mem: [1,2,3]}; - let ops = new OperatorsDos([], [0, 1, 2, 3], new Observer()); - - expect(ops.onFromMem(0x081fffff, 0, org, 1)).toEqual(1); //v0=org.fromMem(); - expect(ops.vars[0] === 3).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(ops.onFromMem(0x086fffff, 1, org, 2)).toEqual(2); //v1=org.fromMem(); - expect(ops.vars[0] === 3).toEqual(true); - expect(ops.vars[1] === 2).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(ops.onFromMem(0x08ffffff, 2, org, 3)).toEqual(3); //v3=org.fromMem(); - expect(ops.vars[0] === 3).toEqual(true); - expect(ops.vars[1] === 2).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 1).toEqual(true); - - ops.destroy(); + describe('onMyX() method', () => { + let org; + let ops; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + + it("Checking simple values", () => { + org.x = 1; + expect(ops.onMyX(0x0c1fffff, 0, org)).toEqual(1); // v0=myX() + expect(ops.vars).toEqual([1,1,2,3]); + org.x = 3; + expect(ops.onMyX(0x0c6fffff, 0, org)).toEqual(1); // v1=myX() + expect(ops.vars).toEqual([1,3,2,3]); + org.x = 0; + expect(ops.onMyX(0x0cffffff, 0, org)).toEqual(1); // v3=myX() + expect(ops.vars).toEqual([1,3,2,0]); + }); + + it('Checking simple values with 4 bits per var', () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); + + org.x = 3; + expect(ops1.onMyX(0x0c1fffff, 0, org)).toEqual(1); // v1=myX() + expect(ops1.vars).toEqual([0,3,2,3,4,5,6,7,8,9,10,11,12,13,14,15]); + org.x = 3; + expect(ops1.onMyX(0x0c6fffff, 0, org)).toEqual(1); // v6=myX() + expect(ops1.vars).toEqual([0,3,2,3,4,5,3,7,8,9,10,11,12,13,14,15]); + org.x = 0; + expect(ops1.onMyX(0x0cffffff, 0, org)).toEqual(1); // v15=myX() + expect(ops1.vars).toEqual([0,3,2,3,4,5,3,7,8,9,10,11,12,13,14,0]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onFromMem() method without memory", () => { - let org = {mem: []}; - let ops = new OperatorsDos([], [7, 1, 2, 3], new Observer()); - expect(ops.onFromMem(0x081fffff, 0, org, 1)).toEqual(1); //v0=org.fromMem(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); + describe('onMyY() method', () => { + let org; + let ops; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + + it("Checking simple values", () => { + org.y = 1; + expect(ops.onMyY(0x0c1fffff, 0, org)).toEqual(1); // v0=myY() + expect(ops.vars).toEqual([1,1,2,3]); + org.y = 3; + expect(ops.onMyY(0x0c6fffff, 0, org)).toEqual(1); // v1=myY() + expect(ops.vars).toEqual([1,3,2,3]); + org.y = 0; + expect(ops.onMyY(0x0cffffff, 0, org)).toEqual(1); // v3=myY() + expect(ops.vars).toEqual([1,3,2,0]); + }); - ops.destroy(); + it('Checking simple values with 4 bits per var', () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); + + org.y = 3; + expect(ops1.onMyY(0x0c1fffff, 0, org)).toEqual(1); // v1=myX() + expect(ops1.vars).toEqual([0,3,2,3,4,5,6,7,8,9,10,11,12,13,14,15]); + org.y = 3; + expect(ops1.onMyY(0x0c6fffff, 0, org)).toEqual(1); // v6=myX() + expect(ops1.vars).toEqual([0,3,2,3,4,5,3,7,8,9,10,11,12,13,14,15]); + org.y = 0; + expect(ops1.onMyY(0x0cffffff, 0, org)).toEqual(1); // v15=myX() + expect(ops1.vars).toEqual([0,3,2,3,4,5,3,7,8,9,10,11,12,13,14,0]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onToMem() method", () => { - let org = {mem: []}; - let ops = new OperatorsDos([], [0, 1, 2, 3], new Observer()); + describe('onCheckLeft() method', () => { + let org; + let ops; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + + it('Checks left, but nothing there', () => { + org.on(EVENTS.CHECK_AT, (x, y, ret) => { + expect(x).toBe(0); + expect(y).toBe(2); + ret.ret = 0; + }); + org.x = 1; + org.y = 2; + expect(ops.onCheckLeft(0x0c7fffff, 0, org)).toEqual(1); // v1=checkLeft() + expect(ops.vars).toEqual([0,0,2,3]); + }); - expect(ops.onToMem(0x08ffffff, 0, org, 1)).toEqual(1); //'v3 = org.toMem(v3)'); - expect(org.mem[0]).toEqual(3); - expect(ops.onToMem(0x086fffff, 0, org, 1)).toEqual(1); //'v1 = org.toMem(v2)'); - expect(org.mem[1]).toEqual(2); - expect(ops.onToMem(0x081fffff, 0, org, 1)).toEqual(1); //'v0 = org.toMem(v1)'); - expect(org.mem[2]).toEqual(2); + it('Checks left and energy there', () => { + org.on(EVENTS.CHECK_AT, (x, y, ret) => { + expect(x).toBe(0); + expect(y).toBe(2); + ret.ret = 9; + }); + org.x = 1; + org.y = 2; + expect(ops.onCheckLeft(0x0c7fffff, 1, org)).toEqual(2); // v1=checkLeft() + expect(ops.vars).toEqual([0,9,2,3]); + }); - ops.destroy(); + it('Checks left with 4 bits per var', () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); + + org.on(EVENTS.CHECK_AT, (x, y, ret) => { + expect(x).toBe(0); + expect(y).toBe(2); + ret.ret = 9; + }); + org.x = 1; + org.y = 2; + expect(ops1.onCheckLeft(0x0c7fffff, 1, org)).toEqual(2); // v7=checkLeft() + expect(ops1.vars).toEqual([0,1,2,3,4,5,6,9,8,9,10,11,12,13,14,15]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onMyX() method", () => { - let org = {x: 1, y:2}; - let ops = new OperatorsDos([], [0, 7, 2, 3], new Observer()); - - expect(ops.onMyX(0x081fffff, 0, org, 1)).toEqual(1); // v0=org.myX(); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 7).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(ops.onMyX(0x086fffff, 0, org, 1)).toEqual(1); // v1=org.myX(); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(ops.onMyX(0x08ffffff, 0, org, 1)).toEqual(1); // v3=org.myX(); - expect(ops.vars[0] === 1).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 1).toEqual(true); - - ops.destroy(); - }); - it("Checking onMyX() method", () => { - let org = {x: 1, y:2}; - let ops = new OperatorsDos([], [0, 7, 2, 3], new Observer()); - - expect(ops.onMyY(0x081fffff, 0, org, 1)).toEqual(1); // v0=org.myY(); - expect(ops.vars[0] === 2).toEqual(true); - expect(ops.vars[1] === 7).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(ops.onMyY(0x086fffff, 0, org, 1)).toEqual(1); // v1=org.myY(); - expect(ops.vars[0] === 2).toEqual(true); - expect(ops.vars[1] === 2).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - expect(ops.onMyY(0x08ffffff, 0, org, 1)).toEqual(1); // v3=org.myY(); - expect(ops.vars[0] === 2).toEqual(true); - expect(ops.vars[1] === 2).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 2).toEqual(true); - - ops.destroy(); - }); + describe('onCheckRight() method', () => { + let org; + let ops; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + + it('Checks right, but nothing there', () => { + org.on(EVENTS.CHECK_AT, (x, y, ret) => { + expect(x).toBe(2); + expect(y).toBe(2); + ret.ret = 0; + }); + org.x = 1; + org.y = 2; + expect(ops.onCheckRight(0x0c7fffff, 0, org)).toEqual(1); // v1=checkRight() + expect(ops.vars).toEqual([0,0,2,3]); + }); + + it('Checks right and energy there', () => { + org.on(EVENTS.CHECK_AT, (x, y, ret) => { + expect(x).toBe(2); + expect(y).toBe(2); + ret.ret = 9; + }); + org.x = 1; + org.y = 2; + expect(ops.onCheckRight(0x0c7fffff, 1, org)).toEqual(2); // v1=checkRight() + expect(ops.vars).toEqual([0,9,2,3]); + }); - it("Checking onCheckLeft() method", () => { - let org = new Observer(EVENT_AMOUNT); - let obs = new Observer(); - let ops = new OperatorsDos([], [1, 7, 2, 3], obs); - - org.x = 1; - org.y = 2; - - org.on(EVENTS.CHECK_AT, (x, y, ret) => { - expect(x === 0 && y === 2).toEqual(true); - ret.ret = 0; - }); - expect(ops.onCheckLeft(0x081fffff, 0, org)).toEqual(1); // v0=org.onCheckLeft(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 7).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - - org.clear(); - org.on(EVENTS.CHECK_AT, (x, y, ret) => { - expect(x === 0 && y === 2).toEqual(true); - ret.ret = 1; - }); - expect(ops.onCheckLeft(0x086fffff, 0, org)).toEqual(1); // v1=org.onCheckLeft(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - - org.clear(); - org.on(EVENTS.CHECK_AT, (x, y, ret) => { - expect(x === 0 && y === 2).toEqual(true); - ret.ret = 2; - }); - expect(ops.onCheckLeft(0x08ffffff, 0, org)).toEqual(1); // v3=org.onCheckLeft(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 2).toEqual(true); - - ops.destroy(); + it('Checks right with 4 bits per var', () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); + + org.on(EVENTS.CHECK_AT, (x, y, ret) => { + expect(x).toBe(2); + expect(y).toBe(2); + ret.ret = 9; + }); + org.x = 1; + org.y = 2; + expect(ops1.onCheckRight(0x0c7fffff, 1, org)).toEqual(2); // v7=checkRight() + expect(ops1.vars).toEqual([0,1,2,3,4,5,6,9,8,9,10,11,12,13,14,15]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onCheckRight() method", () => { - let org = new Observer(EVENT_AMOUNT); - let obs = new Observer(); - let ops = new OperatorsDos([], [1, 7, 2, 3], obs); - - org.x = 1; - org.y = 2; - - org.on(EVENTS.CHECK_AT, (x, y, ret) => { - expect(x === 2 && y === 2).toEqual(true); - ret.ret = 0; - }); - expect(ops.onCheckRight(0x081fffff, 0, org)).toEqual(1); // v0=org.onCheckRight(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 7).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - - org.clear(); - org.on(EVENTS.CHECK_AT, (x, y, ret) => { - expect(x === 2 && y === 2).toEqual(true); - ret.ret = 1; - }); - expect(ops.onCheckRight(0x086fffff, 0, org)).toEqual(1); // v1=org.onCheckRight(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - - org.clear(); - org.on(EVENTS.CHECK_AT, (x, y, ret) => { - expect(x === 2 && y === 2).toEqual(true); - ret.ret = 2; - }); - expect(ops.onCheckRight(0x08ffffff, 0, org)).toEqual(1); // v3=org.onCheckRight(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 2).toEqual(true); - - ops.destroy(); + describe('onCheckUp() method', () => { + let org; + let ops; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + + it('Checks up, but nothing there', () => { + org.on(EVENTS.CHECK_AT, (x, y, ret) => { + expect(x).toBe(1); + expect(y).toBe(1); + ret.ret = 0; + }); + org.x = 1; + org.y = 2; + expect(ops.onCheckUp(0x0c7fffff, 0, org)).toEqual(1); // v1=checkUp() + expect(ops.vars).toEqual([0,0,2,3]); + }); + + it('Checks up and energy there', () => { + org.on(EVENTS.CHECK_AT, (x, y, ret) => { + expect(x).toBe(1); + expect(y).toBe(1); + ret.ret = 9; + }); + org.x = 1; + org.y = 2; + expect(ops.onCheckUp(0x0c7fffff, 1, org)).toEqual(2); // v1=checkUp() + expect(ops.vars).toEqual([0,9,2,3]); + }); + + it('Checks up with 4 bits per var', () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); + + org.on(EVENTS.CHECK_AT, (x, y, ret) => { + expect(x).toBe(1); + expect(y).toBe(1); + ret.ret = 9; + }); + org.x = 1; + org.y = 2; + expect(ops1.onCheckUp(0x0c7fffff, 1, org)).toEqual(2); // v7=checkUp() + expect(ops1.vars).toEqual([0,1,2,3,4,5,6,9,8,9,10,11,12,13,14,15]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onCheckUp() method", () => { - let org = new Observer(EVENT_AMOUNT); - let obs = new Observer(); - let ops = new OperatorsDos([], [1, 7, 2, 3], obs); - - org.x = 1; - org.y = 2; - - org.on(EVENTS.CHECK_AT, (x, y, ret) => { - expect(x === 1 && y === 1).toEqual(true); - ret.ret = 0; - }); - expect(ops.onCheckUp(0x081fffff, 0, org)).toEqual(1); // v0=org.onCheckUp(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 7).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - - org.clear(); - org.on(EVENTS.CHECK_AT, (x, y, ret) => { - expect(x === 1 && y === 1).toEqual(true); - ret.ret = 1; - }); - expect(ops.onCheckUp(0x086fffff, 0, org)).toEqual(1); // v1=org.onCheckUp(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - - org.clear(); - org.on(EVENTS.CHECK_AT, (x, y, ret) => { - expect(x === 1 && y === 1).toEqual(true); - ret.ret = 2; - }); - expect(ops.onCheckUp(0x08ffffff, 0, org)).toEqual(1); // v3=org.onCheckUp(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 2).toEqual(true); - - ops.destroy(); + describe('onCheckDown() method', () => { + let org; + let ops; + + beforeEach(() => {org = new OrganismDos('0', 0, 0, {}); ops = new OperatorsDos([], [0, 1, 2, 3], org)}); + afterEach (() => {ops.destroy(); org.destroy()}); + + it('Checks down, but nothing there', () => { + org.on(EVENTS.CHECK_AT, (x, y, ret) => { + expect(x).toBe(1); + expect(y).toBe(3); + ret.ret = 0; + }); + org.x = 1; + org.y = 2; + expect(ops.onCheckDown(0x0c7fffff, 0, org)).toEqual(1); // v1=checkDown() + expect(ops.vars).toEqual([0,0,2,3]); + }); + + it('Checks down and energy there', () => { + org.on(EVENTS.CHECK_AT, (x, y, ret) => { + expect(x).toBe(1); + expect(y).toBe(3); + ret.ret = 9; + }); + org.x = 1; + org.y = 2; + expect(ops.onCheckDown(0x0c7fffff, 1, org)).toEqual(2); // v1=checkDown() + expect(ops.vars).toEqual([0,9,2,3]); + }); + + it('Checks right with 4 bits per var', () => { + let bpv = OConfig.codeBitsPerVar; + OConfig.codeBitsPerVar = 4; + let ops1 = new OperatorsDos([1], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], org); + + org.on(EVENTS.CHECK_AT, (x, y, ret) => { + expect(x).toBe(1); + expect(y).toBe(3); + ret.ret = 9; + }); + org.x = 1; + org.y = 2; + expect(ops1.onCheckDown(0x0c7fffff, 1, org)).toEqual(2); // v7=checkDown() + expect(ops1.vars).toEqual([0,1,2,3,4,5,6,9,8,9,10,11,12,13,14,15]); + + OConfig.codeBitsPerVar = bpv; + ops1.destroy(); + }); }); - it("Checking onCheckDown() method", () => { - let org = new Observer(EVENT_AMOUNT); - let obs = new Observer(); - let ops = new OperatorsDos([], [1, 7, 2, 3], obs); - - org.x = 1; - org.y = 2; - - org.on(EVENTS.CHECK_AT, (x, y, ret) => { - expect(x === 1 && y === 3).toEqual(true); - ret.ret = 0; - }); - expect(ops.onCheckDown(0x081fffff, 0, org)).toEqual(1); // v0=org.onCheckDown(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 7).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - - org.clear(); - org.on(EVENTS.CHECK_AT, (x, y, ret) => { - expect(x === 1 && y === 3).toEqual(true); - ret.ret = 1; - }); - expect(ops.onCheckDown(0x086fffff, 0, org)).toEqual(1); // v1=org.onCheckDown(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 3).toEqual(true); - - org.clear(); - org.on(EVENTS.CHECK_AT, (x, y, ret) => { - expect(x === 1 && y === 3).toEqual(true); - ret.ret = 2; - }); - expect(ops.onCheckDown(0x08ffffff, 0, org)).toEqual(1); // v3=org.onCheckDown(); - expect(ops.vars[0] === 0).toEqual(true); - expect(ops.vars[1] === 1).toEqual(true); - expect(ops.vars[2] === 2).toEqual(true); - expect(ops.vars[3] === 2).toEqual(true); - - ops.destroy(); + describe('Checks complex DOS scripts for validness', () => { + const newWeights = [.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1]; + const weights = OConfig.orgOperatorWeights.slice(); + let ocfg; + let org; + const script = (code) => { + THelper.script(org.vm, code); + OConfig.codeYieldPeriod = code.length; + }; + + + beforeEach(() => { + ocfg = new ConfigHelper(OConfig); + ocfg.set('codeYieldPeriod', 2); + ocfg.set('codeBitsPerBlock', 8); + ocfg.set('codeBitsPerOperator', 8); + ocfg.set('codeBitsPerVar', 2); + ocfg.set('codeConstBits', 16); + ocfg.set('orgMemBits', 8); + OConfig.orgOperatorWeights.splice(0, OConfig.orgOperatorWeights.length, ...newWeights); + + org = new OrganismDos('0', 0, 0, {}); + _fill(org.vm.vars, 0); + org.energy = 100; + }); + afterEach (() => { + org.destroy(); + ocfg.reset(); + OConfig.orgOperatorWeights.splice(0, OConfig.orgOperatorWeights.length, ...weights); + }); + + /** + * if (v0 === v1) { // true + * v3 = 0x7fff + * } + */ + it('if should go inside the block, if condition is true', () => { + script([0x021abfff, 0x01dfffff]); + org.vm.run(org); + expect(org.vm.vars).toEqual([0,0,0,0x7fff]); + }); + /** + * if (v0 === v1) {} // true + * v3 = 0x7fff + */ + it('if without body should go to the next row, if condition is true', () => { + script([0x021803ff, 0x01dfffff]); + org.vm.run(org); + expect(org.vm.vars).toEqual([0,0,0,0x7fff]); + }); + /** + * v0 = 1 + * if (v0 === v1) {} // true + * v0 = 0 + */ + it('Checks if operator without body and with other code around', () => { + script([0x0100007f, 0x021803ff, 0x0100003f]); + org.vm.run(org); + expect(org.vm.vars).toEqual([0,0,0,0]); + }); + /** + * if (v0 === v1) { // true + * if (v0 === v1) {} // true + * v0 = 1 + * } + * v1 = 1 + */ + it('Checks if inside if with true condition', () => { + script([ + '10 00 01 10 00000010 1111111111', + '10 00 01 10 00000001 1111111111', + '01 00 0000000000000001 111111', + '01 01 0000000000000001 111111' + ]); + org.vm.run(org); + expect(org.vm.vars).toEqual([1,1,0,0]); + }); + /** + * if (v0 !== v1) { // false + * if (v0 === v1) {} // true + * v0 = 1 + * } + * v1 = 1 + */ + it('Checks if inside if with false condition outside', () => { + script([ + '10 00 01 11 00000010 1111111111', + '10 00 01 10 00000001 1111111111', + '01 00 0000000000000001 111111', + '01 01 0000000000000001 111111' + ]); + org.vm.run(org); + expect(org.vm.vars).toEqual([0,1,0,0]); + }); + /** + * if (v0 !== v1) { // false + * if (v0 === v1) { // true + * v0 = 3 + * } + * v0 = 2 + * } + * v1 = 1 + */ + it('Checks if inside if with false condition outside 2', () => { + script([ + '10 00 01 11 00000011 1111111111', + '10 00 01 10 00000010 1111111111', + '01 00 0000000000000011 111111', + '01 00 0000000000000010 111111', + '01 01 0000000000000001 111111' + ]); + OConfig.codeYieldPeriod = 2; + org.vm.run(org); + expect(org.vm.vars).toEqual([0,1,0,0]); + }); + /** + * if (v0 === v1) { // true + * if (v0 === v1) { // true + * v0 = 3 + * } + * v0 = 2 + * } + * v1 = 1 + */ + it('Checks if inside if with true condition and 3 assignments', () => { + script([ + '10 00 01 10 00000011 1111111111', + '10 00 01 10 00000010 1111111111', + '01 00 0000000000000011 111111', + '01 00 0000000000000010 111111', + '01 01 0000000000000001 111111' + ]); + org.vm.run(org); + expect(org.vm.vars).toEqual([2,1,0,0]); + }); + /** + * if (v0 === v1) { // true + * v0 = 2 + * if (v0 === v1) {} // false + * } + * v1 = 1 + */ + it('Checks if inside if with var assign', () => { + script([ + '10 00 01 10 00000010 1111111111', + '01 00 0000000000000010 111111', + '10 00 01 10 00000010 1111111111', + '01 01 0000000000000001 111111' + ]); + org.vm.run(org); + expect(org.vm.vars).toEqual([2,1,0,0]); + }); + /** + * if (v0 === v1) { // true + * if (v0 === v1) { // true + * v0 = 2 + * } + * } + * v1 = 1 + */ + it('Checks if inside if with both true conditions', () => { + script([ + '10 00 01 10 00000010 1111111111', + '10 00 01 10 00000001 1111111111', + '01 00 0000000000000010 111111', + '01 01 0000000000000001 111111' + ]); + org.vm.run(org); + expect(org.vm.vars).toEqual([2,1,0,0]); + }); + + + /** + * while (v0 === v1) { // true + * v0 = 1 + * } + * v1 = 1 + */ + it('Checks while with true and then false conditions', () => { + script([ + '11 00 01 10 00000001 1111111111', + '01 00 0000000000000001 111111', + '01 01 0000000000000001 111111' + ]); + OConfig.codeYieldPeriod = 4; + org.vm.run(org); + expect(org.vm.vars).toEqual([1,1,0,0]); + }); + /** + * while (v0 !== v1) {} // false + * v0 = 1 + */ + it('Checks while with false condition', () => { + script([ + '11 00 01 11 00000000 1111111111', + '01 00 0000000000000001 111111' + ]); + org.vm.run(org); + expect(org.vm.vars).toEqual([1,0,0,0]); + }); + /** + * if (v0 === v1) { + * while (v0 === v1) { // true + * v0 = 1 + * } + * } + * v1 = 1 + */ + it('Checks while with if outside', () => { + script([ + '10 00 01 10 00000010 1111111111', + '11 00 01 10 00000001 1111111111', + '01 00 0000000000000001 111111', + '01 01 0000000000000001 111111' + ]); + OConfig.codeYieldPeriod = 5; + org.vm.run(org); + expect(org.vm.vars).toEqual([1,1,0,0]); + }); + /** + * while (v0 === v1) { + * while (v0 === v1) { // true + * v0 = 1 + * } + * } + * v1 = 1 + */ + it('Checks 2 whiles', () => { + script([ + '11 00 01 10 00000010 1111111111', + '11 00 01 10 00000001 1111111111', + '01 00 0000000000000001 111111', + '01 01 0000000000000001 111111' + ]); + OConfig.codeYieldPeriod = 6; + org.vm.run(org); + expect(org.vm.vars).toEqual([1,1,0,0]); + }); }); }); \ No newline at end of file diff --git a/client/src/manager/plugins/organisms/dos/Organism.js b/client/src/manager/plugins/organisms/dos/Organism.js index 536daa9..db11544 100644 --- a/client/src/manager/plugins/organisms/dos/Organism.js +++ b/client/src/manager/plugins/organisms/dos/Organism.js @@ -4,7 +4,7 @@ * TODO: - * @author flatline */ -const Organism = require('./../Organism'); +const Organism = require('./../Organism').Organism; const Operators = require('./Operators'); class OrganismDos extends Organism { @@ -14,19 +14,16 @@ class OrganismDos extends Organism { * @param {String} id Unique identifier of organism * @param {Number} x Unique X coordinate * @param {Number} y Unique Y coordinate - * @param {Boolean} alive true if organism is alive * @param {Object} item Reference to the Queue item, where * this organism is located - * @param {Function} codeEndCb Callback, which is called at the - * end of every code iteration. * @param {Organism} parent Parent organism if cloning is needed */ - constructor(id, x, y, alive, item, codeEndCb, parent = null) { - super(id, x, y, alive, item, codeEndCb, Operators, parent); + constructor(id, x, y, item, parent = null) { + super(id, x, y, item, Operators, parent); } onRun() { - this.jsvm.run(this); + return this.vm.run(this); } } diff --git a/client/src/manager/plugins/organisms/dos/OrganismSpec.js b/client/src/manager/plugins/organisms/dos/OrganismSpec.js index 9e0c848..460d1b0 100644 --- a/client/src/manager/plugins/organisms/dos/OrganismSpec.js +++ b/client/src/manager/plugins/organisms/dos/OrganismSpec.js @@ -2,200 +2,269 @@ // This spec covers two classes "Organism" and "OrganismDos" // describe("client/src/organism/OrganismDos", () => { - let OrganismDos = require('./Organism'); - let Config = require('./../../../../share/Config').Config; - let OConfig = require('./../../../../manager/plugins/organisms/Config'); - let api = require('./../../../../share/Config').api; - let THelper = require('./../../../../../../common/tests/Helper'); - let cls; - - it("Checking organism creation", () => { - let org = new OrganismDos(0, 1, 2, true, null, ()=>{}); - - expect(org.id).toEqual(0); - expect(org.x).toEqual(1); - expect(org.y).toEqual(2); - expect(org.item).toEqual(null); - expect(org.alive).toEqual(true); - expect(THelper.compare(org.mutationProbs, OConfig.orgMutationProbs)).toEqual(true); - expect(org.mutationPeriod === OConfig.orgRainMutationPeriod).toEqual(true); - expect(org.mutationPercent === OConfig.orgRainMutationPercent).toEqual(true); - expect(org.cloneMutationPercent === OConfig.orgCloneMutationPercent).toEqual(true); - expect(org.changes === 1).toEqual(true); - expect(org.energy === OConfig.orgStartEnergy).toEqual(true); - expect(org.color === OConfig.orgStartColor).toEqual(true); - expect(org.mem.length === 0).toEqual(true); - expect(org.cloneEnergyPercent === OConfig.orgCloneEnergyPercent).toEqual(true); - expect(org.iterations === 0).toEqual(true); - - org.destroy(); + const _fill = require('lodash/fill'); + const OrganismDos = require('./Organism'); + const OEvents = require('./../Organism').EVENTS; + const OConfig = require('./../../../../manager/plugins/organisms/Config'); + const Helper = require('./../../../../../../common/src/Helper'); + + let org; + + beforeEach(() => org = new OrganismDos('0', 1, 2, null)); + afterEach (() => org.destroy()); + + describe('Organism creation', () => { + it("Checking organism creation", () => { + expect(org.id).toEqual('0'); + expect(org.x).toEqual(1); + expect(org.y).toEqual(2); + expect(org.item).toEqual(null); + expect(org.energy > 0).toEqual(true); + expect(org.mutationProbs).toEqual(OConfig.orgMutationProbs); + expect(org.mutationProbs !== OConfig.orgMutationProbs).toEqual(true); + expect(org.mutationPeriod === OConfig.orgRainMutationPeriod).toEqual(true); + expect(org.mutationPercent === OConfig.orgRainMutationPercent).toEqual(true); + expect(org.mem.length).toBe(Math.pow(2, OConfig.orgMemBits)); + expect(org.mem).toEqual(_fill(new Array(Math.pow(2, OConfig.orgMemBits)), 0)); + expect(org.changes).toBe(0); + expect(org.energy).toBe(OConfig.orgStartEnergy); + expect(org.iterations).toBe(-1); + }); + + it("Checking organism creation from parent", () => { + const memSize = OConfig.orgMemBits; + const parent = new OrganismDos('1', 3, 4, null); + + OConfig.orgMemBits = 2; + parent.vm.insertLine(); + parent.energy = 123; + parent.changes = 0xaabbcc; + parent.mutationProbs.splice(0, parent.mutationProbs.length, ...[5,8,1,10,1,2,32,7]); + parent.mutationPeriod = 145; + parent.mutationPercent = 0.2; + parent.mem.splice(0, parent.mem.length, ...[1,2,4,3]); + + let org1 = new OrganismDos('0', 1, 2, null, parent); + + expect(org1.vm.code).toEqual(parent.vm.code); + expect(org1.vm.size).toEqual(parent.vm.size); + expect(org1.energy).toEqual(parent.energy); + expect(org1.color).toEqual(parent.color); + expect(org1.mutationProbs).toEqual(parent.mutationProbs); + expect(org1.mutationPeriod).toEqual(parent.mutationPeriod); + expect(org1.mutationPercent).toEqual(parent.mutationPercent); + expect(org1.mem).toEqual(parent.mem); + expect(org1.changes).toEqual(0); + expect(org1.iterations).toEqual(-1); + expect(org1.energy > 0).toEqual(true); + expect(org1.item).toEqual(null); + + org1.destroy(); + OConfig.orgMemBits = memSize; + }); }); - it("Checking organism creation from parent", () => { - const parent = new OrganismDos(1, 3, 4, true, null, ()=>{}); - parent.jsvm.insertLine(); - parent.energy = 123; - parent.changes = 0xaabbcc; - parent._mutationProbs = [5,8,1,10,1,2,32,7]; - parent.cloneMutationPercent = 0.1; - parent.mutationPeriod = 145; - parent.mutationPercent = 0.2; - parent.cloneEnergyPercent = 0.34; - parent._mem = [1,2,4,3]; - - let org = new OrganismDos(0, 1, 2, true, null, ()=>{}, parent); - - expect(org.jsvm.code[0] === parent.jsvm.code[0]).toEqual(true); - expect(org.jsvm.size === parent.jsvm.size).toEqual(true); - expect(org.energy === parent.energy).toEqual(true); - expect(org.color === parent.color).toEqual(true); - expect(THelper.compare(org.mutationProbs, parent.mutationProbs)).toEqual(true); - expect(org.cloneMutationPercent === parent.cloneMutationPercent).toEqual(true); - expect(org.mutationPeriod === parent.mutationPeriod).toEqual(true); - expect(org.mutationPercent === parent.mutationPercent).toEqual(true); - expect(org.cloneEnergyPercent === parent.cloneEnergyPercent).toEqual(true); - expect(THelper.compare(org.mem, parent.mem)).toEqual(true); - expect(org.changes === 1).toEqual(true); - expect(org.iterations === 0).toEqual(true); - - org.destroy(); + describe('Checking organism alive', () => { + it("Organism should not be dead it he doesn't contain code", () => { + const period = OConfig.orgAlivePeriod; + const energy = OConfig.orgStartEnergy; + OConfig.orgAlivePeriod = 100; + OConfig.orgStartEnergy = 100; + const org1 = new OrganismDos('0', 1, 2, null); + + expect(org1.energy).toBe(100); + org1.run(); + expect(org1.energy).toBe(100); + org1.destroy(); + expect(org1.energy < 1).toBe(true); + + OConfig.orgAlivePeriod = period; + OConfig.orgStartEnergy = energy; + }); + + it("Organism should not be dead after loosing some energy", () => { + const period = OConfig.orgAlivePeriod; + const energy = OConfig.orgStartEnergy; + const weights = OConfig.orgOperatorWeights.slice(); + const yieldPeriod = OConfig.codeYieldPeriod; + const newWeights = [.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1]; + OConfig.orgAlivePeriod = 100; + OConfig.orgStartEnergy = 100; + OConfig.orgOperatorWeights.splice(0, OConfig.orgOperatorWeights.length, ...newWeights); + OConfig.codeYieldPeriod = 1; + const org1 = new OrganismDos('0', 1, 2, null); + _fill(org1.vm.vars, 0); + org1.vm.insertLine(); + + expect(org1.energy).toBe(100); + org1.run(); + expect(org1.energy).toBe(99.9); // 100 - .1 = 99.9 + org1.run(); + expect(org1.energy).toBe(99.80000000000001); // 99.9 - .1 = 99.8 + org1.destroy(); + expect(org1.energy < 1).toBe(true); + + OConfig.orgAlivePeriod = period; + OConfig.orgStartEnergy = energy; + OConfig.orgOperatorWeights.splice(0, OConfig.orgOperatorWeights.length, ...weights); + OConfig.codeYieldPeriod = yieldPeriod; + }); + + it("Organism should not be dead if loosing energy is turned off", () => { + const period = OConfig.orgAlivePeriod; + const energy = OConfig.orgStartEnergy; + OConfig.orgAlivePeriod = 100; + OConfig.orgStartEnergy = 100; + const org1 = new OrganismDos('0', 1, 2, null); + + expect(org1.energy).toBe(100); + org1.run(); + expect(org1.energy).toBe(100); + org1.run(); + expect(org1.energy).toBe(100); + org1.destroy(); + expect(org1.energy < 1).toBe(true); + + OConfig.orgAlivePeriod = period; + OConfig.orgStartEnergy = energy; + }); + + it("Organism should be destroyed on run if energy is set to zero", () => { + const period = OConfig.orgAlivePeriod; + const energy = OConfig.orgStartEnergy; + OConfig.orgAlivePeriod = 100; + OConfig.orgStartEnergy = 100; + const org1 = new OrganismDos('0', 1, 2, null); + + expect(org1.energy).toBe(100); + org1.energy = 0; + org1.run(); + expect(org1.vm).toBe(null); + org1.run(); + + OConfig.orgAlivePeriod = period; + OConfig.orgStartEnergy = energy; + }); }); - it("Checking organism coordinates", () => { - let org = new OrganismDos(0, 1, 2, true, null, ()=>{}); - - org.x = 4; - org.y = 5; - expect(org.x === 4 && org.y === 5).toEqual(true); - org.x = 0; - org.y = 0; - expect(org.x === 0 && org.y === 0).toEqual(true); - org.x = -1; - org.y = -2; - expect(org.x === -1 && org.y === -2).toEqual(true); - - org.destroy(); + describe('Changes check', () => { + it("Checking organism changes", () => { + expect(org.changes).toBe(0); + org.changes = 10; + expect(org.changes).toBe(10); + org.changes += 12; + expect(org.changes).toBe(22); + }); + + it("Checking if changes affects color", () => { + const color = org.color; + expect(org.color).toBe(color); + org.energy++; + expect(org.color).toBe(color); + }); }); - it("Checking if organism if alive", () => { - let org = new OrganismDos(0, 1, 2, true, null, ()=>{}); - const period = OConfig.orgAlivePeriod; - const energy = OConfig.orgStartEnergy; - const speriod = OConfig.orgEnergySpendPeriod; - - OConfig.orgAlivePeriod = 100; - OConfig.orgStartEnergy = 100; - OConfig.orgEnergySpendPeriod = 100; - - expect(org.alive).toEqual(true); - org.run(); - expect(org.alive).toEqual(true); - - expect(org.alive).toEqual(true); - OConfig.orgAlivePeriod = period; - OConfig.orgStartEnergy = energy; - OConfig.orgEnergySpendPeriod = speriod; - - org.destroy(); - }); - - it("Checking organism changes", () => { - let org = new OrganismDos(0, 1, 2, true, null, ()=>{}); - - expect(org.changes).toEqual(1); - org.changes = 10; - expect(org.changes).toEqual(10); - org.changes += 12; - expect(org.changes).toEqual(22); - - org.destroy(); - }); - - it("Checking run() method", () => { - let org = new OrganismDos(0, 1, 2, true, null, ()=>{}); - - expect(org.iterations).toEqual(0); - org.run(); - expect(org.iterations).toEqual(1); - org.run(); - expect(org.iterations).toEqual(2); - - org.destroy(); + describe('Checks coordinates', () => { + it('posId() should return unique hash', () => { + org.x = 2; + org.y = 3; + expect(org.posId).toBe(Helper.posId(2, 3)); + org.x = 0; + org.y = 3; + expect(org.posId).toBe(Helper.posId(0, 3)); + }); + + it('Checks coordinates setters getters', () => { + org.x = 1; + org.y = 2; + expect(org.x).toBe(1); + expect(org.y).toBe(2); + org.x = 0; + org.y = 0; + expect(org.x).toBe(0); + expect(org.y).toBe(0); + org.x = -1; + org.y = -2; + expect(org.x).toBe(-1); + expect(org.y).toBe(-2); + }) }); - it("Checking organism destroy because of age", () => { - const period = OConfig.orgAlivePeriod; - let org = new OrganismDos(0, 1, 2, true, null, ()=>{}); + describe('run() method', () => { + it("Organism's age should be changed through iterations", () => { + const period = OConfig.orgAlivePeriod; + const energy = OConfig.orgStartEnergy; + OConfig.orgAlivePeriod = 100; + OConfig.orgStartEnergy = 100; - OConfig.orgAlivePeriod = 30000; - for (let i = 0; i < OConfig.orgAlivePeriod; i++) { - expect(org.alive).toEqual(true); + expect(org.iterations).toEqual(-1); org.run(); - } - expect(org.alive).toEqual(false); - // we don't need to call destroy, because organism - // should be dead at this moment - OConfig.orgAlivePeriod = period; - }); - - it("Checking organism destroy because of zero energy", () => { - const period = OConfig.orgAlivePeriod; - let org = new OrganismDos(0, 1, 2, true, null, ()=>{}); - - OConfig.orgAlivePeriod = 30000; - expect(org.energy).toEqual(OConfig.orgStartEnergy); - org.energy = 0; - expect(org.alive).toEqual(true); - org.run(); - expect(org.alive).toEqual(false); - // we don't need to call destroy, because organism - // should be dead at this moment - OConfig.orgAlivePeriod = period; - }); - - it("Checking organism destroy because of grab energy", () => { - const period = OConfig.orgEnergySpendPeriod; - let org = new OrganismDos(0, 1, 2, true, null, ()=>{}); - - OConfig.orgEnergySpendPeriod = 1; - org.energy = 1; - expect(org.alive).toEqual(true); - org.run(); - expect(org.alive).toEqual(false); - // - // we don't need to call destroy, because organism - // should be dead at this moment - // - OConfig.orgEnergySpendPeriod = period; - }); - - it("Checking grabbing energy", () => { - let org = new OrganismDos(0, 1, 2, true, null, ()=>{}); - const energy = org.energy; - - org.grabEnergy(10); - expect(org.energy).toEqual(energy - 10); - - org.destroy(); + expect(org.iterations).toEqual(0); + org.run(); + expect(org.iterations).toEqual(1); + + OConfig.orgAlivePeriod = period; + OConfig.orgStartEnergy = energy; + }); + + it("Checking organism destroy because of age", () => { + const period = OConfig.orgAlivePeriod; + + OConfig.orgAlivePeriod = 3000; + for (let i = 0; i < OConfig.orgAlivePeriod + 1; i++) { + expect(org.energy > 0).toBe(true); + org.run(); + } + expect(org.energy < 1).toEqual(true); + // we don't need to call destroy, because organism + // should be dead at this moment + OConfig.orgAlivePeriod = period; + }); }); - it("Checking organism color change", () => { - let org = new OrganismDos(0, 1, 2, true, null, ()=>{}); - const color = org.color; - - org.changes = 10; - expect(org.color).toEqual(color + 10); - - org.destroy(); + describe('Clonning', () => { + it('Organism should fire CLONE event if enough age', () => { + let flag = false; + const minAge = OConfig.orgCloneMinAge; + const minEnergy = OConfig.orgCloneMinEnergy; + const yieldPeriod = OConfig.codeYieldPeriod; + const weights = OConfig.orgOperatorWeights.slice(); + const newWeights = [.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1]; + OConfig.orgCloneMinAge = 1; + OConfig.orgCloneMinEnergy = 1; + OConfig.codeYieldPeriod = 1; + OConfig.orgOperatorWeights.splice(0, OConfig.orgOperatorWeights.length, ...newWeights); + const org1 = new OrganismDos('0', 1, 2, null); + + _fill(org1.vm.vars, 0); + org1.vm.insertLine(); + org1.energy = 100; + org1.on(OEvents.CLONE, () => flag = true); + org1.run(); + expect(flag).toBe(false); + org1.run(); + expect(flag).toBe(false); + org1.run(); + expect(flag).toBe(true); + + org1.destroy(); + OConfig.orgCloneMinAge = minAge; + OConfig.orgCloneMinEnergy = minEnergy; + OConfig.codeYieldPeriod = yieldPeriod; + OConfig.orgOperatorWeights.splice(0, OConfig.orgOperatorWeights.length, ...weights); + }) }); it("Checking destroy() method", () => { - let org = new OrganismDos(0, 1, 2, true, null, ()=>{}); - - expect(org.alive).toEqual(true); - expect(org.energy > 0).toEqual(true); - org.destroy(); - expect(org.alive).toEqual(false); - expect(org.energy > 0).toEqual(false); + const org1 = new OrganismDos('0', 1, 2, null); + + expect(org1.energy > 0).toEqual(true); + org1.destroy(); + expect(org1.vm).toBe(null); + expect(org1.energy).toBe(0); + expect(org1.item).toBe(null); + expect(org1.mem).toBe(null); + expect(org1.mutationProbs).toBe(null); + expect(org1.iterations).toBe(-1); }); }); \ No newline at end of file diff --git a/client/src/manager/plugins/organisms/dos/Organisms.js b/client/src/manager/plugins/organisms/dos/Organisms.js index dff7351..659788d 100644 --- a/client/src/manager/plugins/organisms/dos/Organisms.js +++ b/client/src/manager/plugins/organisms/dos/Organisms.js @@ -4,34 +4,43 @@ * * Events od Manager: * TODO: - * ORGANISM(org) Fires after one organism has processed * * Depends on: * manager/Manager * * @author flatline */ -const BaseOrganisms = require('./../Organisms'); -const Organism = require('./Organism'); -const Config = require('./../../../../share/Config').Config; -const OConfig = require('./../Config'); -const EVENTS = require('./../../../../share/Events').EVENTS; -const Helper = require('./../../../../../../common/src/Helper'); -const DIR = require('./../../../../../../common/src/Directions').DIR; - -const EMPTY = 0; -const ENERGY = 1; -const ORGANISM = 2; +const BaseOrganisms = require('./../Organisms'); +const Organism = require('./Organism'); +const Config = require('./../../../../share/Config').Config; +const OConfig = require('./../Config'); +const EVENTS = require('./../../../../share/Events').EVENTS; +const Helper = require('./../../../../../../common/src/Helper'); +const DIR = require('./../../../../../../common/src/Directions').DIR; +/** + * {Number} World object types + */ +const EMPTY = 0; +const ENERGY = 1; +const ORGANISM = 2; +const OBJECT = 3; +/** + * {Function} Is created to speed up this function call. constants are run + * much faster, then Helper.normalize() + */ +const NORMALIZE = Helper.normalize; +const NORMALIZE_NO_DIR = Helper.normalizeNoDir; class Organisms extends BaseOrganisms { constructor(manager) { super(manager); this._onStepInCb = this._onStepIn.bind(this); - this.manager.on(EVENTS.STEP_IN, this._onStepInCb); + + this.parent.on(EVENTS.STEP_IN, this._onStepInCb); } destroy() { - this.manager.off(EVENTS.STEP_IN, this._onStepInCb); + this.parent.off(EVENTS.STEP_IN, this._onStepInCb); this._onStepInCb = null; super.destroy(); } @@ -53,7 +62,7 @@ class Organisms extends BaseOrganisms { * @override */ onBeforeClone(org) { - return org.energy > 0; + return org.energy > 1; } /** @@ -63,9 +72,26 @@ class Organisms extends BaseOrganisms { * @override */ onClone(org, child) { - let energy = (((org.energy * org.cloneEnergyPercent) + 0.5) << 1) >>> 1; // analog of Math.round() - org.grabEnergy(energy); - child.grabEnergy(child.energy - energy); + const orgEnergy = org.energy; + const childEnergy = child.energy; + // + // Clone percent is always 0.5 + // + let energy = (orgEnergy * .5 + .5) << 0; // analog of Math.round() + // + // This is very special/rare case, when organisms cheating by creating + // ancestors and put all energy into them at the same time resetting + // their iterations property and make them immortal + // + if (energy === orgEnergy) { + energy--; + } + orgEnergy <= energy && this.parent.fire(EVENTS.KILL_CLONE, org); + org.energy -= energy; + childEnergy <= (childEnergy - energy) && this.parent.fire(EVENTS.KILL_CLONE, child); + child.energy -= (childEnergy - energy); + org.energy > 0 && (org.startEnergy = org.energy); + child.energy > 0 && (child.startEnergy = child.energy); } addOrgHandlers(org) { @@ -86,85 +112,66 @@ class Organisms extends BaseOrganisms { return new Organism(...args); } - /** - * Is called after organism has created - * @param {Organism} org - * @override - */ - onAfterCreateOrg(org) { - this.manager.positions[org.posId] = org; - } - - /** - * Is called after organism has killed - * @param {Organism} org Killed organism - * @override - */ - onAfterKillOrg(org) { - delete this.manager.positions[org.posId]; - } - - /** - * Is called after moving of organism is done. Updates this.manager.positions - * map with a new position of organism - * @param {Number} x1 Start X position - * @param {Number} y1 Start Y position - * @param {Number} x2 End X position - * @param {Number} y2 End Y position - * @param {Organism} org Organism, which is moving - * @returns {Boolean} - * @override - */ - onAfterMove(x1, y1, x2, y2, org) { - if (x1 !== x2 || y1 !== y2) { - delete this.manager.positions[Helper.posId(x1, y1)]; - this.manager.positions[Helper.posId(x2, y2)] = org; - } - - return true; - } - - _onGetEnergy(org, x, y, ret) { - if (x < 0 || y < 0 || !Number.isInteger(x) || !Number.isInteger(y)) {return} - const posId = Helper.posId(x, y); - - if (typeof(this.manager.positions[posId]) === 'undefined') { - ret.ret = this.manager.world.getDot(x, y) + _onGetEnergy(x, y, ret) { + if (this.positions[x][y] <= 0) { + ret.ret = this.world.getDot(x, y) } else { - ret.ret = this.manager.positions[posId].energy; + ret.ret = this.positions[x][y].energy; } } _onEat(org, x, y, ret) { - const world = this.manager.world; - const positions = this.manager.positions; - let dir; - - [x, y, dir] = Helper.normalize(x, y); - - const posId = Helper.posId(x, y); - if (typeof(positions[posId]) === 'undefined') { - ret.ret = world.grabDot(x, y, ret.ret); + const eat = ret.ret; + [x, y] = NORMALIZE_NO_DIR(x, y); + const victimOrg = this.positions[x][y]; + // + // World object found. We can't eat objects + // + if (victimOrg < 0) {ret.ret = 0; return} + // + // Energy found + // + if (victimOrg === 0) { + if (eat >= 0) { + ret.ret = this.world.grabDot(x, y, eat); + this.parent.fire(EVENTS.EAT_ENERGY, ret.ret); + } else { + ret.ret = eat; + this.world.setDot(x, y, (((-eat + .5) << 0) >>> 0) + this.world.getDot(x, y)); + this.parent.fire(EVENTS.PUT_ENERGY, -eat); + } + return; + } + // + // Organism found + // + ret.ret = eat < 0 ? 0 : (eat > victimOrg.energy ? victimOrg.energy : eat); + if (victimOrg.energy <= ret.ret) { + this.parent.fire(EVENTS.KILL_EAT, victimOrg); + // + // IMPORTANT: + // We have to do destroy here, to have a possibility for current + // (winner) organism to clone himself after eating other organism. + // This is how organisms compete for an ability to clone + // + victimOrg.destroy(); } else { - ret.ret = ret.ret < 0 ? 0 : (ret.ret > positions[posId].energy ? positions[posId].energy : ret.ret); - positions[posId].grabEnergy(ret.ret); + this.parent.fire(EVENTS.EAT_ORG, victimOrg, ret.ret); + victimOrg.energy -= ret.ret; } } - _onStep(org, x1, y1, x2, y2, ret) { - if (org.alive === false) {return} - const man = this.manager; + _onStep(org, x1, y1, x2, y2) { + if (org.energy < 1) {return} + const man = this.parent; let dir; - [x2, y2, dir] = Helper.normalize(x2, y2); + [x2, y2, dir] = NORMALIZE(x2, y2); // // Organism has moved, but still is within the current world (client) // if (dir === DIR.NO) { - ret.x = x2; - ret.y = y2; - ret.ret = +this.move(x1, y1, x2, y2, org); - return; + return this.move(x1, y1, x2, y2, org); } // // Current organism try to move out of the world. @@ -177,6 +184,7 @@ class Organisms extends BaseOrganisms { org.x = x2; org.y = y2; man.fire(EVENTS.STEP_OUT, x2, y2, dir, org); + man.fire(EVENTS.KILL_STEP_OUT, org); org.x = x1; org.y = y1; org.destroy(); @@ -187,43 +195,51 @@ class Organisms extends BaseOrganisms { // activated client on that side. So this is a border for him. // In this case coordinates (x,y) should stay the same // - if (man.isDistributed() || Config.worldCyclical === false) { - ret.x = x1; - ret.y = y1; - ret.ret = +this.move(x1, y1, x1, y1, org); - return; - } + if (man.isDistributed() || Config.worldCyclical === false) {return} + // + // The world is cyclical (worldCyclical). The organism will + // appear on the other side + // + this.move(x1, y1, x2, y2, org); + } - ret.x = x2; - ret.y = y2; - ret.ret = +this.move(x1, y1, x2, y2, org); + _onCheckAt(x, y, ret) { + [x, y] = NORMALIZE_NO_DIR(x, y); + + if (this.positions[x][y] < 0) { // world object + ret.ret = OBJECT + -this.positions[x][y]; + } else if (this.positions[x][y] === 0) { // energy + ret.ret = this.world.getDot(x, y) > 0 ? ENERGY : EMPTY; + } else { // organism + ret.ret = ORGANISM; + } } /** * Is called if organism step in from the server or other client (Manager/World). * If step in position is not free or maximum organisms are in the world, then - * organism die at the moment. + * organism die at the moment. We have to set true to ret.ret, if in this world + * there is a free place for the organism. And false otherwise. * @param {Number} x Current org X position * @param {Number} y Current org Y position * @param {String} orgJson Organism's serialized json - * @private + * @param {Object} ret Return object */ - _onStepIn(x, y, orgJson) { - if (this.manager.world.isFree(x, y) && this.organisms.size < OConfig.orgMaxOrgs && this.createOrg({x, y})) { - const org = this.organisms.last.val; + _onStepIn(x, y, orgJson, ret) { + if (ret.ret = this.world.isFree(x, y) && this.organisms.size < (OConfig.orgMaxOrgs + OConfig.orgMaxOrgs * OConfig.orgStepOverflowPercent)) { + const item = this.createOrg(x, y); + if (item === false) {return} + const org = item.val; org.unserialize(orgJson); - org.grabEnergy(OConfig.orgStepEnergySpendPercent); - } - } - - _onCheckAt(x, y, ret) { - let dir; - - [x, y, dir] = Helper.normalize(x, y); - if (typeof(this.manager.positions[Helper.posId(x, y)]) === 'undefined') { - ret.ret = this.manager.world.getDot(x, y) > 0 ? ENERGY : EMPTY; - } else { - ret.ret = ORGANISM; + const energy = (org.energy * OConfig.orgStepEnergySpendPercent + .5) << 0; + // + // IMPORTANT: We have to update organism's coordinates + // + org.x = x; + org.y = y; + this.world.setDot(x, y, org.color); + org.energy <= energy && this.parent.fire(EVENTS.KILL_STEP_IN, org); + org.energy -= energy; } } } diff --git a/client/src/manager/plugins/organisms/garmin/Code2String.js b/client/src/manager/plugins/organisms/garmin/Code2String.js index 1cfeaeb..034b478 100644 --- a/client/src/manager/plugins/organisms/garmin/Code2String.js +++ b/client/src/manager/plugins/organisms/garmin/Code2String.js @@ -5,7 +5,7 @@ * * @author flatline */ -const Num = require('./../../../../jsvm/Num'); +const Num = require('./../../../../vm/Num'); /** * {Function} Just a shortcuts @@ -26,23 +26,20 @@ class Code2String { this._offsets = []; /** * {Object} These operator handlers should return string representation - * of numeric based byte jsvm. + * of numeric based byte vm. */ this._OPERATORS_CB = { 0 : this._onVar.bind(this), 1 : this._onCondition.bind(this), - //2 : this._onLoop.bind(this), 2 : this._onOperator.bind(this), 3 : this._onNot.bind(this), - //4 : this._onPi.bind(this), - //5 : this._onTrig.bind(this), 4 : this._onFromMem.bind(this), 5 : this._onToMem.bind(this) }; this._OPERATORS_CB_LEN = Object.keys(this._OPERATORS_CB).length; /** * {Array} Available conditions for if operator. Amount should be - * the same like (1 << BITS_PER_VAR) + * the same like (1 << Num.BITS_PER_VAR) */ this._CONDITIONS = ['<', '>', '==', '!=']; /** @@ -51,9 +48,8 @@ class Code2String { this._OPERATORS = [ '+', '-', '*', '/', '%', '&', '|', '^', '>>', '<<', '>>>', '<', '>', '==', '!=', '<=' ]; - this._TRIGS = ['sin', 'cos', 'tan', 'abs']; - Num.setOperatorAmount(this._OPERATORS_CB_LEN); + Num.init(this._OPERATORS_CB_LEN); // // API of the Manager for accessing outside. (e.g. from Console) // @@ -65,7 +61,6 @@ class Code2String { this._OPERATORS_CB = null; this._CONDITIONS = null; this._OPERATORS = null; - this._TRIGS = null; } format(code, separator = '\n') { @@ -78,7 +73,7 @@ class Code2String { for (let i = 0; i < len; i++) { operator = operators[Num.getOperator(code[i])](code[i], i, len); // - // This jsvm is used for closing blocks for if, for and other + // This vm is used for closing blocks for if, for and other // blocked operators. // if (offsets[offsets.length - 1] === i && offsets.length > 0) { @@ -97,13 +92,13 @@ class Code2String { /** * Parses variable operator. Format: let = const|number. Num bits format: - * BITS_PER_OPERATOR bits - operator id - * BITS_PER_VAR bits - destination var index - * BITS_PER_VAR bits - assign type (const (half of bits) or variable (half of bits)) - * BITS_PER_VAR bits - variable index or all bits till the end for constant + * Num.BITS_PER_OPERATOR bits - operator id + * Num.BITS_PER_VAR bits - destination var index + * Num.BITS_PER_VAR bits - assign type (const (half of bits) or variable (half of bits)) + * Num.BITS_PER_VAR bits - variable index or all bits till the end for constant * - * @param {Num} num Packed into number jsvm line - * @return {String} Parsed jsvm line string + * @param {Num} num Packed into number vm line + * @return {String} Parsed vm line string */ _onVar(num) { const var1 = VAR1(num); @@ -118,15 +113,6 @@ class Code2String { return `if(v${VAR0(num)}${this._CONDITIONS[VAR2(num)]}v${VAR1(num)}) goto(${offs})`; } -// _onLoop(num, line, lines) { -// const var0 = VAR0(num); -// const var3 = Num.getBits(num, BITS_AFTER_THREE_VARS, Num.BITS_OF_TWO_VARS); -// const index = line + var3 < lines ? line + var3 : lines - 1; -// -// this._offsets.push(index); -// return `for(v${var0}=v${VAR1(num)};v${var0} Num.getVar(n, 1); const VAR2 = (n) => Num.getVar(n, 2); const BITS_AFTER_THREE_VARS = Num.BITS_PER_OPERATOR + Num.BITS_PER_VAR * 3; -const BITS_OF_TWO_VARS = Num.BITS_OF_TWO_VARS; const IS_NUM = Helper.isNumeric; const HALF_OF_VAR = Num.MAX_VAR / 2; const CONDITION_BITS = 2; @@ -44,28 +42,26 @@ class OperatorsGarmin extends Operators { ]; /** * {Array} Available conditions for if operator. Amount should be - * the same like (1 << BITS_PER_VAR) + * the same like (1 << Num.BITS_PER_VAR) */ this._CONDITIONS = [(a,b)=>aa>b, (a,b)=>a===b, (a,b)=>a!==b]; /** * {Array} Available operators for math calculations */ this._OPERATORS = [ - (a,b)=>a+b, (a,b)=>a-b, (a,b)=>a*b, (a,b)=>a/b, (a,b)=>a%b, (a,b)=>a&b, (a,b)=>a|b, (a,b)=>a^b, (a,b)=>a>>b, (a,b)=>a<a>>>b, (a,b)=>+(a+(a>b), (a,b)=>+(a===b), (a,b)=>+(a!==b), (a,b)=>+(a<=b) + (a,b)=>a+b, (a,b)=>a-b, (a,b)=>a*b, (a,b)=>a/(b||1), (a,b)=>a%(b||1), (a,b)=>a&b, (a,b)=>a|b, (a,b)=>a^b, (a,b)=>a>>b, (a,b)=>a<a>>>b, (a,b)=>+(a+(a>b), (a,b)=>+(a===b), (a,b)=>+(a!==b), (a,b)=>+(a<=b) ]; - this._TRIGS = [(a)=>Math.sin(a), (a)=>Math.cos(a), (a)=>Math.tan(a), (a)=>Math.abs(a)]; // // We have to set amount of available operators for correct // working of mutations of operators. // - Num.setOperatorAmount(this._OPERATORS_CB.length); + Num.init(this._OPERATORS_CB.length); } destroy() { this._OPERATORS_CB = null; this._CONDITIONS = null; this._OPERATORS = null; - this._TRIGS = null; super.destroy(); } @@ -74,25 +70,25 @@ class OperatorsGarmin extends Operators { /** * Parses variable operator. Format: let = const|number. Num bits format: - * BITS_PER_OPERATOR bits - operator id - * BITS_PER_VAR bits - destination var index - * BITS_PER_VAR bits - assign type (const (half of bits) or variable (half of bits)) - * BITS_PER_VAR bits - variable index or all bits till the end for constant + * Num.BITS_PER_OPERATOR bits - operator id + * Num.BITS_PER_VAR bits - destination var index + * Num.BITS_PER_VAR bits - assign type (const (half of bits) or variable (half of bits)) + * Num.BITS_PER_VAR bits - variable index or all bits till the end for constant * - * @param {Num} num Packed into number jsvm line - * @param {Number} line Current line in jsvm - * @return {Number} Parsed jsvm line string + * @param {Num} num Packed into number vm line + * @param {Number} line Current line in vm + * @return {Number} Parsed vm line string */ onVar(num, line) { const vars = this.vars; const var1 = VAR1(num); - vars[VAR0(num)] = var1 >= HALF_OF_VAR ? Num.getBits(num, BITS_AFTER_THREE_VARS, BITS_OF_TWO_VARS) : vars[var1]; + vars[VAR0(num)] = var1 >= HALF_OF_VAR ? Num.getBits(num, BITS_AFTER_THREE_VARS, Num.BITS_OF_TWO_VARS) : vars[var1]; return line + 1; } onCondition(num, line, org, lines) { - const val3 = Num.getBits(num, BITS_AFTER_THREE_VARS, BITS_OF_TWO_VARS); + const val3 = Num.getBits(num, BITS_AFTER_THREE_VARS, Num.BITS_OF_TWO_VARS); const offs = line + val3 < lines ? line + val3 + 1 : lines; const cond = VAR2(num) >>> (OConfig.codeBitsPerVar - CONDITION_BITS); @@ -106,7 +102,7 @@ class OperatorsGarmin extends Operators { // onLoop(num, line, org, lines, ret) { // const vars = this.vars; // const var0 = VAR0(num); -// const val3 = Num.getBits(num, BITS_AFTER_THREE_VARS, BITS_OF_TWO_VARS); +// const val3 = Num.getBits(num, _BITS_AFTER_THREE_VARS, Num.BITS_OF_TWO_VARS); // const offs = line + val3 < lines ? line + val3 + 1 : lines; // // if (ret) { @@ -128,7 +124,7 @@ class OperatorsGarmin extends Operators { onOperator(num, line) { const vars = this.vars; - vars[VAR0(num)] = this._OPERATORS[Num.getBits(num, BITS_AFTER_THREE_VARS, BITS_OF_TWO_VARS)](vars[VAR1(num)], vars[VAR2(num)]); + vars[VAR0(num)] = this._OPERATORS[Num.getBits(num, BITS_AFTER_THREE_VARS, Num.BITS_OF_TWO_VARS)](vars[VAR1(num)], vars[VAR2(num)]); return line + 1; } @@ -137,16 +133,6 @@ class OperatorsGarmin extends Operators { return line + 1; } -// onPi(num, line) { -// this.vars[VAR0(num)] = Math.PI; -// return line + 1; -// } -// -// onTrig(num, line) { -// this.vars[VAR0(num)] = this._TRIGS[VAR2(num)](this.vars[VAR1(num)]); -// return line + 1; -// } - onFromMem(num, line, org) {this.vars[VAR0(num)] = org.mem.pop() || 0; return line + 1} onToMem(num, line, org) { diff --git a/client/src/manager/plugins/organisms/garmin/Organism.js b/client/src/manager/plugins/organisms/garmin/Organism.js index 5864185..2ed9a63 100644 --- a/client/src/manager/plugins/organisms/garmin/Organism.js +++ b/client/src/manager/plugins/organisms/garmin/Organism.js @@ -4,9 +4,8 @@ * TODO: - * @author flatline */ -const Organism = require('./../Organism'); +const Organism = require('./../Organism').Organism; const Operators = require('./Operators'); -const Config = require('./../../../../share/Config').Config; const EVENTS = require('./../../../../share/Events').EVENTS; const Fitness = require('./Fitness'); @@ -17,19 +16,17 @@ class OrganismGarmin extends Organism { * @param {String} id Unique identifier of organism * @param {Number} x Unique X coordinate * @param {Number} y Unique Y coordinate - * @param {Boolean} alive true if organism is alive * @param {Object} item Reference to the Queue item, where * this organism is located - * @param {Function} codeEndCb Callback, which is called at the - * end of every code iteration. + * @param {Observer} obs Observer for sending external events * @param {Organism} parent Parent organism if cloning is needed */ - constructor(id, x, y, alive, item, codeEndCb, parent = null) { - super(id, x, y, alive, item, codeEndCb, Operators, parent); + constructor(id, x, y, item, obs, parent = null) { + super(id, x, y, item, Operators, obs, parent); this._needRun = true; - this.jsvm.on(EVENTS.RESET_CODE, this._onResetCode.bind(this)); + this.vm.on(EVENTS.RESET_CODE, this._onResetCode.bind(this)); } onBeforeRun() { @@ -37,7 +34,7 @@ class OrganismGarmin extends Organism { } onRun() { - if (Fitness.run(this)) {this.fire(EVENTS.STOP, this)} + if (Fitness.run(this)) {this.obs.fire(EVENTS.STOP, this)} this._needRun = false; } @@ -48,7 +45,6 @@ class OrganismGarmin extends Organism { /** * Is called when some modifications in code appeared and we have * to re-execute it again - * @private */ _onResetCode() { this._needRun = true; diff --git a/client/src/manager/plugins/organisms/garmin/Organisms.js b/client/src/manager/plugins/organisms/garmin/Organisms.js index 4f32498..8a584f1 100644 --- a/client/src/manager/plugins/organisms/garmin/Organisms.js +++ b/client/src/manager/plugins/organisms/garmin/Organisms.js @@ -3,14 +3,12 @@ * in fitness mode. * * Events od Manager: - * ORGANISM(org) Fires after one organism has processed * * Depends on: * manager/Manager * * @author flatline */ -const Config = require('./../../../../share/Config').Config; const Organism = require('./Organism'); const Console = require('./../../../../share/Console'); const EVENTS = require('./../../../../share/Events').EVENTS; @@ -21,6 +19,9 @@ class Organisms extends BaseOrganisms { constructor(manager) { super(manager); this._maxChanges = 0; + this._maxEnergy = 0; + + this.callbacks[EVENTS.STOP] = this._onStop.bind(this); } /** @@ -39,21 +40,17 @@ class Organisms extends BaseOrganisms { this._maxEnergy = org.energy; Console.warn('--------------------------------------------------'); Console.warn('Max energy: ', org.energy, ', org Id: ', org.id); - Console.warn('[' + org.jsvm.code + ']'); - Console.warn(this.manager.api.formatCode(org.jsvm.code)); + Console.warn('[' + org.vm.code + ']'); + Console.warn(this.manager.api.formatCode(org.vm.code)); } if (org.changes > this._maxChanges) {this._maxChanges = org.changes} } - addOrgHandlers(org) { - super.addOrgHandlers(org); - org.on(EVENTS.STOP, this._onStop.bind(this)); - } - reset() { super.reset(); this._maxChanges = 0; + this._maxEnergy = 0; } /** @@ -77,8 +74,8 @@ class Organisms extends BaseOrganisms { this.manager.stop(); Console.warn('--------------------------------------------------'); Console.warn('org id: ', org.id, ', energy: ', org.energy); - Console.warn('[' + org.jsvm.code + ']'); - Console.warn(this.manager.api.formatCode(org.jsvm.code)); + Console.warn('[' + org.vm.code + ']'); + Console.warn(this.manager.api.formatCode(org.vm.code)); } } diff --git a/client/src/manager/plugins/status/Status.js b/client/src/manager/plugins/status/Status.js new file mode 100644 index 0000000..0ed60b6 --- /dev/null +++ b/client/src/manager/plugins/status/Status.js @@ -0,0 +1,258 @@ +/** + * Base class for plugin which collects real time data about system like: + * population energy, changes, organisms lps etc and provide this data to + * other visualization classes (e.g. Charts). Output data is an object of + * described below format: + * + * lps: Lines Per Second - average amount of run code lines + * per one second + * org: Average amount of organisms at the moment of logging + * nrg: Amount of energy of average organism + * che: Changes amount of average organism + * fit: Fitness of average organism + * cod: Code size of average organism + * + * @author flatline + */ +const _fill = require('lodash/fill'); +const Helper = require('./../../../../../common/src/Helper'); +const EVENTS = require('./../../../share/Events').EVENTS; +const Configurable = require('./../../../../../common/src/Configurable'); +const Config = require('./../../../share/Config').Config; + +class Status extends Configurable { + static _toFixed(val, fixed) { + return +val.toFixed(val < 10 && val > -10 ? fixed : 0); + } + + /** + * Is called every time, when new status data is available + * @param {Object} status Status data + * @param {Number} orgs Amount of organisms + * @abstract + */ + onStatus(status, orgs) {} + + constructor(manager, statCfg, apiCfg = {}) { + super(manager, {Config, cfg: statCfg}, apiCfg); + + this._status = { + lps :0, ips :0, orgs :0, energy :0, oenergy :0, eenergy:0, wenergy:0, changes :0, fit:0, age:0, code:0, + kill:0, killenergy:0, killage:0, killeat:0, killover:0, killout:0, killin :0, killclone:0 + }; + this._stamp = 0; + this._ips = 0; + this._ipsTimes = 0; + this._energy = 0; + this._pickEnergy = 0; + this._eatEnergy = 0; + this._putEnergy = 0; + this._fitness = 0; + this._changes = 0; + this._codeSize = 0; + this._codeRuns = 0; + this._age = 0; + this._ageCount = 0; + this._times = 0; + this._kill = new Array(9); + this._worldEnergy = 0.0; + this._statusCfg = statCfg; + this._firstCall = true; + + this._onLoopCb = this._onLoop.bind(this); + this._onIpsCb = this._onIps.bind(this); + this._onEatEnergyCb = this._onEatEnergy.bind(this); + this._onEatOrgCb = this._onEatOrg.bind(this); + this._onPutEnergyCb = this._onPutEnergy.bind(this); + this._onKillOrgCb = this._onKillOrg.bind(this); + this._onKillEnergyCb = this._onKillHandlerOrg.bind(this, 1); + this._onKillTourCb = this._onKillHandlerOrg.bind(this, 2); + this._onKillAgeCb = this._onKillHandlerOrg.bind(this, 3); + this._onKillEatCb = this._onKillHandlerOrg.bind(this, 4); + this._onKillOverCb = this._onKillHandlerOrg.bind(this, 5); + this._onKillOutCb = this._onKillHandlerOrg.bind(this, 6); + this._onKillInCb = this._onKillHandlerOrg.bind(this, 7); + this._onKillCloneCb = this._onKillHandlerOrg.bind(this, 8); + this._onWorldEnergyCb = this._onWorldEnergy.bind(this); + + Helper.override(manager, 'onLoop', this._onLoopCb); + manager.on(EVENTS.IPS, this._onIpsCb); + manager.on(EVENTS.EAT_ENERGY, this._onEatEnergyCb); + manager.on(EVENTS.EAT_ORG, this._onEatOrgCb); + manager.on(EVENTS.PUT_ENERGY, this._onPutEnergyCb); + manager.on(EVENTS.KILL, this._onKillOrgCb); + manager.on(EVENTS.KILL_TOUR, this._onKillTourCb); + manager.on(EVENTS.KILL_NO_ENERGY, this._onKillEnergyCb); + manager.on(EVENTS.KILL_AGE, this._onKillAgeCb); + manager.on(EVENTS.KILL_EAT, this._onKillEatCb); + manager.on(EVENTS.KILL_OVERFLOW, this._onKillOverCb); + manager.on(EVENTS.KILL_STEP_OUT, this._onKillOutCb); + manager.on(EVENTS.KILL_STEP_IN, this._onKillInCb); + manager.on(EVENTS.KILL_CLONE, this._onKillCloneCb); + manager.on(EVENTS.WORLD_ENERGY, this._onWorldEnergyCb); + + _fill(this._kill, 0); + } + + destroy() { + const man = this.parent; + + man.off(EVENTS.WORLD_ENERGY, this._onWorldEnergyCb); + man.off(EVENTS.KILL_CLONE, this._onKillCloneCb); + man.off(EVENTS.KILL_STEP_IN, this._onKillInCb); + man.off(EVENTS.KILL_STEP_OUT, this._onKillOutCb); + man.off(EVENTS.KILL_OVERFLOW, this._onKillOverCb); + man.off(EVENTS.KILL_EAT, this._onKillEatCb); + man.off(EVENTS.KILL_AGE, this._onKillAgeCb); + man.off(EVENTS.KILL_NO_ENERGY, this._onKillEnergyCb); + man.off(EVENTS.KILL_TOUR, this._onKillTourCb); + man.off(EVENTS.KILL, this._onKillOrgCb); + man.off(EVENTS.PUT_ENERGY, this._onPutEnergyCb); + man.off(EVENTS.EAT_ORG, this._onEatOrgCb); + man.off(EVENTS.EAT_ENERGY, this._onEatEnergyCb); + man.off(EVENTS.IPS, this._onIpsCb); + Helper.unoverride(man, 'onLoop', this._onLoopCb); + + this._onWorldEnergyCb = null; + this._onKillOrgCb = null; + this._onKillTourCb = null; + this._onPutEnergyCb = null; + this._onEatOrgCb = null; + this._onEatEnergyCb = null; + this._onKillCloneCb = null; + this._onKillInCb = null; + this._onKillOutCb = null; + this._onKillOverCb = null; + this._onKillEatCb = null; + this._onKillAgeCb = null; + this._onKillEnergyCb = null; + this._onIpsCb = null; + this._onLoopCb = null; + this._status = null; + this._statusCfg = null; + } + + _onBeforeLoop(orgs) { + const size = orgs.size || 1; + let energy = 0; + let startEnergy = 0; + let iterations = 0; + let fitness = 0; + let changes = 0; + let codeSize = 0; + let item = orgs.first; + let org; + + while(item && (org = item.val)) { + energy += org.energy; + startEnergy += org.startEnergy; + iterations += org.iterations; + changes += org.changes; + fitness += org.fitness(); + codeSize += org.vm.size; + item = item.next; + } + + this._energy = energy / size; + this._changes = changes / size; + this._fitness = fitness / size; + this._codeSize = codeSize; + } + + _onLoop() { + if (!this._statusCfg.active) {return} + this._times++; + const stamp = Date.now(); + if (stamp - this._stamp < this._statusCfg.period) {return} + const orgs = this.parent.organisms; + const status = this._status; + const orgAmount = orgs.size || 1; + const fix = Status._toFixed; + + this._onBeforeLoop(orgs); + + status.ips = fix(this._ips / this._ipsTimes, 2); + status.lps = fix((this.parent.codeRuns - this._codeRuns) / ((stamp - this._stamp) / 1000), 0); + status.orgs = orgAmount; + status.energy = fix(this._energy, 2); + status.oenergy = fix(this._pickEnergy / orgAmount, 2); + status.eenergy = fix(this._eatEnergy / orgAmount, 2); + status.puenergy = fix(this._putEnergy / orgAmount, 2); + status.changes = +(this._changes).toFixed(1); + status.fit = fix(this._fitness / 100, 2); + status.age = fix(this._age / (this._ageCount || 1), 2); + status.code = +(this._codeSize / orgAmount).toFixed(2); + + status.kill = fix(this._kill[0], 2); + status.killenergy = fix(this._kill[1], 2); + status.killtour = fix(this._kill[2], 2); + status.killage = fix(this._kill[3], 2); + status.killeat = fix(this._kill[4], 2); + status.killover = fix(this._kill[5], 2); + status.killout = fix(this._kill[6], 2); + status.killin = fix(this._kill[7], 2); + status.killclone = fix(this._kill[8], 2); + + status.wenergy = fix(this._worldEnergy, 5); + + !this._firstCall && this.onStatus(status, orgs.size); + this._onAfterLoop(stamp); + this._firstCall = false; + } + + _onIps(ips) { + this._ips += ips; + this._ipsTimes++; + } + + _onAfterLoop(stamp) { + this._times = 0; + this._codeRuns = this.parent.codeRuns; + this._age = 0; + this._ageCount = 0; + this._eatEnergy = 0; + this._pickEnergy = 0; + this._ips = 0; + this._ipsTimes = 0; + this._stamp = stamp; + _fill(this._kill, 0); + } + + /** + * Calculates eat energy, excluding eating other organisms + * @param {Number} eat Amount of eat energy + */ + _onEatEnergy(eat) { + this._eatEnergy += eat; + } + + _onEatOrg(org, eat) { + this._pickEnergy += eat; + } + + /** + * Calculates putting of energy by organisms to the world + * @param {Number} put Amount of put energy + */ + _onPutEnergy(put) { + this._putEnergy += put; + } + + _onKillOrg(org) { + if (!this._statusCfg.active) {return} + this._age += org.iterations; + this._ageCount++; + this._kill[0] ++; + } + + _onKillHandlerOrg(index, org) { + this._kill[index]++; + index === 4 && (this._pickEnergy += org.energy); + } + + _onWorldEnergy(percent) { + this._worldEnergy = percent; + } +} + +module.exports = Status; \ No newline at end of file diff --git a/client/src/manager/plugins/status/charts/Chart.js b/client/src/manager/plugins/status/charts/Chart.js new file mode 100644 index 0000000..6cdc962 --- /dev/null +++ b/client/src/manager/plugins/status/charts/Chart.js @@ -0,0 +1,196 @@ +/** + * Draws one line chart using Google charts library. The purpose of + * this class is to show dynamics of changing for some property. For + * example: energy, age or code size. update() method should obtain + * new portion of property data to draw on chart. Works only in browser. + * Requires internet connection for google charts dynamic load. + * + * @author flatline + */ +const _get = require('lodash/get'); +const Helper = require('./../../../../../../common/src/Helper'); +const Config = require('./Config'); + +const GOOGLE_CHARTS_URL = 'https://www.gstatic.com/charts/loader.js'; + +class Chart { + constructor(title, cfg) { + this._options = { + title : title, + hAxis : {textStyle: {fontSize: 10}}, + vAxis : {textStyle: {fontSize: 10}, gridlines: {count: 10}}, + legend : 'none', + chartArea: {left: 90, top: 30, bottom: 50, right: 3, width: '100%', height: '100%'}, + }; + this._data = null; + this._chart = null; + this._ready = false; + this._el = null; + this._cfg = cfg; + + this._loadLib(this._onReady.bind(this)); + } + + /** + * Updates chart with new portion of data + * @param {Array} data Array of two new values + * @return {Boolean} + */ + update(data) { + if (!this._ready) {return false} + + this._data.addRow(data); + _get(this, '_cfg.active') && this._chart.draw(this._data, this._options); + + if (this._data.getNumberOfRows() > Config.dataMaxSize) { + this._data.removeRows(0, Config.dataMaxSize * 0.1); + } + + return true; + } + + set transparent(t) {this._el && (this._el.style.opacity = t)} + set pos(p) {this._updatePos(p)} + set active(a) {this._updateActive(a)} + + reset() { + if (!this._ready) {return false} + this._data = this._createDataTable(); + return true; + } + + destroy() { + this._cfg.active && this._chart.clearChart(); + this._el.parentNode.removeChild(this._el); + this._el = null; + this._data = null; + this._chart = null; + this._options = null; + } + + _loadLib(cb) { + let scriptEl; + const googleLib = window.google; + const setReadyCb = () => { + window.google.charts.load('current', {'packages': ['corechart']}); + window.google.charts.setOnLoadCallback(() => cb()); + }; + // + // Google charts library has already loaded + // + if (googleLib) {return setReadyCb()} + // + // Google charts library is loading now + // + if (scriptEl = document.querySelector(`script[src="${GOOGLE_CHARTS_URL}"]`)) { + return scriptEl.addEventListener('load', setReadyCb); + } + // + // Google charts library hasn't loaded yet + // + scriptEl = document.createElement('SCRIPT'); + scriptEl.type = 'text/javascript'; + scriptEl.src = GOOGLE_CHARTS_URL; + scriptEl.onload = setReadyCb; + document.head.appendChild(scriptEl); + } + + _onReady() { + this._ready = true; + !this._data && (this._data = this._createDataTable()); + if (!this._cfg.active || this._el) {return} + + document.body.appendChild(this._el = Helper.setStyles('DIV', { + position: 'absolute', + opacity : this._cfg.transparent + })); + this._updatePos(this._cfg.pos); + + this._chart = new window.google.visualization.LineChart(this._el); + } + + /** + * Returns chart container sizes depending on position. Sizes format: + * [width, height, left, top] + * @param {String} pos Position + * @returns {Array} [width, height, left, top] + */ + _getSize(pos) { + return { + full : ['100%', '100%', '0', '0' ], + + top : ['100%', '50%', '0', '0' ], + down : ['100%', '50%', '0', '50%' ], + left : ['50%', '100%', '0', '0' ], + right : ['50%', '100%', '50%', '0' ], + + topleft : ['50%', '50%', '0', '0' ], + downleft : ['50%', '50%', '0', '50%' ], + topright : ['50%', '50%', '50%', '0' ], + downright: ['50%', '50%', '50%', '50%' ], + + '0-0|9' : ['33.3%', '33.3%', '0', '0' ], + '1-0|9' : ['33.3%', '33.3%', '33.3%', '0' ], + '2-0|9' : ['33.3%', '33.3%', '66.6%', '0' ], + '0-1|9' : ['33.3%', '33.3%', '0', '33.3%'], + '1-1|9' : ['33.3%', '33.3%', '33.3%', '33.3%'], + '2-1|9' : ['33.3%', '33.3%', '66.6%', '33.3%'], + '0-2|9' : ['33.3%', '33.3%', '0', '66.6%'], + '1-2|9' : ['33.3%', '33.3%', '33.3%', '66.6%'], + '2-2|9' : ['33.3%', '33.3%', '66.6%', '66.6%'], + + '0-0|16' : ['25%', '25%', '0', '0' ], + '1-0|16' : ['25%', '25%', '25%', '0' ], + '2-0|16' : ['25%', '25%', '50%', '0' ], + '3-0|16' : ['25%', '25%', '75%', '0' ], + + '0-1|16' : ['25%', '25%', '0', '25%' ], + '1-1|16' : ['25%', '25%', '25%', '25%' ], + '2-1|16' : ['25%', '25%', '50%', '25%' ], + '3-1|16' : ['25%', '25%', '75%', '25%' ], + + '0-2|16' : ['25%', '25%', '0', '50%' ], + '1-2|16' : ['25%', '25%', '25%', '50%' ], + '2-2|16' : ['25%', '25%', '50%', '50%' ], + '3-2|16' : ['25%', '25%', '75%', '50%' ], + + '0-3|16' : ['25%', '25%', '0', '75%' ], + '1-3|16' : ['25%', '25%', '25%', '75%' ], + '2-3|16' : ['25%', '25%', '50%', '75%' ], + '3-3|16' : ['25%', '25%', '75%', '75%' ] + }[pos]; + } + + _updatePos(pos) { + if (!this._el) {return} + const size = this._getSize(pos); + + Helper.setStyles(this._el, { + width : size[0], + height: size[1], + left : size[2], + top : size[3] + }); + } + + _updateActive(active) { + if (active) { + this._onReady(); + } else { + this._chart && this._chart.clearChart(); + this._el && this._el.parentNode.removeChild(this._el); + this._chart = this._el = null; + } + } + + _createDataTable() { + const data = new window.google.visualization.DataTable(); + + data.addColumn('string', 'horizontal'); + data.addColumn('number', 'vertical'); + + return data; + } +} + +module.exports = Chart; \ No newline at end of file diff --git a/client/src/manager/plugins/status/charts/Charts.js b/client/src/manager/plugins/status/charts/Charts.js new file mode 100644 index 0000000..3066047 --- /dev/null +++ b/client/src/manager/plugins/status/charts/Charts.js @@ -0,0 +1,302 @@ +/** + * Manager plugin, which draws real time charts based on real time data. For example: + * energy, iq, changes and so on. The same values like Status plugin uses. + * + * @author flatline + */ +const _each = require('lodash/each'); +const _has = require('lodash/has'); +const _get = require('lodash/get'); +const Helper = require('./../../../../../../common/src/Helper'); +const Status = require('./../Status'); +const Chart = require('./Chart'); +const Config = require('./Config'); + +const API = { + transparent: ['_transparent', 'Sets transparent level' ], + pos : ['_pos', 'Sets chart position' ], + pos9 : ['_pos9', 'Sets chart position in 3x3 grid' ], + pos16 : ['_pos16', 'Sets chart position in 4x4 grid' ], + active : ['_active', 'Activates/Deactivates chart' ], + on : ['_on', 'Activates/Deactivates chart by name' ], + off : ['_off', 'Deactivates chart' ], + reset : ['_reset', 'Resets chart data' ], + preset : ['_preset', 'Positioning charts according to preset'] +}; + +const PRESETS = { + general : { + lps : 'topleft', + eenergy : 'downleft', + changes : 'topright', + code : 'downright' + }, + general9 : { + lps : '0-0|9', + orgs : '0-1|9', + eenergy : '0-2|9', + changes : '1-0|9', + age : '1-1|9', + code : '1-2|9', + kill : '2-0|9', + ips : '2-1|9', + fit : '2-2|9' + }, + general16: { + lps : '0-0|16', + orgs : '0-1|16', + energy : '0-2|16', + eenergy : '0-3|16', + oenergy : '1-0|16', + changes : '1-1|16', + age : '1-2|16', + code : '1-3|16', + fit : '2-0|16', + kill : '2-1|16', + killeat : '2-2|16', + killenergy: '2-3|16' + }, + kill9 : { + kill : '0-0|9', + killtour : '0-1|9', + killenergy: '0-2|9', + killage : '1-0|9', + killeat : '1-1|9', + killover : '1-2|9', + killout : '2-0|9', + killin : '2-1|9', + killclone : '2-2|9' + }, + energy : { + lps : 'topleft', + energy : 'downleft', + eenergy : 'topright', + oenergy : 'downright' + } +}; + +class Charts extends Status { + static _to12h(time) { + let hours = time.getHours(); + let minutes = time.getMinutes(); + + hours = hours % 12; + hours = hours ? hours : 12; + + return hours + ':' + minutes; + } + + static _createHeader() { + return document.body.appendChild(Helper.setStyles('DIV', { + position : 'absolute', + top : '7px', + left : '35px', + color : '#fff', + fontSize : '18px', + fontFamily: 'Consolas' + })); + } + + constructor(manager) { + super(manager, Config, API); + const periodSec = Config.period / 1000; + + this._data = new Array(2); + this._headerEl = Charts._createHeader(); + this._charts = { + lps : new Chart('LPS - Lines Per Second', Config.charts.lps), + ips : new Chart('IPS - Iterations Per Second', Config.charts.ips), + orgs : new Chart('Amount of organisms', Config.charts.orgs), + energy : new Chart('Average organism energy', Config.charts.energy), + oenergy : new Chart('Average organism\'s eat energy from other organisms', Config.charts.oenergy), + eenergy : new Chart('Average organism\'s picked energy (energy only)', Config.charts.eenergy), + puenergy : new Chart('Average organism\'s put energy to the world', Config.charts.puenergy), + changes : new Chart('Average organism\'s changes (Mutations)', Config.charts.changes), + fit : new Chart('Average organism\'s Fitness', Config.charts.fit), + age : new Chart('Average organism\'s Age', Config.charts.age), + code : new Chart('Average organism\'s code size', Config.charts.code), + kill : new Chart(`Amount of killed organisms per ${periodSec} sec (all)`, Config.charts.kill), + killtour : new Chart(`Amount of killed organisms in tournament per ${periodSec} sec`, Config.charts.killtour), + killenergy: new Chart(`Amount of killed organisms with 0 energy per ${periodSec} sec`, Config.charts.killenergy), + killage : new Chart(`Amount of killed organisms with max age per ${periodSec} sec`, Config.charts.killage), + killeat : new Chart(`Amount of killed organisms eat by other per ${periodSec} sec`, Config.charts.killeat), + killover : new Chart(`Amount of organisms killed after overflow per ${periodSec} sec`, Config.charts.killover), + killout : new Chart(`Amount of organisms killed after step out per ${periodSec} sec`, Config.charts.killout), + killin : new Chart(`Amount of organisms killed after step in per ${periodSec} sec`, Config.charts.killin), + killclone : new Chart(`Amount of organisms killed during clone per ${periodSec} sec`, Config.charts.killclone) + }; + } + + destroy() { + _each(this._charts, chart => chart.destroy()); + this._headerEl.parentNode.removeChild(this._headerEl); + this._headerEl = null; + this._charts = null; + this._data = null; + + super.destroy(); + } + + /** + * Is called every time, when new status data is available + * @param {Object} status Status data + * @override + */ + onStatus(status) { + this._updateCharts(status); + this._updateHeader(status); + } + + _updateCharts(status) { + const data = this._data; + const charts = this._charts; + data[0] = Charts._to12h(new Date); + + _each(charts, (chart, key) => { + data[1] = status[key]; + chart.update(data); + }); + } + + _updateHeader(status) { + const man = this.parent; + const active = man.activeAround; + if (!man.canvas) {return} + const conns = `${active[0] ? '^' : ''}${active[1] ? '>' : ''}${active[2] ? 'v' : ''}${active[3] ? '<' : ''}`; + const ips = `ips:${status.ips}`; + const enrg = `enrg:${status.eenergy}`; + const onrg = `onrg:${status.oenergy}`; + const wnrg = `wnrg:${status.wenergy}`; + const code = `cod:${status.code}`; + const age = `age:${status.age}`; + const kill = `kil:${status.kill}`; + const kilo = `kilo:${status.killeat}`; + const orgs = `org:${status.orgs}`; + + this._headerEl.textContent = `${man.clientId ? 'id:' + man.clientId : ''} ${conns === '' ? '' : 'con:' + conns} ${ips} ${wnrg} ${enrg} ${onrg} ${code} ${age} ${kill} ${kilo} ${orgs}`; + } + + /** + * Sets specified chart transparent coefficient. May be called + * with one Number parameter to set it for all charts. + * @param {String|Number} chart Chart name, e.g: 'energy', or 'lps' or opacity value + * @param {Number=} t Value between 0...1 or undefined + * @api + */ + _transparent(chart, t) { + if (typeof t === 'undefined') { + _each(this._charts, (c, k) => this._setProperty(k, 'transparent', chart)); + } else { + this._setProperty(chart, 'transparent', t); + } + } + + /** + * Sets current chart position. Available positions: + * top, down, left, right, topleft, downleft, topright, + * downright, full. + * @param {String} chart Chart name, e.g: 'energy', or 'lps' + * @param {String} p new position + * @api + */ + _pos(chart, p) {this._setProperty(chart, 'pos', p)} + + /** + * Positioning method, which position a chart in 3x3 grid. + * Positioning based on x,y coordinates. e.g.: pos('kill', 0,0) + * will be located in a left top corner + * @param {String} chart Chart name, e.g: 'energy', or 'lps' + * @param {Number} x X coordinate + * @param {Number} y Y coordinate + * @api + */ + _pos9(chart, x, y) {this._setProperty(chart, 'pos', `${x > 2 ? 2 : x < 0 ? 0 : x}-${y > 2 ? 2 : y < 0 ? 0 : y}|9`)} + + /** + * Positioning method, which position a chart in 4x4 grid. + * Positioning based on x,y coordinates. e.g.: pos('kill', 0,0) + * will be located in a left top corner + * @param {String} chart Chart name, e.g: 'energy', or 'lps' + * @param {Number} x X coordinate + * @param {Number} y Y coordinate + * @api + */ + _pos16(chart, x, y) {this._setProperty(chart, 'pos', `${x > 3 ? 3 : x < 0 ? 0 : x}-${y > 3 ? 3 : y < 0 ? 0 : y}|16`)} + + /** + * Sets current chart position. Available positions: + * top, down, left, right, topleft, downleft, topright, + * downright, full. This method may be called with one + * Boolean parameter. In this case all charts will be activated + * or deactivated. + * @param {String|Boolean} chart Chart name, e.g: 'energy', or 'lps' + * @param {Boolean=} a New active state + * @api + */ + _active(chart, a) { + const noA = typeof a === 'undefined'; + const noChart = typeof chart === 'undefined'; + const bChart = typeof chart === 'boolean'; + + a = noA ? (bChart ? chart : true) : a; + chart = noChart ? null : (bChart ? null : chart); + + if (chart === null) { + _each(this._charts, (c, k) => this._setProperty(k, 'active', a)); + } else { + this._setProperty(chart, 'active', a); + } + } + + /** + * Shortcut for _active() method. See it for details. + */ + _on(...args) { + this._active(...args) + } + + /** + * Opposite to _on(). + * @param {String=} chart Chart name + */ + _off(chart) { + if (typeof chart === 'string') { + this._active(chart, false); + } else { + this._active(false); + } + } + + /** + * Resets chart data. It means, that chart will be refreshed with + * data started from this moment and further. You may call this + * method without parameters to reset all charts. + * @param {String=} chart Chart name, e.g: 'energy', or 'lps' + * @api + */ + _reset(chart) { + if (typeof chart === 'undefined') { + _each(this._charts, c => {c.reset(); return true}); + } else { + _get(this, `_charts.${chart}`, {}).reset && this._charts[chart].reset(); + } + } + + _preset(name) { + if (typeof(PRESETS[name]) === 'undefined') {return} + + _each(this._charts, (inst, chart) => this._off(chart)); + _each(PRESETS[name], (pos, chart) => { + this._pos(chart, pos); + this._on(chart); + }); + } + + _setProperty(chart, prop, val) { + if (typeof(this._charts[chart]) === 'undefined') {return} + _has(this.cfg, `charts.${chart}.${prop}`) && (this.cfg.charts[chart][prop] = val); + this._charts[chart][prop] = val; + } +} + +module.exports = Charts; \ No newline at end of file diff --git a/client/src/manager/plugins/status/charts/Config.js b/client/src/manager/plugins/status/charts/Config.js new file mode 100644 index 0000000..00871a7 --- /dev/null +++ b/client/src/manager/plugins/status/charts/Config.js @@ -0,0 +1,47 @@ +/** + * Configuration of Status plugin + * + * @author flatline + */ +const Config = { + /** + * {Boolean} Turns on/off plugin data collection + */ + active: true, + /** + * {Number} Delay in milliseconds between showing one status line + */ + period: 2000, + /** + * {Number} Maximum amount of chart rows, after which we will cut + * it on 10% + */ + dataMaxSize: 100000, + /** + * {Object} Available chart types + */ + charts: { + lps : {pos: 'downleft', active: false, transparent: 0.8}, + killout : {pos: 'topright', active: false, transparent: 0.8}, + energy : {pos: 'downright', active: false, transparent: 0.8}, + orgs : {pos: '0-0|16', active: false, transparent: 0.8}, + oenergy : {pos: '0-1|16', active: false, transparent: 0.8}, + eenergy : {pos: '0-2|16', active: false, transparent: 0.8}, + puenergy : {pos: '0-3|16', active: false, transparent: 0.8}, + fit : {pos: '1-0|16', active: false, transparent: 0.8}, + age : {pos: '1-1|16', active: false, transparent: 0.8}, + code : {pos: '1-2|16', active: false, transparent: 0.8}, + kill : {pos: '1-3|16', active: false, transparent: 0.8}, + killtour : {pos: '2-0|16', active: false, transparent: 0.8}, + killenergy: {pos: '2-1|16', active: false, transparent: 0.8}, + killage : {pos: '2-2|16', active: false, transparent: 0.8}, + killeat : {pos: '2-3|16', active: false, transparent: 0.8}, + killover : {pos: '3-0|16', active: false, transparent: 0.8}, + changes : {pos: '3-1|16', active: false, transparent: 0.8}, + killin : {pos: '3-2|16', active: false, transparent: 0.8}, + killclone : {pos: '3-3|16', active: false, transparent: 0.8}, + ips : {pos: 'downright', active: false, transparent: 0.8} + } +}; + +module.exports = Config; \ No newline at end of file diff --git a/client/src/manager/plugins/status/console/Config.js b/client/src/manager/plugins/status/console/Config.js new file mode 100644 index 0000000..f07246f --- /dev/null +++ b/client/src/manager/plugins/status/console/Config.js @@ -0,0 +1,17 @@ +/** + * Configuration of Console plugin + * + * @author flatline + */ +const Config = { + /** + * {Boolean} Turns on/off plugin data collection + */ + active: true, + /** + * {Number} Delay in milliseconds between showing one status line + */ + period: 2000 +}; + +module.exports = Config; \ No newline at end of file diff --git a/client/src/manager/plugins/status/console/Console.js b/client/src/manager/plugins/status/console/Console.js new file mode 100644 index 0000000..07ea2f4 --- /dev/null +++ b/client/src/manager/plugins/status/console/Console.js @@ -0,0 +1,63 @@ +/** + * This plugin is a primitive version of real time charts. It shows + * different parameters of the jevo.js system, like average energy, + * lps (Lines Per Second), average code size and so on. Here + * labels explanation: + * + * lps: Lines Per Second - average amount of run code lines + * per one second + * org: Average amount of organisms at the moment of logging + * nrg: Amount of energy of average organism + * che: Changes amount of average organism + * fit: Fitness of average organism + * cod: Code size of average organism + * + * @author flatline + */ +const Status = require('./../Status'); +const Config = require('./Config'); + +const GREEN = 'color: #00aa00'; +const RED = 'color: #aa0000'; + +class Console extends Status { + static _format(value, name, pad) {return `${name}:${value}`.padEnd(pad)} + + constructor(manager) { + super(manager, Config); + } + + destroy() { + super.destroy(); + } + + /** + * Is called every time, when new status data is available + * @param {Object} status Status data + * @param {Number} orgs Amount of organisms + * @override + */ + onStatus(status, orgs) { + const man = this.parent; + const active = man.activeAround; + const format = Console._format; + + const con = `${active[0] ? '^' : ' '}${active[1] ? '>' : ' '}${active[2] ? 'v' : ' '}${active[3] ? '<' : ' '} `; + const conns = `con:${con === ' ' ? 'no ' : con}`; + const sips = format(status.ips, 'ips', 12); + const slps = format(status.lps, 'lps', 14); + const sorgs = format(orgs, 'org', 10); + const senergy = format(status.energy, 'nrg', 19); + const spenergy = format(status.oenergy, 'onrg', 15); + const seenergy = format(status.eenergy, 'enrg', 16); + const skill = format(status.kill, 'kil', 12); + const schanges = format(status.changes, 'che', 12); + const sage = format(status.age, 'age', 11); + const scode = format(status.code, 'cod', 12); + + // TODO: under Node.js should use Server/Console.xxx() + console.log(`%c${conns}${sips}${slps}${sorgs}%c${senergy}${spenergy}${seenergy}${skill}${schanges}${sage}${scode}`, GREEN, RED); + } +} + +module.exports = Console; \ No newline at end of file diff --git a/client/src/share/Config.js b/client/src/share/Config.js index bc75324..d70a4bb 100644 --- a/client/src/share/Config.js +++ b/client/src/share/Config.js @@ -11,6 +11,8 @@ const QUIET_ALL = 0; const QUIET_IMPORTANT = 1; const QUIET_NO = 2; +const IS_NODE_JS = typeof window === 'undefined'; + class ClientConfig extends Config {} ClientConfig.init({ @@ -22,6 +24,11 @@ ClientConfig.init({ QUIET_ALL, QUIET_IMPORTANT, QUIET_NO, + /** + * {Boolean} Running mode. It's also possible to run construct only on + * server side without browser. For this it should be set to true. + */ + MODE_NODE_JS: IS_NODE_JS, /** * {Array} Array of paths to Manager's plugins. Root folder for plugins * should be './client/src/manager/plugins/'. @@ -32,7 +39,9 @@ ClientConfig.init({ 'Config', 'client/Client', 'Energy', - 'Status', + 'Stones', + 'status/console/Console', + IS_NODE_JS ? '' : 'status/charts/Charts', 'ips/Ips', 'backup/Backup' ], @@ -48,56 +57,68 @@ ClientConfig.init({ /** * {Number} World height */ - worldHeight: 1010, + worldHeight: 1080, /** * {Number} Turns on cyclic world mode. It means that organisms may go outside * it's border, but still be inside. For example, if the world has 10x10 - * size and the organism has 10x5 position in it, one step right will move - * this organism at the position 1x5. The same scenario regarding Y + * size and the organism has 9x5 position in it, one step right will move + * this organism at the position 0x5. The same scenario regarding Y * coordinate (height). It actual only for one instance mode (no distributed * calculations). */ worldCyclical: true, /** - * {Number} Amount of energy blocks in a world. Blocks will be placed in a - * random way... + * {Number} An amount of iteration, after which we have to check world energy + * percent. May be 0 if you want to disable energy generation */ - worldEnergyDots: 1000, + worldEnergyCheckPeriod: 5000, /** - * {Number} Amount of energy in every block. See worldEnergyDots - * config for details. + * {Number} size of one clever energy block in dots */ - worldEnergyInDot: 0x00FF00, + worldEnergyBlockSize: 10, /** - * {Number} Minimum percent of energy in current world. Under percent i mean - * percent from entire world area (100%). If the energy will be less - * or equal then this percent, then new random energy should be added. - * Should be less then 100.0 and more and equal to 0.0. 0.17 is a - * normal percent for this system. + * {Number} Index of energy color. Starts from 0. Ends with 4000. See Organism.MAX_COLORS + * constant for details */ - worldEnergyCheckPercent: 0.1, + worldEnergyColorIndex: 0, /** - * {Number} An amount of iteration, after which we have to check world energy - * amount. Works in pair with worldEnergyCheckPercent. May be 0 if - * you want to disable it + * {Number} Percent from all energy in a world until clever energy will be added. + * After this value clever energy will be stopped to add until it's amount will + * be less then worldEnergyMinPercent. These two configs create cyclical + * energy adding to the world. */ - worldEnergyCheckPeriod: 10000, + worldEnergyMaxPercent: .3, /** - * {Number} Mode for showing/supressing of messages. Possible values: + * {Number} Opposite to worldEnergyMaxPercent. Sets minimum percent from + * all energy in a world after which clever energy will turn on (be added to the + * world again). + */ + worldEnergyMinPercent: .28, + /** + * {Number} Percent of stones in a world. Percent from world size: + * stoneAmount = worldStonesPercent * worldWidth * worldHeight + */ + worldStonesPercent: .25, + /** + * {Number} Color index for stones in a world. See Organism.MAX_COLORS + * constant for details + */ + worldStoneColorIndex: 1800, + /** + * {Number} Zoom speed 0..1 + */ + worldZoomSpeed: 0.1, + /** + * {Number} Mode for showing/suppressing of messages. Possible values: * 0 - all messages * 1 - only important messages * 2 - no messages */ modeQuiet: QUIET_IMPORTANT, - /** - * {Boolean} Running mode. It's also possible to run construct only on - * server side without browser. For this it should be set to true. - */ - modeNodeJs: false, /** * {Number} Port number for connecting with server */ - serverPort: 8099, + serverPort: 8301, /** * {String} Host for connecting with server */ diff --git a/client/src/share/Events.js b/client/src/share/Events.js index 2c44dfb..d099c33 100644 --- a/client/src/share/Events.js +++ b/client/src/share/Events.js @@ -4,28 +4,42 @@ * * @author flatline * TODO: find unused and remove. But after main code is done. + * TODO: describe all events */ const EVENTS = { - ITERATION : 0, - IPS : 1, - BACKUP : 2, - ORGANISM : 3, - GRAB_ENERGY : 4, - UPDATE_ENERGY : 5, - KILL_ORGANISM : 6, - MUTATIONS : 7, - CLONE : 8, - EAT : 9, - STEP : 10, - STEP_OUT : 11, - STEP_IN : 12, - BORN_ORGANISM : 13, - GET_ENERGY : 14, - DESTROY : 15, - RUN : 16, - STOP : 17, - RESET_CODE : 18, - CHECK_AT : 19 + ITERATION : 0, + LOOP : 1, + IPS : 2, + CODE_RUN : 3, + BACKUP : 4, + CLONE : 5, + KILL : 6, // general kill event + KILL_TOUR : 7, // killed during organisms tournament + KILL_NO_ENERGY : 8, // killed if zero energy + KILL_AGE : 9, // killed if max age reached + KILL_EAT : 10, // killed, because other organism has eat this one + KILL_OVERFLOW : 11, // population reaches it's maximum, we have to kill one organism + KILL_STEP_OUT : 12, // killed, because organism step outside the world + KILL_STEP_IN : 13, // killed, because of punishment for step in from near client + KILL_CLONE : 14, // killed, because of lack of energy after clone + MUTATIONS : 15, + EAT : 16, + EAT_ORG : 17, + EAT_ENERGY : 18, + PUT_ENERGY : 19, + STEP : 20, + STEP_OUT : 21, + STEP_IN : 22, + BORN_ORGANISM : 23, + GET_ENERGY : 24, + DESTROY : 25, + RUN : 26, + STOP : 27, + RESET_CODE : 28, + CHECK_AT : 29, + WORLD_ENERGY : 30, + WORLD_ENERGY_UP : 31 + }; const EVENT_AMOUNT = Object.keys(EVENTS).length; diff --git a/client/src/view/Canvas.js b/client/src/view/Canvas.js index 75cd965..20b405a 100644 --- a/client/src/view/Canvas.js +++ b/client/src/view/Canvas.js @@ -3,37 +3,46 @@ * * @author flatline */ +const Panzoom = require('panzoom'); +const Helper = require('./../../../common/src/Helper'); +const Config = require('./../share/Config').Config; + class Canvas { constructor(width, height) { - const id = 'world'; - const doc = document; - const bodyEl = doc.body; + const id = 'world'; + const doc = document; - this._prepareDom(); - bodyEl.innerHTML += ``; + doc.body.innerHTML += ``; this._id = id; this._width = width; this._height = height; this._canvasEl = doc.querySelector('#' + id); this._ctx = this._canvasEl.getContext('2d'); - this._text = {x: 0, y: 0, t: ''}; this._imgData = this._ctx.createImageData(this._width, this._height); this._data = this._imgData.data; this._animate = this._onAnimate.bind(this); this._visualize = true; + this._panZoom = null; + this._fullEl = this._createFullScreen(); - this._ctx.font = "13px Consolas"; - this._ctx.fillStyle = "white"; + this._prepareDom(); + this._initPanZoomLib(); this.clear(); window.requestAnimationFrame(this._animate); } destroy() { - this._canvasEl.parentNode.removeChild(this._canvasEl); - this._ctx = null; - this._imgData = null; - this._data = null; + const parentNode = this._canvasEl.parentNode; + + this._panZoom.dispose(); + parentNode.removeChild(this._canvasEl); + parentNode.removeChild(this._fullEl); + this._canvasEl = null; + this._fullEl = null; + this._ctx = null; + this._imgData = null; + this._data = null; } visualize(visualize = true) { @@ -41,14 +50,6 @@ class Canvas { this._onAnimate(); } - text(x, y, text) { - const t = this._text; - - t.t = text; - t.x = x; - t.y = y; - } - dot(x, y, color) { this._dot(x, y, color); } @@ -84,11 +85,33 @@ class Canvas { data[offs + 2] = color & 0xff; } - _onAnimate() { - const text = this._text; + _createFullScreen() { + const el = document.body.appendChild(Helper.setStyles('DIV', { + position : 'absolute', + width : '20px', + height : '20px', + top : '7px', + left : '7px', + border : '1px #000 solid', + backgroundColor: '#f7ed0e', + borderRadius : '6px', + cursor : 'pointer' + })); + + el.title = 'fullscreen'; + el.onclick = () => { + this._panZoom.zoomAbs(0, 0, 1.0); + this._panZoom.moveTo(0, 0); + this._canvasEl.style.width = '100%'; + this._canvasEl.style .height = '100%'; + + }; + + return el; + } + _onAnimate() { this._ctx.putImageData(this._imgData, 0, 0); - this._ctx.fillText(text.t, text.x, text.y); if (this._visualize === true) { window.requestAnimationFrame(this._animate); @@ -96,15 +119,41 @@ class Canvas { } _prepareDom() { - const bodyEl = document.querySelector('body'); + const bodyEl = document.body; const htmlEl = document.querySelector('html'); - bodyEl.style.width = '100%'; - bodyEl.style.height = '100%'; - bodyEl.style.margin = 0; - htmlEl.style.width = '100%'; - htmlEl.style.height = '100%'; - htmlEl.style.margin = 0; + Helper.setStyles(bodyEl, { + width : '100%', + height : '100%', + margin : 0, + backgroundColor: '#9e9e9e' + }); + Helper.setStyles(htmlEl, { + width : '100%', + height : '100%', + margin : 0 + }); + + this._ctx.font = "18px Consolas"; + this._ctx.fillStyle = "white"; + // + // This style hides scroll bars on full screen 2d canvas + // + document.querySelector('html').style.overflow = 'hidden'; + } + + /** + * Initializes 'panzoom' library, which adds possibility to + * zoom and scroll canvas by mouse. imageRendering css property + * removes smooth effect while zooming + */ + _initPanZoomLib() { + this._canvasEl.style.imageRendering = 'pixelated'; + this._panZoom = Panzoom(this._canvasEl, { + zoomSpeed : Config.worldZoomSpeed, + smoothScroll: false + }); + this._panZoom.zoomAbs(0, 0, 1.0); } } diff --git a/client/src/view/World.js b/client/src/view/World.js index 782fbf3..6b8d43a 100644 --- a/client/src/view/World.js +++ b/client/src/view/World.js @@ -19,7 +19,6 @@ */ const Observer = require('./../../../common/src/Observer'); const Helper = require('./../../../common/src/Helper'); -const EVENTS = require('./../../src/share/Events').EVENTS; const EVENT_AMOUNT = require('./../../src/share/Events').EVENT_AMOUNT; const DOT = 0; @@ -30,7 +29,8 @@ const WEVENTS = { * {Number} Amount of attempts for finding free place in a world. * The same like this.getDot(x, y) === 0 */ -const FREE_DOT_ATTEMPTS = 300; +const FREE_DOT_ATTEMPTS = 100; +const OBJECT_TYPES = {}; class World extends Observer { constructor (width, height) { @@ -63,7 +63,7 @@ class World extends Observer { } getDot(x, y) { - if (x < 0 || x >= this._width || y < 0 || y >= this._height) {return false} + if (x < 0 || x >= this._width || y < 0 || y >= this._height) {return 0} return this._data[x][y]; } @@ -87,7 +87,7 @@ class World extends Observer { while (this.getDot(x = rand(width), y = rand(height)) > 0 && i-- > 0) {} - return i > 0 ? {x, y} : false + return i > 0 ? [x, y] : [-1, -1] } getNearFreePos(x, y) { @@ -103,13 +103,15 @@ class World extends Observer { ]; for (let i = 0, j = 0; i < 8; i++) { - if (this.getDot(positions[j], positions[j + 1]) === 0) { - return {x: positions[j], y: positions[j + 1]}; + x = positions[j]; + y = positions[j + 1]; + if (this.getDot(x, y) === 0 && x >= 0 && x < this._width && y >= 0 && y < this._height) { + return [positions[j], positions[j + 1]]; } j += 2; } - return false; + return [-1, -1]; } isFree(x, y) { @@ -117,4 +119,4 @@ class World extends Observer { } } -module.exports = {World, EVENTS: WEVENTS}; \ No newline at end of file +module.exports = {World, EVENTS: WEVENTS, OBJECT_TYPES}; \ No newline at end of file diff --git a/client/src/vm/Num.js b/client/src/vm/Num.js new file mode 100644 index 0000000..1a7c6a8 --- /dev/null +++ b/client/src/vm/Num.js @@ -0,0 +1,99 @@ +/** + * Class - helper for working with byte code numbers + * + * @author flatline + */ +const Helper = require('./../../../common/src/Helper'); +const OConfig = require('./../manager/plugins/organisms/Config'); + +class Num { + /** + * Analog of constructor. Initializes all internal constants for + * working with byte code numbers. May be called many times + * @param {Number} operatorAmount Amount of operators in current + * script implementation + */ + static init(operatorAmount) { + this.MAX_BITS = 32; + this.OPERATOR_AMOUNT = operatorAmount; + this.BITS_PER_VAR = OConfig.codeBitsPerVar; + this.BITS_PER_OPERATOR = OConfig.codeBitsPerOperator; + this.NO_OPERATOR_MASK = 0xffffffff >>> this.BITS_PER_OPERATOR; + this.BITS_OF_TWO_VARS = this.BITS_PER_VAR * 2; + this.BITS_OF_FIRST_VAR = this.MAX_BITS - this.BITS_PER_VAR; + this.MAX_VAR = 1 << this.BITS_PER_VAR; + this.VAR_BITS_OFFS = this.MAX_BITS - this.BITS_PER_OPERATOR; + + this.BITS_OF_VAR0 = this.BITS_PER_OPERATOR; + this.BITS_OF_VAR1 = this.BITS_PER_OPERATOR + this.BITS_PER_VAR; + this.BITS_OF_VAR2 = this.BITS_PER_OPERATOR + 2 * this.BITS_PER_VAR; + this.BITS_OF_VAR3 = this.BITS_PER_OPERATOR + 3 * this.BITS_PER_VAR; + } + + /** + * Returns random number for byte code. We have to use >>> 0 at + * the end, because << operator works with signed 32bit numbers, + * but not with unsigned like we need + * @returns {number} + */ + static rand() { + return (Helper.rand(this.OPERATOR_AMOUNT) << (this.VAR_BITS_OFFS) | Helper.rand(this.NO_OPERATOR_MASK)) >>> 0; + } + + static getOperator(num) { + return num >>> this.VAR_BITS_OFFS; + } + + static setOperator(num, op) { + return (op << this.VAR_BITS_OFFS | (num & this.NO_OPERATOR_MASK)) >>> 0; + } + + static getVar(num, index = 0) { + return num << (this.BITS_PER_OPERATOR + index * this.BITS_PER_VAR) >>> this.BITS_OF_FIRST_VAR; + } + + static getVar0(num) { + return num << this.BITS_OF_VAR0 >>> this.BITS_OF_FIRST_VAR; + } + + static getVar1(num) { + return num << this.BITS_OF_VAR1 >>> this.BITS_OF_FIRST_VAR; + } + + static getVar2(num) { + return num << this.BITS_OF_VAR2 >>> this.BITS_OF_FIRST_VAR; + } + + static getVar3(num) { + return num << this.BITS_OF_VAR3 >>> this.BITS_OF_FIRST_VAR; + } + + /** + * Sets variable bits into value 'val' and returns updated full number. + * Example: _setVar(0xaabbccdd, 2, 0x3) -> 0x + * @param {Number} num Original number + * @param {Number} index Variable index + * @param {Number} val New variable value + * @returns {Number} + */ + static setVar(num, index, val) { + const bits = index * this.BITS_PER_VAR; + const lBits = this.VAR_BITS_OFFS - bits; + const rBits = this.BITS_PER_OPERATOR + bits + this.BITS_PER_VAR; + + return (num >>> lBits << lBits | val << (this.VAR_BITS_OFFS - bits - this.BITS_PER_VAR) | num << rBits >>> rBits) >>> 0; + } + + /** + * Returns specified bits from 32bit number. e.g.: getBits(0b11001100, 3, 2) -> 01 + * @param {Number} num + * @param {Number} start first bit offset + * @param {Number} len Amount of bits to get + * @return {Number} Cut bits (number) + */ + static getBits(num, start, len) { + return num << start >>> (this.MAX_BITS - len); + } +} + +module.exports = Num; \ No newline at end of file diff --git a/client/src/vm/NumSpec.js b/client/src/vm/NumSpec.js new file mode 100644 index 0000000..5934658 --- /dev/null +++ b/client/src/vm/NumSpec.js @@ -0,0 +1,147 @@ +let Num = require('./Num'); +let OConfig = require('./../manager/plugins/organisms/Config'); + +describe("client/src/organism/Num", () => { + const bpo = OConfig.codeBitsPerOperator; + const bpv = OConfig.codeBitsPerVar; + + beforeEach(() => {OConfig.codeBitsPerOperator = 8; OConfig.codeBitsPerVar = 2}); + afterEach( () => {OConfig.codeBitsPerOperator = bpo; OConfig.codeBitsPerVar = bpv}); + + describe('Checks initialization', () => { + it ('Checks init() method', () => { + Num.init(3); + expect(Num.OPERATOR_AMOUNT).toBe(3); + expect(Num.BITS_PER_VAR).toBe(OConfig.codeBitsPerVar); + expect(Num.BITS_PER_OPERATOR).toBe(OConfig.codeBitsPerOperator); + }); + }); + + describe('Checks rand() method', () => { + it('Checks if rand() generates a number', () => { + for (let i = 0; i < 10000; i++) { + const n = Num.rand(); + expect(typeof n).toBe('number'); + expect(n >= 0).toBe(true); + } + }); + it('Checks if rand() generates correct operator', () => { + for (let i = 0; i < 10000; i++) { + expect(Num.getOperator(Num.rand()) < Num.OPERATOR_AMOUNT).toBe(true); + } + }); + }); + + describe('Checks operator', () => { + it("Checking getting random zero operator", () => { + Num.init(0); + expect(Num.getOperator(Num.rand())).toEqual(0); + }); + it("Checking getting random operator (0..1)", () => { + Num.init(1); + const n = Num.getOperator(Num.rand()); + expect(n === 0 || n === 1).toEqual(true); + }); + it("Checking getting random operator with probability", () => { + Num.init(3); + let n; + + for (let i = 0; i < 10000; i++) { + n = Num.getOperator(Num.rand()); + expect(n >= 0 && n <= 2).toEqual(true); + } + }); + + it('Checking getOperator() method', () => { + const n = 0xabffffff; + Num.init(0xbb); + expect(Num.getOperator(n)).toEqual(0xab); + }); + + it('Checking setOperator() method', () => { + const n = 0xabffffff; + + Num.init(0xff); + expect(Num.setOperator(n, 0xbd)).toEqual(0xbdffffff); + expect(Num.setOperator(n, 0x00)).toEqual(0x00ffffff); + expect(Num.setOperator(n, 0x01)).toEqual(0x01ffffff); + expect(Num.setOperator(n, 0xff)).toEqual(0xffffffff); + }); + }); + + describe('Checks getVar() method', () => { + it('Checking getVar() method', () => { + Num.init(0xff); + let n = 0xabffffff; + + expect(Num.getVar(n, 0)).toEqual(3); + expect(Num.getVar(n, 1)).toEqual(3); + expect(Num.getVar(n, 3)).toEqual(3); + + n = 0xbcbfffff; + expect(Num.getVar(n, 0)).toEqual(2); + expect(Num.getVar(n, 1)).toEqual(3); + + n = 0xbc9fffff; + expect(Num.getVar(n, 0)).toEqual(2); + expect(Num.getVar(n, 1)).toEqual(1); + + n = 0xbc00ffff; + expect(Num.getVar(n, 0)).toEqual(0); + expect(Num.getVar(n, 1)).toEqual(0); + }); + it('Checking getVarX() methods', () => { + Num.init(0xff); + let n = 0xabffffff; + + expect(Num.getVar0(n)).toEqual(3); + expect(Num.getVar1(n)).toEqual(3); + expect(Num.getVar2(n)).toEqual(3); + + n = 0xbcbfffff; + expect(Num.getVar0(n)).toEqual(2); + expect(Num.getVar1(n)).toEqual(3); + + n = 0xbc9fffff; + expect(Num.getVar0(n)).toEqual(2); + expect(Num.getVar1(n)).toEqual(1); + expect(Num.getVar2(n)).toEqual(3); + expect(Num.getVar3(n)).toEqual(3); + + n = 0xbc00ffff; + expect(Num.getVar0(n)).toEqual(0); + expect(Num.getVar1(n)).toEqual(0); + expect(Num.getVar2(n)).toEqual(0); + expect(Num.getVar3(n)).toEqual(0); + + n = 0x00000000; + expect(Num.getVar0(n)).toEqual(0); + expect(Num.getVar1(n)).toEqual(0); + expect(Num.getVar2(n)).toEqual(0); + expect(Num.getVar3(n)).toEqual(0); + }); + }); + + describe('Checks setVar() method', () => { + it('Checking setVar() method', () => { + Num.init(0xff); + expect(Num.setVar(0xabffffff, 0, 2)).toEqual(0xabbfffff); + expect(Num.setVar(0xabffffff, 0, 3)).toEqual(0xabffffff); + expect(Num.setVar(0xabffffff, 0, 0)).toEqual(0xab3fffff); + expect(Num.setVar(0xabffffff, 2, 0)).toEqual(0xabf3ffff); + expect(Num.setVar(0xabffffff, 2, 2)).toEqual(0xabfbffff); + }); + }); + + describe('Checks getBits() method', () => { + it('Checking getBits() method', () => { + Num.init(0xff); + expect(Num.getBits(0xabffffff, 0, 8)).toEqual(0xab); + expect(Num.getBits(0xabffffff, 0, 4)).toEqual(0xa); + expect(Num.getBits(0xabffffff, 4, 4)).toEqual(0xb); + expect(Num.getBits(0xabfbffff, 12, 2)).toEqual(0x2); + expect(Num.getBits(0xabcdffff, 8, 8)).toEqual(0xcd); + expect(Num.getBits(0xabcdffff, 16, 8)).toEqual(0xff); + }); + }); +}); \ No newline at end of file diff --git a/client/src/jsvm/Operators.js b/client/src/vm/Operators.js similarity index 91% rename from client/src/jsvm/Operators.js rename to client/src/vm/Operators.js index 1e09374..ffff580 100644 --- a/client/src/jsvm/Operators.js +++ b/client/src/vm/Operators.js @@ -16,9 +16,9 @@ class Operators { */ this.vars = vars; /** - * {Observer} Observer for sending events outside + * {Observer} Observer for sending external events */ - this.obs = obs; + this.obs = obs; } destroy() { diff --git a/client/src/vm/VM.js b/client/src/vm/VM.js new file mode 100644 index 0000000..df075bd --- /dev/null +++ b/client/src/vm/VM.js @@ -0,0 +1,290 @@ +/** + * Simple Virtual Machine for DOS language. Runs code line by line till the + * last and calls operators associated callbacks. + * TODO: explain here code one number format,... + * + * @author flatline + * TODO: think about custom operators callbacks from outside. This is how + * TODO: we may solve custom tasks + */ +const Helper = require('./../../../common/src/Helper'); +const Observer = require('./../../../common/src/Observer'); +const OConfig = require('./../manager/plugins/organisms/Config'); +const EVENTS = require('./../../src/share/Events').EVENTS; +const EVENT_AMOUNT = require('./../../src/share/Events').EVENT_AMOUNT; +const Num = require('./Num'); +/** + * {Number} Maximum stack size, which may be used for recursion or function parameters + */ +const MAX_STACK_SIZE = 30000; + +class VM extends Observer { + /** + * Creates VM instance. parent is used if VM instance is in a + * cloning mode and we have to create a copy of it. + * @param {Observer} obs observer for external events firing + * @param {Function} operatorCls Class of operators + * @param {Array} weights Weights of operations + * @param {VM} parent Parent VM instance in case of cloning + */ + constructor(obs, operatorCls, weights, parent = null) { + super(EVENT_AMOUNT); + + this._obs = obs; + this._weights = weights; + /** + * {Function} Class of operators, with implementation of all available + * script parts for current VM instance + */ + this._operatorCls = operatorCls; + this._vars = parent && parent.vars && parent.vars.slice() || this._getVars(); + this._code = parent && parent.code.slice() || []; + this._line = parent && parent.line || 0; + /** + * {Array} Array of two numbers. first - line number where we have + * to return if first line appears. second - line number, where ends + * closing block '}' of block operator (e.g. for, if,...). + */ + this._offsets = [this._code.length]; + /** + * {Function} Class, which implement all supported operators + */ + this._operators = new operatorCls(this._offsets, this._vars, obs); + this._ops = this._operators.operators; + } + + get code() {return this._code} + get size() {return this._code.length} + get operators() {return this._operators} + get vars() {return this._vars} + get line() {return this._line} + + serialize() { + return { + // 'obs' field will be added after deserialization + // 'operatorsCls' field will be added after deserialization + // 'operators' field will be added after deserialization + // 'weights' field will be added after deserialization + offsets: this._offsets.slice(), + vars : this._vars.slice(), + code : this._code.slice(), + line : this._line + }; + } + + unserialize(json) { + this._offsets = json.offsets; + this._vars = json.vars; + this._code = json.code; + this._line = json.line; + this._operators = new this._operatorCls(this._offsets, this._vars, this._obs); + } + + /** + * Walks through code lines (32bit numbers) one by one and runs associated + * with line type callback. These callbacks interpret one line of code like: + * condition, loop, function call etc... + * @param {Organism} org Current organism + * @return {Number} Amount of run lines + */ + run(org) { + const code = this._code; + const lines = code.length; + if (lines < 1) {return 0} + const ops = this._ops; + const offs = this._offsets; + const period = OConfig.codeYieldPeriod; + const OFFS = Num.VAR_BITS_OFFS; + const WEIGHTS = this._weights; + let len = period; + let line = this._line; + let operator; + + while (len > 0 && org.energy > 0) { + operator = code[line] >>> OFFS; + line = ops[operator](code[line], line, org, lines); + // + // This is very important peace of logic. As big the organism is + // as more energy he spends + // + org.energy -= WEIGHTS[operator]; + // + // We found closing bracket '}' of some loop and have to return + // to the beginning of operator (e.g.: for) + // + while (offs.length > 1 && line === offs[offs.length - 1]) { + offs.pop(); + line = offs.pop(); + } + // + // We reach the end of the script and have to run it from the beginning + // + if (line >= lines && org.energy > 0) { + line = 0; + this._operators.offsets = this._offsets = [code.length]; + } + len--; + } + this._line = line; + + return period - len; + } + + destroy() { + this._ops = null; + this._operators.destroy && this._operators.destroy(); + this._operators = null; + this._offsets = []; + this._code = null; + this._vars = null; + this._operatorCls = null; + this._weights = null; + this._obs = null; + + super.destroy(); + } + + /** + * Does crossover between two parent byte codes. Takes second vm's code part + * (from start1 to end1 offset) and inserts it into first vm code part (start...end). + * For example: + * code1 : [1,2,3] + * code2 : [4,5,6] + * start : 1 + * end : 2 + * start1: 0 + * end1 : 2 + * vm1.crossover(vm2) // [4,5,6] instead [2,3] ->, vm1 === [1,4,5,6] + * + * @param {VM} vm VM instance, from where we have to cut code part + * @returns {Number} Amount of changes in current (this) vm + */ + crossover(vm) { + const rand = Helper.rand; + const len = this._code.length; + const len1 = vm.code.length; + let start = rand(len); + let end = rand(len); + let start1 = rand(len1); + let end1 = rand(len1); + let adds; + + if (start > end) {[start, end] = [end, start]} + if (start1 > end1) {[start1, end1] = [end1, start1]} + + adds = end1 - start1 - end + start; + if (this._code.length + adds >= OConfig.codeMaxSize) { + return 0 + } + this._code.splice(...[start, end - start + 1].concat(vm.code.slice(start1, end1 + 1))); + this._reset(); + + return adds; + } + + /** + * Takes few lines from itself and inserts them before or after copied + * part. All positions are random. + * @return {Number} Amount of added/copied lines + */ + copyLines() { + const rand = Helper.rand; + const code = this._code; + const codeLen = code.length; + const start = rand(codeLen); + const end = start + rand(codeLen - start); + // + // Because we use spread (...) operator stack size is important + // for amount of parameters and we shouldn't exceed it + // + if (end - start > MAX_STACK_SIZE) { + return 0; + } + // + // Organism size should be less them codeMaxSize + // + if (codeLen + end - start >= OConfig.codeMaxSize) {return 0} + // + // We may insert copied piece before "start" (0) or after "end" (1) + // + if (rand(2) === 0) { + code.splice(rand(start), 0, ...code.slice(start, end)); + this._reset(); + return end - start; + } + + code.splice(end + rand(codeLen - end + 1), 0, ...code.slice(start, end)); + this._reset(); + + return end - start; + } + + /** + * Inserts random generated number into the byte code at random position + */ + insertLine() { + this._code.splice(Helper.rand(this._code.length), 0, Num.rand()); + this._reset(); + } + + updateLine(index, number) { + this._code[index] = number; + this._reset(); + } + + /** + * Removes random generated number into byte vm at random position + */ + removeLine() { + this._code.splice(Helper.rand(this._code.length), 1); + this._reset(); + } + + getLine(index) { + return this._code[index]; + } + + /** + * Generates random code and inserts it starting from 'pos'. The + * length of generated code is 'size'. Final code length shouldn't + * be greater then original size. + * @param {Number} pos Insert position + * @param {Number} size generated code size + */ + generate(pos, size) { + const orgCode = this._code; + + size = pos + size; + while (pos < size) {orgCode[pos++] = Num.rand()} + this._reset(); + } + + _reset() { + this.fire(EVENTS.RESET_CODE); + this._line = 0; + this._operators.offsets = (this._offsets = [this._code.length]); + } + + /** + * Generates default variables vm. It should be in ES5 version, because + * speed is important. Amount of vars depends on OConfig.codeBitsPerVar config. + * @returns {Array} Filled variable array + */ + _getVars() { + if (this._vars && this._vars.length > 0) {return this._vars} + + const len = Math.pow(2, OConfig.codeBitsPerVar); + const range = OConfig.codeVarInitRange; + const range2 = range / 2; + const rand = Helper.rand; + let vars = new Array(len); + + for (let i = 0; i < len; i++) { + vars[i] = rand(range) - range2; + } + + return (this._vars = vars); + } +} + +module.exports = VM; \ No newline at end of file diff --git a/client/src/vm/VMSpec.js b/client/src/vm/VMSpec.js new file mode 100644 index 0000000..08fe69d --- /dev/null +++ b/client/src/vm/VMSpec.js @@ -0,0 +1,607 @@ +describe("client/src/organism/VM", () => { + const Observer = require('./../../../common/src/Observer'); + const EVENT_AMOUNT = require('./../../../client/src/share/Events').EVENT_AMOUNT; + const OrganismDos = require('./../manager/plugins/organisms/dos/Organism'); + const Helper = require('./../../../common/src/Helper'); + const THelper = require('./../../../common/tests/Helper'); + const OConfig = require('./../manager/plugins/organisms/Config'); + const VM = require('./VM'); + const Num = require('./Num'); + const Operators = require('./Operators'); + const OperatorsDos = require('./../manager/plugins/organisms/dos/Operators'); + let obs; + + beforeEach(() => obs = new Observer(10)); + afterEach (() => obs.destroy()); + + describe('Checking creation and destroy', () => { + it("Checking vm creation", () => { + let flag = false; + const vm = new VM(obs, () => flag = true, []); + + expect(flag).toEqual(true); + expect(vm.size).toEqual(0); + expect(vm.line).toEqual(0); + + vm.destroy(); + }); + + it("Checking parent argument and 'cloning' mode", () => { + const parent = new VM(obs, () => {}, []); + + parent.insertLine(); + const vm = new VM(obs, () => {}, [], parent); + + expect(vm.code).toEqual(parent.code); + expect(vm.size).toEqual(parent.size); + expect(vm.vars).toEqual(parent.vars); + expect(vm.line).toEqual(parent.line); + + parent.destroy(); + vm.destroy(); + }); + + it("Checking 'vars' getter for non 'cloning' mode", () => { + const vm = new VM(obs, () => {}, []); + + expect(vm.vars.length === Math.pow(2, OConfig.codeBitsPerVar)).toEqual(true); + + vm.destroy(); + }); + + it("Checking no code size", () => { + const vm = new VM(obs, () => {}, []); + + expect(vm.size).toEqual(0); + vm.run(); + expect(vm.size).toEqual(0); + + vm.destroy(); + }); + + it("Checking destroy", () => { + const vm = new VM(obs, () => {}, []); + + vm.destroy(); + expect(vm.code).toEqual(null); + }); + }); + + describe('Checking properties and getters', () => { + it("Checking 'code' and 'size' properties", () => { + const vm = new VM(obs, () => {}, []); + + expect(vm.code instanceof Array).toEqual(true); + expect(vm.size).toEqual(0); + + vm.insertLine(); + expect(vm.code instanceof Array).toEqual(true); + expect(vm.size).toEqual(1); + + vm.insertLine(); + expect(vm.size).toEqual(2); + vm.removeLine(); + expect(vm.size).toEqual(1); + vm.removeLine(); + expect(vm.size).toEqual(0); + + vm.destroy(); + }); + + it("Checking 'operators' property", () => { + const vm = new VM(obs, Operators, []); + + expect(vm.operators instanceof Operators).toEqual(true); + + vm.destroy(); + }); + }); + + describe('Checking serialization/deserialization', () => { + it('Checks serialization', () => { + const vm = new VM(obs, () => {}, []); + + vm.vars.splice(0, vm.vars.length, ...[0,1,2,3]); + expect(vm.serialize()).toEqual({ + offsets: [0], + vars : [0,1,2,3], + code : [], + line : 0 + }); + + vm.destroy(); + }); + it('Checks serialization 2', () => { + const vm = new VM(obs, () => {}, []); + + vm.vars.splice(0, vm.vars.length, ...[4,3,2,1]); + vm.insertLine(); + expect(vm.serialize()).toEqual({ + offsets: [1], + vars : [4,3,2,1], + code : [vm.code[0]], + line : 0 + }); + + vm.destroy(); + }); + + it('Checks deserialization', () => { + const vm = new VM(obs, Operators, []); + + vm.unserialize({ + offsets: [0], + vars : [0,1,2,3], + code : [], + line : 1 + }); + expect(vm.operators.offs).toEqual([0]); + expect(vm.vars).toEqual([0,1,2,3]); + expect(vm.code).toEqual([]); + expect(vm.line).toEqual(1); + + vm.destroy(); + }); + it('Checks deserialization 2', () => { + const vm = new VM(obs, Operators, []); + + vm.unserialize({ + offsets: [3], + vars : [4,1,2,3], + code : [0xaabbccdd], + line : 0 + }); + expect(vm.operators.offs).toEqual([3]); + expect(vm.vars).toEqual([4,1,2,3]); + expect(vm.code).toEqual([0xaabbccdd]); + expect(vm.line).toEqual(0); + + vm.destroy(); + }); + }); + + describe('run() method', () => { + it('Shouldn\'t work if code size is 0', () => { + const vm = new VM(obs, Operators, []); + const org = new OrganismDos('0', 0, 0, {}); + const energy = org.energy; + + vm.run(org); + expect(vm.line).toEqual(0); + expect(org.energy).toEqual(energy); + + org.destroy(); + vm.destroy(); + }); + it('Shouldn\'t work if no energy', () => { + const vm = new VM(obs, Operators, []); + const org = new OrganismDos('0', 0, 0, {}); + const energy = -1; + + org.energy = energy; + vm.insertLine(); + vm.run(org); + expect(vm.line).toEqual(0); + expect(org.energy).toEqual(energy); + + org.destroy(); + vm.destroy(); + }); + it('Should run correct operator callbacks', () => { + const period = OConfig.codeYieldPeriod; + OConfig.codeYieldPeriod = 1; + const vm = new VM(obs, OperatorsDos, []); + const org = new OrganismDos('0', 0, 0, {}); + let flag = false; + + vm.insertLine(); + vm.updateLine(0, 0x00000000); + vm.operators.operators[0] = () => flag = true; + vm.run(org); + expect(flag).toEqual(true); + + org.destroy(); + vm.destroy(); + OConfig.codeYieldPeriod = period; + }); + it('Should run the same amount as codeYieldPeriod', () => { + const period = OConfig.codeYieldPeriod; + OConfig.codeYieldPeriod = 3; + const vm = new VM(obs, OperatorsDos, OConfig.orgOperatorWeights); + const org = new OrganismDos('0', 0, 0, {}); + let flag = 0; + + vm.insertLine(); + vm.updateLine(0, 0x00000000); + vm.operators.operators[0] = () => {flag++; return 1}; + vm.run(org); + expect(flag).toEqual(3); + + org.destroy(); + vm.destroy(); + OConfig.codeYieldPeriod = period; + }); + it('Should decrease energy', () => { + const alive = OConfig.orgAlivePeriod; + const period = OConfig.codeYieldPeriod; + const energy = OConfig.orgStartEnergy; + const weights = OConfig.orgOperatorWeights.slice(); + const newWeights = [.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1,.1]; + OConfig.orgOperatorWeights.splice(0, OConfig.orgOperatorWeights.length, ...newWeights); + OConfig.codeYieldPeriod = 1; + OConfig.orgStartEnergy = 100; + OConfig.orgAlivePeriod = 100; + const vm = new VM(obs, OperatorsDos, OConfig.orgOperatorWeights); + const org = new OrganismDos('0', 0, 0, {}); + + vm.insertLine(); + vm.updateLine(0, 0x00000000); + expect(org.energy).toEqual(100); + vm.run(org); + expect(org.energy).toEqual(99.9); // 100 - .1 = 99.9 + + org.destroy(); + vm.destroy(); + OConfig.orgStartEnergy = energy; + OConfig.codeYieldPeriod = period; + OConfig.orgAlivePeriod = alive; + OConfig.orgOperatorWeights.splice(0, OConfig.orgOperatorWeights.length, ...weights); + }); + it('Should return correct amount of run lines', () => { + const obs = new Observer(EVENT_AMOUNT); + const vm = new VM(obs, OperatorsDos, OConfig.orgOperatorWeights); + const org = new OrganismDos('0', 0, 0, {}); + const period = OConfig.codeYieldPeriod; + + OConfig.codeYieldPeriod = 2; + vm.insertLine(); + expect(vm.run(org)).toEqual(2); + + org.destroy(); + vm.destroy(); + obs.destroy(); + OConfig.codeYieldPeriod = period; + }); + }); + + describe('Crossover', () => { + it("Checking crossover with increasing child code", () => { + const vm1 = new VM(obs, OperatorsDos, []); + const vm2 = new VM(obs, OperatorsDos, []); + const rand = Helper.rand; + let i = -1; + + Helper.rand = () => { + i++; + if (i === 0) {return 1} + if (i === 1) {return 2} + if (i === 2) {return 1} + if (i === 3) {return 3} + }; + + THelper.script(vm1, [16000000, 16000001, 16000002, 16000003, 16000004]); + THelper.script(vm2, [17000000, 17000001, 17000002, 17000003, 17000004]); + + i = -1; + vm1.crossover(vm2); + expect(vm1.code).toEqual([16000000, 17000001, 17000002, 17000003, 16000003, 16000004]); + + Helper.rand = rand; + vm1.destroy(); + vm2.destroy(); + }); + it("Checking crossover with decreasing child code", () => { + const vm1 = new VM(obs, OperatorsDos, []); + const vm2 = new VM(obs, OperatorsDos, []); + const rand = Helper.rand; + let i = -1; + + Helper.rand = () => { + i++; + if (i === 0) {return 1} + if (i === 1) {return 2} + if (i === 2) {return 1} + if (i === 3) {return 1} + }; + + THelper.script(vm1, [16000000, 16000001, 16000002, 16000003, 16000004]); + THelper.script(vm2, [17000000, 17000001, 17000002, 17000003, 17000004]); + + i = -1; + vm1.crossover(vm2); + expect(vm1.code).toEqual([16000000, 17000001, 16000003, 16000004]); + + Helper.rand = rand; + vm1.destroy(); + vm2.destroy(); + }); + it("Checking crossover with the same child code size", () => { + const vm1 = new VM(obs, OperatorsDos, []); + const vm2 = new VM(obs, OperatorsDos, []); + const rand = Helper.rand; + let i = -1; + + Helper.rand = () => { + i++; + if (i === 0) {return 1} + if (i === 1) {return 3} + if (i === 2) {return 1} + if (i === 3) {return 3} + }; + + THelper.script(vm1, [16000000, 16000001, 16000002, 16000003, 16000004]); + THelper.script(vm2, [17000000, 17000001, 17000002, 17000003, 17000004]); + + i = -1; + vm1.crossover(vm2); + expect(vm1.code).toEqual([16000000, 17000001, 17000002, 17000003, 16000004]); + Helper.rand = rand; + vm1.destroy(); + vm2.destroy(); + }); + it("Checking crossover with no code size in parents", () => { + const vm1 = new VM(obs, OperatorsDos, []); + const vm2 = new VM(obs, OperatorsDos, []); + + vm1.crossover(vm2); + expect(vm1.size).toEqual(0); + expect(vm2.size).toEqual(0); + + vm1.destroy(); + vm2.destroy(); + }); + it("Checking crossover with no code size for one parent and twp lines of code for other", () => { + const vm1 = new VM(obs, OperatorsDos, []); + const vm2 = new VM(obs, OperatorsDos, []); + const rand = Helper.rand; + let i = -1; + + Helper.rand = () => { + i++; + if (i === 0) {return 0} + if (i === 1) {return 0} + if (i === 2) {return 1} + if (i === 3) {return 2} + }; + + THelper.script(vm2, [17000000, 17000001, 17000002, 17000003]); + + i = -1; + vm1.crossover(vm2); + expect(vm1.code).toEqual([17000001, 17000002]); + + Helper.rand = rand; + vm1.destroy(); + vm2.destroy(); + }); + it("Checking crossover with no code size for one parent and twp lines of code for other 2", () => { + const vm1 = new VM(obs, OperatorsDos, []); + const vm2 = new VM(obs, OperatorsDos, []); + const rand = Helper.rand; + let i = -1; + + Helper.rand = () => { + i++; + if (i === 0) {return 1} + if (i === 1) {return 2} + if (i === 2) {return 0} + if (i === 3) {return 0} + }; + + THelper.script(vm1, [16000000, 16000001, 16000002, 16000003]); + + i = -1; + vm1.crossover(vm2); + expect(vm1.code).toEqual([16000000, 16000003]); + expect(vm2.size).toEqual(0); + + Helper.rand = rand; + vm1.destroy(); + vm2.destroy(); + }); + }); + + describe('copyLines() method', () => { + it('Checking copyLines() method', () => { + const vm = new VM(obs, OperatorsDos, []); + let rand = Helper.rand; + let i = -1; + + vm.insertLine(); + vm.insertLine(); + vm.insertLine(); + vm.insertLine(); + Helper.rand = function () { + i++; + if (i === 0) { // start + return 1; + } else if (i === 1) { // end + return 2; + } else if (i === 2) { // rand(2) + return 0; + } else if (i === 3) { // rand(start) + return 0; + } + }; + i = -1; + expect(vm.size).toEqual(4); + vm.copyLines(); + expect(vm.size).toEqual(6); + expect(vm.code[0]).toEqual(vm.code[3]); + expect(vm.code[1]).toEqual(vm.code[4]); + + Helper.rand = rand; + vm.destroy(); + }); + it('Checking copyLines() method 2', () => { + const vm = new VM(obs, OperatorsDos, []); + let rand = Helper.rand; + let i = -1; + + vm.insertLine(); + vm.insertLine(); + vm.insertLine(); + vm.insertLine(); + Helper.rand = function () { + i++; + if (i === 0) { // start + return 1; + } else if (i === 1) { // end + return 2; + } else if (i === 2) { // rand(2) + return 1; + } else if (i === 3) { // rand(codeLen - end) + return 0; + } + }; + i = -1; + expect(vm.size).toEqual(4); + vm.copyLines(); + expect(vm.size).toEqual(6); + expect(vm.code[1]).toEqual(vm.code[3]); + expect(vm.code[2]).toEqual(vm.code[4]); + + Helper.rand = rand; + vm.destroy(); + }); + it('Checking copyLines() method 3', () => { + const vm = new VM(obs, OperatorsDos, []); + let rand = Helper.rand; + let i = -1; + + vm.insertLine(); + vm.insertLine(); + vm.insertLine(); + vm.insertLine(); + Helper.rand = function () { + i++; + if (i === 0) { // start + return 1; + } else if (i === 1) { // end + return 2; + } else if (i === 2) { // rand(2) + return 1; + } else if (i === 3) { // rand(codeLen - end) + return 1; + } + }; + i = -1; + expect(vm.size).toEqual(4); + vm.copyLines(); + expect(vm.size).toEqual(6); + expect(vm.code[1]).toEqual(vm.code[4]); + expect(vm.code[2]).toEqual(vm.code[5]); + + Helper.rand = rand; + vm.destroy(); + }); + it('Checking copyLines() method with no code', () => { + const vm = new VM(obs, OperatorsDos, []); + let rand = Helper.rand; + + Helper.rand = () => 0; + expect(vm.size).toEqual(0); + vm.copyLines(); + expect(vm.size).toEqual(0); + + Helper.rand = rand; + vm.destroy(); + }); + }); + + describe('insertLine() method', () => { + it('Checking insertLine() method', () => { + const vm = new VM(obs, OperatorsDos, []); + + expect(vm.size).toEqual(0); + vm.insertLine(); + expect(vm.size).toEqual(1); + vm.insertLine(); + expect(vm.size).toEqual(2); + + vm.destroy(); + }); + it('Checking insertLine() method 2', () => { + const vm = new VM(obs, OperatorsDos, []); + let rand = Num.rand; + + Num.rand = () => 0xabcdefff; + expect(vm.size).toEqual(0); + vm.insertLine(); + expect(vm.size).toEqual(1); + + expect(vm.code[0]).toEqual(0xabcdefff); + + Num.rand = rand; + vm.destroy(); + }); + }); + + describe('updateLine() method', () => { + it('Checking updateLine() method', () => { + const vm = new VM(obs, OperatorsDos, []); + let rand = Num.rand; + + Num.rand = () => 0xabcdefff; + vm.insertLine(); + expect(vm.code[0]).toEqual(0xabcdefff); + + vm.updateLine(0, 0xffffffff); + expect(vm.code[0]).toEqual(0xffffffff); + + vm.updateLine(0, 0x12345678); + expect(vm.code[0]).toEqual(0x12345678); + + Num.rand = rand; + vm.destroy(); + }); + }); + + describe('removeLine() method', () => { + it('Checking removeLine() method', () => { + const vm = new VM(obs, OperatorsDos, []); + + vm.insertLine(); + expect(vm.size).toEqual(1); + vm.removeLine(); + expect(vm.size).toEqual(0); + + vm.destroy(); + }); + it('Checking removeLine() for empty code', () => { + const vm = new VM(obs, OperatorsDos, []); + + expect(vm.size).toEqual(0); + vm.removeLine(); + expect(vm.size).toEqual(0); + vm.removeLine(); + expect(vm.size).toEqual(0); + + vm.destroy(); + }); + }); + + describe('getLine() method', () => { + it('Checking getLine()', () => { + const vm = new VM(obs, OperatorsDos, []); + let get = Num.rand; + + Num.rand = () => 0xabcdefff; + expect(vm.size).toEqual(0); + expect(vm.getLine(0)).toEqual(undefined); + expect(vm.getLine(1)).toEqual(undefined); + vm.insertLine(); + expect(vm.size).toEqual(1); + expect(vm.getLine(0)).toEqual(0xabcdefff); + + vm.removeLine(); + expect(vm.size).toEqual(0); + expect(vm.getLine(0)).toEqual(undefined); + expect(vm.getLine(1)).toEqual(undefined); + expect(vm.getLine(9)).toEqual(undefined); + + Num.rand = get; + vm.destroy(); + }); + }); +}); \ No newline at end of file diff --git a/common/src/Configurable.js b/common/src/Configurable.js index 24aac64..dd516c9 100644 --- a/common/src/Configurable.js +++ b/common/src/Configurable.js @@ -78,7 +78,6 @@ class Configurable { * using api like this: man.api..show(). 'description' will be shown * if user types like this: man.api..show.desc. * @param {Object} apiCfg Object of name for key and String or Array for value - * @private */ _updateApi(apiCfg) { const api = this._parent.api; @@ -98,8 +97,7 @@ class Configurable { const isStr = typeof key === 'string'; let desc = isStr && 'No description' || key[1]; - cfg[c] = (isStr && this[key] || this[key[0]]).bind(this); - cfg[c].desc = desc; + Helper.setApi(cfg, c, (isStr && this[key] || this[key[0]]).bind(this), desc); } } diff --git a/common/src/Directions.js b/common/src/Directions.js index d175171..07604e9 100644 --- a/common/src/Directions.js +++ b/common/src/Directions.js @@ -18,5 +18,10 @@ const NAMES = { 3: 'Left', 4: 'No' }; +/** + * {Array} Array of flipped directions. Is used for connecting with nearest + * servers: left -> right, up -> down, right -> left, down -> up + */ +const FLIP_DIR = [DIR.DOWN, DIR.LEFT, DIR.UP, DIR.RIGHT]; -module.exports = {DIR, NAMES}; \ No newline at end of file +module.exports = {DIR, FLIP_DIR, NAMES}; \ No newline at end of file diff --git a/common/src/FastArray.js b/common/src/FastArray.js new file mode 100644 index 0000000..24b5122 --- /dev/null +++ b/common/src/FastArray.js @@ -0,0 +1,114 @@ +/** + * Implementation of fast array. First assumption of this class is in fixed array + * size. Second that get() method will be called must more times, then set() or + * del() or resize(). Resize is possible, but should be rare to keep it fast. Is + * used for storing organisms population. This class doesn't check size overflow + * due performance issue. Removing element means setting 0 to specified index. + * This class should not be used for storing numbers! + * + * @author flatline + */ +class FastArray { + constructor(size) { + /** + * {Array} Source container for custom objects + */ + this._arr = new Array(size); + /** + * {Array} Array of free indexes in _arr. Every time + * user calls del() method _arr obtains hole in it. + * Index of this hole wil be stored in this array + */ + this._freeIndexes = new Array(size); + /** + * {Number} Index of last free index in _freeIndexes array + */ + this._index = size - 1; + /** + * {Number} Allocated size of array. This is maximum amount + * of elements, which may be stored in FastArray + */ + this._size = size; + + for (let i = 0; i < size; i++) { + this._freeIndexes[i] = i; + this._arr[i] = 0; + } + } + + destroy() { + this._arr = null; + this._freeIndexes = null; + this._size = 0; + } + + /** + * Analog of Array.length + * @returns {Number} Amount of not empty elements in FastArray. + * Not all cells in an array may be filled by values. + */ + get length() {return this._size - this._index - 1} + + /** + * Returns allocated size + * @returns {Number} + */ + get size() {return this._size} + + /** + * Returns next free index in FastArray + * @returns {Number} + */ + get freeIndex() { + return this._freeIndexes[this._index]; + } + + /** + * Sets value to FastArray. You can't set value index due to + * optimization reason. Only a value + * @param {*} v Any value except number + */ + set(v) {this._arr[this._freeIndexes[this._index--]] = v} + + /** + * Returns a value by index + * @param {Number} i Value index + * @returns {*} + */ + get(i) {return this._arr[i]} + + /** + * Removes a value by index + * @param {Number} i Value index + */ + del(i) { + if (this._arr !== 0) + this._arr[i] = 0; + this._freeIndexes[++this._index] = i; + } + + /** + * Returns last added value by set() method + * @returns {*} Value + */ + lastAdded() { + return this._arr[this._freeIndexes[this._index + 1]]; + } + + /** + * Resizes an array. Values will not be removed during resize. + * This method is very slow and should be called not often. + * @param {Number} size New array size + */ + resize(size) { + const indexes = this._freeIndexes; + const arr = this._arr; + this._index = -1; + arr.length = indexes.length = (this._size = size); + for (let i = 0; i < size; i++) { + (arr[i] === 0) && (indexes[++this._index] = i); + } + } +} + +module.exports = FastArray; \ No newline at end of file diff --git a/common/src/Helper.js b/common/src/Helper.js index 47f81b5..21e8d2d 100644 --- a/common/src/Helper.js +++ b/common/src/Helper.js @@ -3,6 +3,7 @@ * * @author flatline */ +const _each = require('lodash/each'); const Config = require('./../../client/src/share/Config').Config; const DIR = require('./Directions').DIR; @@ -48,7 +49,34 @@ class Helper { } /** - * Sets first letter to lover case + * Apply styles packed in object. key: style name, val: style value + * @param {Element|String} el Element to apply styles or tag name to create + * @param {Object} styles Styles object + * @return {Element} Element with applied styles + */ + static setStyles(el, styles) { + el = typeof el === 'string' ? document.createElement(el) : el; + const style = el.style; + + _each(styles, (val, name) => style[name] = val); + + return el; + } + + /** + * Sets API function, which may be used by users in Chrome console (in DevTools) + * @param {Object} obj Destination object + * @param {String} name Name of function/property + * @param {Function|*} fn Function or value (if property) + * @param {String} desc Description + */ + static setApi(obj, name, fn, desc = '') { + obj[name] = fn; + Helper.isFunc(fn) && (obj[name].desc = desc); + } + + /** + * Sets first letter to lower case * @param {String} s * @returns {String} */ @@ -66,12 +94,14 @@ class Helper { obj[fnName] = fn.fn; delete fn.fn; } + /** * Generates random Int number in range 0:n-1 * @param {Number} n Right number value in a range * @return {Number} */ static rand(n) {return Math.trunc(Math.random() * n)} + /** * It calculates probability index from variable amount of components. * Let's imagine we have two actions: one and two. We want @@ -104,11 +134,13 @@ class Helper { return i; } + /** * Checks if position is empty. x == y == 0 - this is empty * @param {Object} pos Position to check */ static empty(pos) {return pos.x === 0 && pos.y === 0} + /** * Does normalization of X and Y coordinates. It's used * in cyclical mode for checking if we out of bound (world). @@ -133,6 +165,70 @@ class Helper { return [x, y, dir]; } + /** + * Does normalization of X and Y coordinates. It's used + * in cyclical mode for checking if we out of bound (world). + * In non cyclical mode it just returns the same coordinates. + * Usage: [x, y] = Helper.normalizeNoDir(10, -1); // 10, 100 (height - 1) + * 'dir' parameter means 'direction' and will be set only if + * one or two coordinates are out of bounds (world). Otherwise + * 'dir' parameter will be set to DIR.NO value. + * @param {Number} x + * @param {Number} y + * @returns {[x,y]} + */ + static normalizeNoDir(x, y) { + if (x < 0) {x = Config.worldWidth - 1} + else if (x >= Config.worldWidth) {x = 0} + + if (y < 0) {y = Config.worldHeight - 1} + else if (y >= Config.worldHeight) {y = 0} + + return [x, y]; + } + + /** + * Checks if specified coordinates are within current world + * @param {Number} x + * @param {Number} y + * @returns {Boolean} + */ + static inWorld(x, y) { + return x >= 0 && x < Config.worldWidth && y >= 0 && y < Config.worldHeight; + } + + /** + * Flips X or Y to the opposite coordinates. e.g: + * width=10, height=10, x=0, y=1 -> x=9, y=1 + * width=10, height=10, x=3, y=9 -> x=3, y=0 + * @param {Number} x X coordinate + * @param {Number} y Y coordinate + * @param {Number} dir Moving direction + */ + static flip(x, y, dir) { + const xMap = {0: Config.worldWidth - 1, [Config.worldWidth - 1]: 0}; + const yMap = {0: Config.worldHeight - 1, [Config.worldHeight - 1]: 0}; + + return [(dir === DIR.LEFT || dir === DIR.RIGHT) && typeof(xMap[x]) !== 'undefined' ? xMap[x] : x, (dir === DIR.UP || dir === DIR.DOWN) && typeof(yMap[y]) !== 'undefined' ? yMap[y] : y]; + } + + /** + * Flips region according to moving direction + * @param {Array} region Old region + * @param {Number} dir Moving direction + * @returns {Array} New region + */ + static flipRegion(region, dir) { + const newReg = [region[0], region[1]]; + + if (dir === DIR.UP) {newReg[1]--} + else if (dir === DIR.RIGHT) {newReg[0]++} + else if (dir === DIR.DOWN) {newReg[1]++} + else if (dir === DIR.LEFT) {newReg[0]--} + + return newReg; + } + /** * Analog of jQuery.isNumeric() * @param {*} n Value to check @@ -142,6 +238,10 @@ class Helper { return !isNaN(parseFloat(n)) && isFinite(n); } + static isFunc(v) { + return typeof v === 'function'; + } + /** * Generates unique numeric ids * @returns {Number} @@ -150,10 +250,6 @@ class Helper { return ++this._id; } - static isFunc(v) { - return typeof v === 'function'; - } - // TODO: will be used later // /** // * Saves custom data into the file. If file exists, it will diff --git a/common/src/Observer.js b/common/src/Observer.js index 37fe435..6e525a0 100644 --- a/common/src/Observer.js +++ b/common/src/Observer.js @@ -1,7 +1,9 @@ /** * Observer implementation. May fire, listen(on()) and clear all the event * handlers. This class is optimized for speed. This is why it works with - * array of numbers as events instead of frequent strings. + * array of numbers as events instead of frequent strings and uses Object + * for storing event handlers. It fast on fire and slow on removing event + * handlers (off() method). * * Usage: * import {EVENTS} from '.../Events.js' @@ -24,33 +26,41 @@ class Observer { this._resetEvents(); } - on (event, handler) { - if (typeof(this._handlers[event]) === 'undefined') {return false} - this._handlers[event].push(handler); + on(event, handler) { + const eventObj = this._handlers[event]; + if (typeof(eventObj) === 'undefined') {return false} + eventObj[eventObj.amount++] = handler; return true; } - off (event, handler) { - let index; + off(event, handler) { let handlers = this._handlers[event]; + let len = handlers && handlers.amount; - if ((index = handlers.indexOf(handler)) < 0) {return false} - handlers.splice(index, 1); + for (let i = 0; i < len; i++) { + if (handlers[i] === handler) { + handlers.amount = len - 1; + handlers[i] = handlers[i+1]; + delete handlers[handlers.amount]; + return true; + } + } - return true; + return false; } /** * This method is a most frequently called one. So we have to * optimize it as much as possible - * @param {Number} event Event number + * @param {Number} event Event number. Not string * @param {*} args List of arguments - * @param args */ - fire (event, ...args) { - let handlers = this._handlers[event] || []; - for (let handler of handlers) {handler(...args)} + fire(event, ...args) { + const handlers = this._handlers[event] || {}; + for (let i = 0; i < +handlers.amount; i++) { + handlers[i](...args); + } } /** @@ -58,18 +68,18 @@ class Observer { * to use on()/off() methods for working with events, but max * event index set in constructor will be the same. */ - clear () { + clear() { this._resetEvents(); } destroy() { - this.clear(); + this._handlers = {}; } _resetEvents() { const len = this._maxIndex; - const handlers = this._handlers = new Array(len); - for (let i = 0; i < len; i++) {handlers[i] = []} + const handlers = this._handlers = {}; + for (let i = 0; i < len; i++) {handlers[i] = {amount: 0}} } } diff --git a/common/src/ObserverSpec.js b/common/src/ObserverSpec.js index 53a08ed..26fd8a2 100644 --- a/common/src/ObserverSpec.js +++ b/common/src/ObserverSpec.js @@ -1,8 +1,5 @@ -// TODO: add two events test describe("common/src/share/Observer", () => { let Observer = require('./Observer'); - let Config = require('./../../client/src/share/Config').Config; - let Console = require('./../../client/src/share/Console'); const EVENT = 0; const EVENT2 = 1; let obs; @@ -10,162 +7,184 @@ describe("common/src/share/Observer", () => { beforeEach(() => obs = new Observer(2)); afterEach(() => obs = null); - it("Checking empty Observer creation", () => { - let o = new Observer(); - let flag = false; - - function handler() {flag = true} - Console.mode(Config.QUIET_NO); - o.on(EVENT, handler); - expect(flag).toEqual(false); - o.fire(EVENT); - expect(flag).toEqual(false); - Console.mode(Config.QUIET_IMPORTANT); + describe('Creation/Destroy', () => { + it("Checking empty Observer creation", () => { + let o = new Observer(); + let flag = false; + + function handler() {flag = true} + o.on(EVENT, handler); + expect(flag).toEqual(false); + o.fire(EVENT); + expect(flag).toEqual(false); + }); + it("Destroy Observer creation", () => { + let flag = false; + + function handler() {flag = true} + obs.on(EVENT, handler); + obs.fire(EVENT); + expect(flag).toEqual(true); + + flag = false; + obs.destroy(); + obs.fire(EVENT); + expect(flag).toEqual(false); + }); }); - it("Checking on()/fire() methods", () => { - let flag = false; - function handler() {flag = true} - - obs.on(EVENT, handler); - obs.fire(EVENT); - expect(flag).toEqual(true); - }); - it("Checking on()/fire() for two events", () => { - let flag = false; - - function handler() {flag = true} - - obs.on(EVENT, handler); - obs.on(EVENT2, handler); - obs.fire(EVENT); - flag = false; - obs.fire(EVENT2); - expect(flag).toEqual(true); - - flag = false; - obs.clear(); - obs.fire(EVENT); - obs.fire(EVENT2); - expect(flag).toEqual(false); - }); - it("Checking on() without firing", () => { - let flag = false; - - function handler() {flag = true} - - obs.on(EVENT, handler); - expect(flag).toEqual(false); - }); - it("Checking off() method", () => { - let flag = false; - - function handler() {flag = true} - - obs.on(EVENT, handler); - expect(obs.off(EVENT, handler)).toEqual(true); - obs.fire(EVENT); - expect(flag).toEqual(false); - }); - it("Checking off() method with incorrect event id", () => { - let flag = false; - - function handler() {flag = true} - - obs.on(EVENT, handler); - expect(obs.off(EVENT2, handler)).toEqual(false); - obs.fire(EVENT); - expect(flag).toEqual(true); - }); - it("Checking double off() method", () => { - let flag = false; - - function handler() {flag = true} - - obs.on(EVENT, handler); - expect(obs.off(EVENT, handler)).toEqual(true); - expect(obs.off(EVENT, handler)).toEqual(false); - obs.fire(EVENT); - expect(flag).toEqual(false); - }); - it("Checking off() with no handlers", () => { - let flag = false; - - function handler() {flag = true} - - expect(obs.off(EVENT, handler)).toEqual(false); - obs.fire(EVENT); - expect(flag).toEqual(false); + describe('on/fire/off() methods', () => { + it("Checking on()/fire() methods", () => { + let flag = false; + + function handler() {flag = true} + + obs.on(EVENT, handler); + obs.fire(EVENT); + expect(flag).toEqual(true); + }); + it("Checking on()/fire() for two events", () => { + let flag = false; + + function handler() {flag = true} + + obs.on(EVENT, handler); + obs.on(EVENT2, handler); + obs.fire(EVENT); + flag = false; + obs.fire(EVENT2); + expect(flag).toEqual(true); + + flag = false; + obs.clear(); + obs.fire(EVENT); + obs.fire(EVENT2); + expect(flag).toEqual(false); + }); + it("Checking on() without firing", () => { + let flag = false; + + function handler() {flag = true} + + obs.on(EVENT, handler); + expect(flag).toEqual(false); + }); + it("Checking off() method", () => { + let flag = false; + + function handler() {flag = true} + + obs.on(EVENT, handler); + expect(obs.off(EVENT, handler)).toEqual(true); + obs.fire(EVENT); + expect(flag).toEqual(false); + }); + it("Checking off() method with incorrect event id", () => { + let flag = false; + + function handler() {flag = true} + + obs.on(EVENT, handler); + expect(obs.off(EVENT2, handler)).toEqual(false); + obs.fire(EVENT); + expect(flag).toEqual(true); + }); + it("Checking double off() method", () => { + let flag = false; + + function handler() {flag = true} + + obs.on(EVENT, handler); + expect(obs.off(EVENT, handler)).toEqual(true); + expect(obs.off(EVENT, handler)).toEqual(false); + obs.fire(EVENT); + expect(flag).toEqual(false); + }); + it("Checking off() with no handlers", () => { + let flag = false; + + function handler() {flag = true} + + expect(obs.off(EVENT, handler)).toEqual(false); + obs.fire(EVENT); + expect(flag).toEqual(false); + }); + it("Checking two event handlers", () => { + let inc = 0; + + function handler1() {inc++} + function handler2() {inc++} + + obs.on(EVENT, handler1); + obs.on(EVENT, handler2); + obs.fire(EVENT); + expect(inc).toEqual(2); + }); }); - it("Checking two event handlers", () => { - let inc = 0; - function handler1() {inc++} - function handler2() {inc++} - - obs.on(EVENT, handler1); - obs.on(EVENT, handler2); - obs.fire(EVENT); - expect(inc).toEqual(2); - }); - it("Checking complex behavior", () => { - let inc = 0; - - function handler1() {inc++} - function handler2() {inc++} - - obs.on(EVENT, handler1); - obs.on(EVENT, handler1); - obs.on(EVENT, handler2); - expect(obs.off(EVENT, handler1)).toEqual(true); - obs.fire(EVENT); - expect(inc).toEqual(2); - expect(obs.off(EVENT, handler1)).toEqual(true); - obs.fire(EVENT); - expect(inc).toEqual(3); - }); - it("Checking complex behavior 2", () => { - let inc = 0; - - function handler1() {inc++} - function handler2() {inc++} - - obs.on(EVENT, handler1); - obs.on(EVENT, handler1); - obs.on(EVENT, handler2); - obs.fire(EVENT); - obs.fire(EVENT); - expect(inc).toEqual(6); + describe('Complex tests', () => { + it("Checking complex behavior", () => { + let inc = 0; + + function handler1() {inc++} + function handler2() {inc++} + + obs.on(EVENT, handler1); + obs.on(EVENT, handler1); + obs.on(EVENT, handler2); + expect(obs.off(EVENT, handler1)).toEqual(true); + obs.fire(EVENT); + expect(inc).toEqual(2); + expect(obs.off(EVENT, handler1)).toEqual(true); + obs.fire(EVENT); + expect(inc).toEqual(3); + }); + it("Checking complex behavior 2", () => { + let inc = 0; + + function handler1() {inc++} + function handler2() {inc++} + + obs.on(EVENT, handler1); + obs.on(EVENT, handler1); + obs.on(EVENT, handler2); + obs.fire(EVENT); + obs.fire(EVENT); + expect(inc).toEqual(6); + }); }); - it("Checking clear() method", () => { - let inc = 0; - - function handler1() {inc++} - function handler2() {inc++} - - obs.on(EVENT, handler1); - obs.on(EVENT, handler2); - obs.clear(); - obs.fire(EVENT); - expect(inc).toEqual(0); - - obs.on(EVENT, handler1); - obs.fire(EVENT); - expect(inc).toEqual(1); - }); - it("Checking clear() method and fire with incorrect event id", () => { - let inc = 0; - - function handler() {inc++} - - obs.on(EVENT, handler); - obs.clear(); - obs.fire(EVENT); - obs.fire(EVENT2); - expect(inc).toEqual(0); - obs.on(EVENT, handler); - obs.fire(EVENT2); - expect(inc).toEqual(0); + describe('clear() method', () => { + it("Checking clear() method", () => { + let inc = 0; + + function handler1() {inc++} + function handler2() {inc++} + + obs.on(EVENT, handler1); + obs.on(EVENT, handler2); + obs.clear(); + obs.fire(EVENT); + expect(inc).toEqual(0); + + obs.on(EVENT, handler1); + obs.fire(EVENT); + expect(inc).toEqual(1); + }); + it("Checking clear() method and fire with incorrect event id", () => { + let inc = 0; + + function handler() {inc++} + + obs.on(EVENT, handler); + obs.clear(); + obs.fire(EVENT); + obs.fire(EVENT2); + expect(inc).toEqual(0); + + obs.on(EVENT, handler); + obs.fire(EVENT2); + expect(inc).toEqual(0); + }); }); }); \ No newline at end of file diff --git a/common/src/Queue.js b/common/src/Queue.js index 9c22dec..58caf91 100644 --- a/common/src/Queue.js +++ b/common/src/Queue.js @@ -50,6 +50,29 @@ class Queue { this._first.val = val; } + /** + * The same like add(), but inserts after specified item in a queue + * @param {Object} item Item, after which val will be inserted + * @param {*} val Value to insert + * @return {Object} Inserted item + */ + addAfter(item, val) { + if (item === this._last) {this.add(val); return this._last} + if (this._size++ > 0) { + const newItem = { + prev: item, + next: item.next, + val + }; + item.next.prev = newItem; + item.next = newItem; + return newItem; + } + this._first.val = val; + + return this._first; + } + /** * Removes specified item from the queue. 'item' parameter is not * the same as value inside this item. Remember remove position may diff --git a/common/src/net/Api.js b/common/src/net/Api.js index bbaaaa6..6caf183 100644 --- a/common/src/net/Api.js +++ b/common/src/net/Api.js @@ -31,10 +31,9 @@ class Api { */ this.api = {}; this.parent = parent; - /** * {WebSocket} Currently active socket. It's available only during - * message is received + * message is received in _onMessage() method */ this._sock = null; this._onMessageCb = this._onMessage.bind(this); @@ -59,7 +58,6 @@ class Api { * answers. * @param {WebSocket} sock Communication socket * @param {Event} event Event with parameters obtained from the client - * @private */ _onMessage(sock, event) { const data = JSON.parse(event.data || event); diff --git a/common/src/net/Client.js b/common/src/net/Client.js new file mode 100644 index 0000000..eee5b5f --- /dev/null +++ b/common/src/net/Client.js @@ -0,0 +1,95 @@ +/** + * Base class for clients. Implements WebSocket client logic. Work in pair with + * server/src/server/Server class. Work in browser and under Node.js. + * + * @author flatline + */ +const Connection = require('./../../../common/src/net/Connection').Connection; +const EVENTS = require('./../../../common/src/net/Connection').EVENTS; +const EVENTS_LEN = Object.keys(EVENTS).length; +const OPEN = EVENTS_LEN; + +const CLIENT_EVENTS = Object.assign({ + OPEN +}, EVENTS); + +class Client extends Connection { + /** + * @param {String} host Host of server + * @param {Number} port Port number + * @param {Boolean} nodeJs true if client is running under Node.js + * @param {Object} events Events map + */ + constructor(host, port, nodeJs, events = null) { + super(Object.keys(events || CLIENT_EVENTS).length); + this.EVENTS = events || CLIENT_EVENTS; + this.host = host; + this.port = port; + this._nodeJs = nodeJs; + } + + run() { + if (this.active) {return} + this._client = this._createWebSocket(); + this._client.onerror = this.onError.bind(this); + this._client.onclose = this.onClose.bind(this); + this._client.onopen = this.onOpen.bind(this); + } + + stop() {this.active && this._client.close()} + + get socket() {return this._client} + + destroy() { + this.stop(); + if (this._client) { + this._client.onclose = null; + this._client.onmessage = null; + this._client.onerror = null; + } + + super.destroy(); + } + + /** + * Is called on connection close with server. Close reason will be in + * this.closeReason field after calling super.onClose() method + * @param {Event} event + * @override + */ + onClose(event) { + super.onClose(event); + this.active = false; + } + + /** + * Is called after client has connected to the server + * @param {Event} event + * @override + */ + onOpen(event) { + this.active = true; + this._client.onmessage = this.onMessage.bind(this, this._client); + this.fire(OPEN, event); + } + + _createWebSocket() { + // + // In browser we use browser's native WS implementation. Under Node.js + // we use implementation of 'ws' library + // + const WS = this._nodeJs ? require('ws') : window.WebSocket; + let client = null; + + try { + client = new WS(`${this.host}:${this.port}`); + client.on('error', (e) => this.fire(this.EVENTS.ERR, e)); + } catch (e) { + this.fire(this.EVENTS.ERR, e.message); + } + + return client; + } +} + +module.exports = {Client, EVENTS: CLIENT_EVENTS}; \ No newline at end of file diff --git a/common/src/net/Connection.js b/common/src/net/Connection.js index bb2e2b7..47cc7ff 100644 --- a/common/src/net/Connection.js +++ b/common/src/net/Connection.js @@ -63,7 +63,6 @@ class Connection extends Observer { * @param {Number} type Request type (see Requests.TYPES) * @param {*} params Array of parameters * @return {Number} Unique request id - * @abstract */ request(sock, type, ...params) {this.fire(REQUEST, sock, type, ...params)} @@ -74,7 +73,6 @@ class Connection extends Observer { * @param {Number} type Request type (see Requests.TYPES) * @param {Number} reqId Unique request id, returned by send() method * @param {Array} params Custom parameters to send - * @abstract */ response(sock, type, reqId, ...params) {this.fire(RESPONSE, sock, type, reqId, ...params)} @@ -82,7 +80,6 @@ class Connection extends Observer { * Is called every time if server/client sends us a request or response (response). * @param {WebSocket} sock Socket, received the message * @param {Event} event Message event. Data is in 'data' property - * @abstract */ onMessage(sock, event) { this.fire(MSG, sock, event); @@ -91,7 +88,6 @@ class Connection extends Observer { /** * Is called on every error during web sockets work * @param {Event} event Error event - * @abstract */ onError(event) { this.fire(ERR, event); diff --git a/common/src/net/Requests.js b/common/src/net/Requests.js index 3184348..fa2e01c 100644 --- a/common/src/net/Requests.js +++ b/common/src/net/Requests.js @@ -17,16 +17,19 @@ const TYPES = { // // Requests section // - REQ_MOVE_ORG : 0, - REQ_GIVE_ID : 1, - REQ_SET_NEAR_ACTIVE: 2, - REQ_GET_ID : 3, + REQ_MOVE_ORG : 0, // Organism moves from one world(manager) to another(sibling) + REQ_MOVE_ORG_BACK : 1, // Organism sends back to source client\server + REQ_SET_NEAR_ACTIVE : 2, // Other (near) server wants to connect with current one + REQ_GET_ID : 3, // Client wants obtain it's unique id from server + REQ_MOVE_ORG_FROM_SERVER : 4, // Organism came from near server // // Responses section // - RES_MOVE_ERR : 1000, - RES_INVALID_TYPE : 1001, - RES_GET_ID_OK : 1002 + RES_MOVE_ERR : 1000, + RES_MOVE_OK : 1001, + RES_INVALID_TYPE : 1002, + RES_GET_ID_OK : 1003, + RES_SET_NEAR_ACTIVE_OK : 1004 }; module.exports = {TYPES, MASKS}; \ No newline at end of file diff --git a/client/src/manager/plugins/client/plugins/Async.js b/common/src/plugins/AsyncClient.js similarity index 81% rename from client/src/manager/plugins/client/plugins/Async.js rename to common/src/plugins/AsyncClient.js index 68e1d4d..aa63b31 100644 --- a/client/src/manager/plugins/client/plugins/Async.js +++ b/common/src/plugins/AsyncClient.js @@ -1,14 +1,16 @@ /** - * Implements asynchronous interface of `Async` class for `Client'. See - * `common/src/plugins/Async` class for details. + * Implements asynchronous interface of `AsyncChild` class for `Client'. See + * `common/src/plugins/AsyncChild` class for details. It tracks client states + * like 'running', 'stopping', 'failed' etc and adds appropriate methods to + * parent: isRunning(), isStopping(),... * * @author flatline */ -const AsyncChild = require('./../../../../../../common/src/plugins/AsyncChild'); -const Helper = require('./../../../../../../common/src/Helper'); +const AsyncChild = require('./AsyncChild'); +const Helper = require('./../Helper'); -class Async extends AsyncChild { - constructor(parent) { +class AsyncClient extends AsyncChild { + constructor(parent, cfg = null) { super(parent); const EVENTS = parent.EVENTS; @@ -16,13 +18,14 @@ class Async extends AsyncChild { this._running = false; this._stopping = false; this._failed = false; + this._openEvent = cfg && cfg.openEvent || EVENTS.GET_ID; this._onOpenCb = this._onOpen.bind(this); this._onCloseCb = this._onClose.bind(this); this._onRunCb = this._onRun.bind(this); this._onStopCb = this._onStop.bind(this); - parent.on(EVENTS.GET_ID, this._onOpenCb); + parent.on(this._openEvent, this._onOpenCb); parent.on(EVENTS.CLOSE, this._onCloseCb); parent.on(EVENTS.DESTROY, this._onCloseCb); @@ -95,4 +98,4 @@ class Async extends AsyncChild { } } -module.exports = Async; \ No newline at end of file +module.exports = AsyncClient; \ No newline at end of file diff --git a/common/src/plugins/AsyncParent.js b/common/src/plugins/AsyncParent.js index 802fe29..b5a35a8 100644 --- a/common/src/plugins/AsyncParent.js +++ b/common/src/plugins/AsyncParent.js @@ -11,17 +11,17 @@ * @author flatline */ /** - * {Number} Amount of milliseconds we are waiting for one plugin to start or + * {Number} Amount of nanoseconds we are waiting for one plugin to start or * stop. In case of timeout exception will be thrown. */ -const WAIT_TIMEOUT_MS = 35000; +const WAIT_TIMEOUT_NS = 35 * 1e9; const CHECK_INTERVAL_MS = 50; class AsyncParent { constructor(parent, cfg = {}) { this._parent = parent; this._cfg = cfg; - this._plugins = parent.plugins; + this._plugins = cfg.classes || parent.plugins; this._destroying = false; this._done = null; /** @@ -59,7 +59,7 @@ class AsyncParent { for (let p of plugins) { if (p.isAsync) { - const id = Date.now(); + const id = this._now(); waitMap[id] = setInterval(this._onInterval.bind(this, id, p, run, done), CHECK_INTERVAL_MS); } } @@ -69,12 +69,24 @@ class AsyncParent { _onInterval(id, plugin, run, done) { if (plugin.isFailed() || plugin.isActive() === run) {this._clearWaiter(id)} if (!this._hasWaiters()) {return this._onDone(run, done)} - if (Date.now() - id > WAIT_TIMEOUT_MS) { + if (this._now() - id > WAIT_TIMEOUT_NS) { this._clearWaiter(id); throw `Async waiting timeout. Plugin: ${plugin.constructor.name}`; } } + /** + * Returns time stamp in nanoseconds. Works under browser and Node.js + * @return {Number} Nanoseconds time stamp + */ + _now() { + if (this._cfg.isBrowser) { + return window.performance.now(); + } + const now = process.hrtime(); + return now[0] * 1e9 + now[1]; + } + _clearWaiter(id) { clearInterval(this._waitMap[id]); delete this._waitMap[id]; diff --git a/common/src/plugins/Plugins.js b/common/src/plugins/Plugins.js index a563449..45bb4e0 100644 --- a/common/src/plugins/Plugins.js +++ b/common/src/plugins/Plugins.js @@ -31,7 +31,7 @@ class Plugins { if (cfg.async) { Helper.override(parent, 'run', this._onRunCb); Helper.override(parent, 'stop', this._onStopCb); - this._async = new AsyncParent(parent, {run: cfg.run}); + this._async = new AsyncParent(parent, {run: cfg.run, isBrowser: cfg.isBrowser}); } } @@ -44,21 +44,6 @@ class Plugins { return require(path); } - _onRun(done = () => {}) {this._async.run(done)} - _onStop(done = () => {}) {this._async.stop(done)} - - _createPlugins(parent, cfg) { - const parentPlugins = parent.plugins = []; - - for (let p of cfg.plugins) { - const path = p.path || p; - const name = path.split('/').slice(-1)[0]; - let pluginCls = this.require(path); - - parentPlugins.push(new (pluginCls[name] || pluginCls)(parent, p.cfg || {})); - } - } - /** * Is called if parent instance calls destroy() method. Here we * destroy all created plugins and the reference to this instance @@ -94,6 +79,22 @@ class Plugins { // me._async ? me._async.stop(onAfterDestroy) : onAfterDestroy(); } + + _onRun(done = () => {}) {this._async.run(done)} + _onStop(done = () => {}) {this._async.stop(done)} + + _createPlugins(parent, cfg) { + const parentPlugins = parent.plugins = []; + + for (let p of cfg.plugins) { + const path = p.path || p; + if (!path) {continue} + const name = path.split('/').slice(-1)[0]; + let pluginCls = this.require(path); + + parentPlugins.push(new (pluginCls[name] || pluginCls)(parent, p.cfg || {})); + } + } } module.exports = Plugins; \ No newline at end of file diff --git a/common/src/plugins/Request.js b/common/src/plugins/Request.js index 6edbf28..949aabb 100644 --- a/common/src/plugins/Request.js +++ b/common/src/plugins/Request.js @@ -5,10 +5,11 @@ * * @author flatline */ -const Helper = require('./../Helper'); -const Config = require('./../../../client/src/share/Config').Config; -const MASKS = require('./../net/Requests').MASKS; -const Console = require(`./../../../${Config.modeNodeJs ? 'server' : 'client'}/src/share/Console`); +const WebSocket = require('ws'); +const Helper = require('./../Helper'); +const Config = require('./../../../client/src/share/Config').Config; +const MASKS = require('./../net/Requests').MASKS; +const Console = require(`./../../../${Config.MODE_NODE_JS ? 'server' : 'client'}/src/share/Console`); class Request { /** @@ -73,9 +74,16 @@ class Request { const cb = Helper.isFunc(params[params.length - 1]) ? params.pop() : null; const reqId = Helper.getId(); + if (sock.readyState > WebSocket.OPEN) { + this._onSendErr('Request is interrupted, because connection has closed'); + return reqId; + } cb && (this._requests[reqId] = cb); - sock.send(JSON.stringify([type, (reqId | MASKS.REQ_MASK) >>> 0].concat(params)), this._onSendErrCb); - + try { + sock.send(JSON.stringify([type, (reqId | MASKS.REQ_MASK) >>> 0].concat(params)), this._onSendErrCb); + } catch (e) { + this._onSendErr(e); + } return reqId; } @@ -90,7 +98,11 @@ class Request { * @override */ _onResponse(sock, type, reqId, ...params) { - sock.send(JSON.stringify([type, (reqId & MASKS.RES_MASK) >>> 0].concat(params)), this._onSendErrCb); + try { + sock.send(JSON.stringify([type, (reqId & MASKS.RES_MASK) >>> 0].concat(params)), this._onSendErrCb); + } catch (e) { + this._onSendErr(e); + } } /** @@ -112,7 +124,6 @@ class Request { * [type, reqId|null, ...params]. * @param {WebSocket} sock Owner socket * @param {Event} event Event object with received data - * @private */ _onMessage(sock, event) { const data = JSON.parse(event.data || event); diff --git a/common/tests/Config.js b/common/tests/Config.js new file mode 100644 index 0000000..6ca1f91 --- /dev/null +++ b/common/tests/Config.js @@ -0,0 +1,37 @@ +/** + * Helper class for managing different configurations. It's useful + * for setting temporary config parameters to config object and resetting + * them later back. Is used in tests. + * + * Usage: + * CConfig.prop = 1; + * const cfg = new Config(CConfig); // CConfig.prop = 1 + * cfg.set('prop', 2); + * + * @author flatline + */ +const _set = require('lodash/set'); +const _get = require('lodash/get'); +const _merge = require('lodash/merge'); + +class Config { + constructor(cfgObj) { + this._tmpCfg = {}; + this._cfg = cfgObj; + } + + set(prop, val) { + const tmpCfg = this._tmpCfg; + const cfg = this._cfg; + + _get(tmpCfg, prop) === undefined && _set(tmpCfg, prop, _get(cfg, prop)); + _set(cfg, prop, val); + } + + reset() { + _merge(this._cfg, this._tmpCfg); + this._tmpCfg = {}; + } +} + +module.exports = Config; \ No newline at end of file diff --git a/common/tests/Helper.js b/common/tests/Helper.js index aad1cd2..209396a 100644 --- a/common/tests/Helper.js +++ b/common/tests/Helper.js @@ -4,17 +4,6 @@ * @author flatline */ class Helper { - /** - * Compares two arrays only on first level - * @param {Array} arr1 - * @param {Array} arr2 - * @returns {Boolean} - */ - static compare(arr1, arr2) { - if (arr1.length !== arr2.length) {return false} - return !arr1.some((a) => arr2.indexOf(a) === -1) - } - /** * Waiting for obj.done property to be true and calls cb() callback * after that. If property is not changed, then error will be thrown @@ -53,12 +42,59 @@ class Helper { const eventCb = () => waitObj.done = true; const waitCb = () => {obj.off(event, eventCb); cb(); return true}; - !cb && (cb = preCb) && (preCb = ()=>{}); + !cb && (cb = preCb) && (preCb = () => {}); obj.on(event, eventCb); preCb(); waitObj.done && waitCb() || Helper.wait(waitObj, waitCb); } + + /** + * Runs all testing tasks of a queue one by one in a synchronous manner. + * Testing task is another array of four or two elements - event parameters with + * a function at the end. Four items array is used with waitEvent() and two + * items array is used with wait() + * @param {Function} done Done callback + * @param {Array} queue Testing tasks + */ + static testQ(done, ...queue) { + const len = queue.length; + let i = 0; + let iterate = (q) => { + if (q.length === 4) { + Helper.waitEvent(q[0], q[1], q[2], () => { + q[3](); + i < len && iterate(queue[i++]) || done(); + }); + } else if (q.length === 2) { + Helper.wait(q[0], () => { + q[1](); + i < len && iterate(queue[i++]) || done(); + }); + } else if (q.length === 1) { + Helper.wait(q[0], () => { + i < len && iterate(queue[i++]) || done(); + }); + } + + return true; + }; + + len && iterate(queue[i++]) || done(); + } + + /** + * Inserts script into specified organism. Code may be presented + * as an array of numbers (native format) or as a binary string + * of bits, e.g.: '10 00 01 10 00000010 1111111111'. Spaces will + * be cut from this string. + * @param {VM} vm Destination virtual machine + * @param {Array} code Code we are inserting in + */ + static script(vm, code) { + for (let i = 0; i < code.length; i++) {vm.insertLine()} + for (let i = 0; i < code.length; i++) {vm.updateLine(i, typeof code[i] === 'string' ? parseInt(code[i].split(' ').join(''), 2) : code[i])} + } } module.exports = Helper; \ No newline at end of file diff --git a/package.json b/package.json index 6e553fe..5c98a7e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "construct", "version": "0.2.0", - "description": "JavaScript digital organisms simulator", + "description": "Construct - JavaScript digital organisms simulator", "main": "index.js", "directories": { "doc": "doc" @@ -11,6 +11,7 @@ "babel-preset-env": "^1.6.0", "jasmine": "^2.6.0", "js-beautify": "^1.6.14", + "panzoom": "^4.1.0", "webpack": "^3.9.1", "ws": "^3.3.2" }, diff --git a/server/src/App.js b/server/src/App.js index 6bc0c2d..fb6ebe9 100644 --- a/server/src/App.js +++ b/server/src/App.js @@ -6,6 +6,5 @@ */ const Server = require('./server/Server').Server; const Config = require('./share/Config').Config; - const server = new Server(Config.port); server.run(); \ No newline at end of file diff --git a/server/src/server/AroundServers.js b/server/src/server/AroundServers.js index 66783d7..6c2e095 100644 --- a/server/src/server/AroundServers.js +++ b/server/src/server/AroundServers.js @@ -1,55 +1,158 @@ /** * This class stores logic of communication with nearest servers (up, right, * down and left), which are connected to current one. It keeps connections - * to them and updates active status. + * to them and updates active status. There are two types of connection with + * servers and saving their sockets: + * 1. create local clients and connect them to remote servers + * 2. catch input connection from remote client + * + * The type of connection depends on who was created first and last. First + * created server A, which is above second created server B should do nothing. + * Server B should create four clients and connect them to A and other nearest + * servers, if they exist. For server A, input client connection from bottom + * will be stored in this._socks[DIR.DOWN]. The same scenario for right, down + * and left servers as well. * * @author flatline */ -const DIR = require('./../../../common/src/Directions').DIR; -const TYPES = require('./../../../common/src/net/Requests').TYPES; +const DIR = require('./../../../common/src/Directions').DIR; +const FLIP_DIR = require('./../../../common/src/Directions').FLIP_DIR; +const NAMES = require('./../../../common/src/Directions').NAMES; +const TYPES = require('./../../../common/src/net/Requests').TYPES; +const AsyncParent = require('./../../../common/src/plugins/AsyncParent'); +const Observer = require('./../../../common/src/Observer'); +const Console = require('./../share/Console'); +const Client = require('./Client'); +/** + * Events of this class + */ +const OPEN = 0; +const CLOSE = 1; +const EVENTS = { + OPEN, + CLOSE +}; -class AroundServers { +// TODO: later here should be auto connect mechanism for this._clients +class AroundServers extends Observer { constructor(parent) { + super(Object.keys(EVENTS).length); + + this.EVENTS = EVENTS; /** * {Connection} Connection instance of current Client or Server */ - this._parent = parent; + this._parent = parent; /** - * {Object} All nearest servers by direction + * {Object} Four sockets for sending messages to nearest servers. + * They may be: clients created within current class or sockets + * obtained after input client connection (created by remote server). */ - // TODO: rename to _clients. It should be a map of Client instances - this._socks = {}; - this._socks[DIR.UP] = null; - this._socks[DIR.RIGHT] = null; - this._socks[DIR.DOWN] = null; - this._socks[DIR.LEFT] = null; + this._socks = new Array(4); + /** + * {Object} Optional clients for connection with nearest servers. + * This map may be empty ar partly empty if servers around make + * connection first. Keys - directions, values - client instances. + */ + this._clients = new Array(4); + /** + * {AsyncParent} Keep reference to AsyncParent class, which tracks + * async running of classes through AsyncChild interface. + */ + this._async = null; + // + // Try to create clients map for connection with nearest servers + // + this._createClients(); + } + + run(done = () => {}) { + this._clients.forEach(c => c.run()); + this._async.run(done); + return true; + } + + stop(done = () => {}) { + this._clients.forEach(c => c.stop()); + this._async.stop(done); + return true; + } + + destroy(done = () => {}) { + this.stop(() => { + this._parent = null; + this._socks = null; + this._clients.forEach(c => c.destroy()); + this._clients = null; + this._async.destroy(done); + super.destroy(); + }); + } + + /** + * Returns direction by socket + * @param {WebSocket} sock + * @return {Number} Direction DIR.NO is also available + */ + getDirection(sock) { + const index = this._socks.indexOf(sock); + return index < 0 && DIR.NO || index; } - destroy() { - this._parent = null; - this._socks = null; + hasSocket(dir) { + return !!this._socks[dir]; } - // TODO: rename to 'clients' - get socks() {return this._socks} + getSocket(dir) { + return this._socks[dir]; + } setSocket(sock, dir) { - this._socks[dir] = sock; - } - - activate(activate = true) { - const socks = this._socks; - const parent = this._parent; - const UP = DIR.UP; - const RIGHT = DIR.RIGHT; - const DOWN = DIR.DOWN; - const LEFT = DIR.LEFT; - const ACTIVE = TYPES.REQ_SET_NEAR_ACTIVE; - - socks[UP] && parent.request(socks[UP], ACTIVE, DOWN, activate); - socks[RIGHT] && parent.request(socks[RIGHT], ACTIVE, LEFT, activate); - socks[DOWN] && parent.request(socks[DOWN], ACTIVE, UP, activate); - socks[LEFT] && parent.request(socks[LEFT], ACTIVE, RIGHT, activate); + dir !== DIR.NO && (this._socks[dir] = sock); + } + + _createClients() { + const parent = this._parent; + const cfg = parent.cfg; + const clients = this._clients; + + clients[DIR.UP] = new Client(parent, DIR.UP, cfg.upHost, cfg.upPort, true); + clients[DIR.RIGHT] = new Client(parent, DIR.RIGHT, cfg.rightHost, cfg.rightPort, true); + clients[DIR.DOWN] = new Client(parent, DIR.DOWN, cfg.downHost, cfg.downPort, true); + clients[DIR.LEFT] = new Client(parent, DIR.LEFT, cfg.leftHost, cfg.leftPort, true); + + this._addHandlers(clients); + this._async = new AsyncParent(this, {classes: clients, isBrowser: false}); + } + + _addHandlers(clients) { + clients.forEach((c, i) => { + c.on(c.EVENTS.OPEN, this._onOpen.bind(this, c, i)); + c.on(c.EVENTS.CLOSE, this._onClose.bind(this, i)); + }); + } + /** + * Sends a request to tell remote server, that this client is + * for inter server communication only. dir parameter sets nearest + * server location (up...left) + * @param {WebSocket} client WebSocket client instance for connection + * with nearest server + * @param {Number} dir Direction of nearest server (up...left) + */ + _onOpen(client, dir) { + client.request(client.socket, TYPES.REQ_SET_NEAR_ACTIVE, FLIP_DIR[dir], (type) => { + if (type !== TYPES.RES_SET_NEAR_ACTIVE_OK) { + Console.error(`Unable update active status on [${NAMES[dir]}] server. Response type: ${type}`); + return; + } + this._socks[dir] = client.socket; + this.fire(OPEN, dir); + }); + } + + _onClose(dir) { + this._socks[dir] = null; + this.fire(CLOSE, dir); } } diff --git a/server/src/server/Client.js b/server/src/server/Client.js new file mode 100644 index 0000000..c4dff28 --- /dev/null +++ b/server/src/server/Client.js @@ -0,0 +1,87 @@ +/** + * Client, which created by server for connecting with nearest servers. + * + * @author flatline + */ +const NAMES = require('./../../../common/src/Directions').NAMES; +const Console = require('./../share/Console'); +const BaseClient = require('./../../../common/src/net/Client').Client; +const EVENTS = require('./../../../common/src/net/Client').EVENTS; +const Plugins = require('./Plugins'); + +const PLUGINS = [ + 'src/plugins/Request', + {path: 'src/plugins/AsyncClient', cfg: {openEvent: EVENTS.OPEN}} +]; + +class Client extends BaseClient { + constructor(server, dir, host, port) { + super(host, port, true); + /** + * {Server} Parent server created current client for communication + * with near servers. + */ + this._server = server; + this._dir = dir; + // + // Plugins should be created at the end of constructor to + // have an ability to access this class public fields + // + this._plugins = new Plugins(this, {plugins: PLUGINS}); + } + + destroy() { + super.destroy(); + this._dir = null; + this._plugins = null; + } + + /** + * Is called if error occurred + * @param {Object} event Error event object + * @override + */ + onError(event) { + super.onError(event); + Console.warn(`[${NAMES[this._dir]}] server error: ${event.message} on ${this.host}:${this.port}`); + } + + /** + * Is called every time if near server sends us a request. This is + * a bridge between near server and current server + * @param {WebSocket} sock Socket, received the message + * @param {Event} event Message event. Data is in 'data' property + * @override + */ + onMessage(sock, event) { + super.onMessage(sock, event); + this._server.onMessage(sock, event); + } + + /** + * Is called on connection close with server. Close reason will be in + * this.closeReason field after calling super.onClose() method + * @param {Event} event + * @override + */ + onClose(event) { + const active = this.active; + super.onClose(event); + // + // We have to show message only if we had been active for some time + // + active && Console.warn(`[${NAMES[this._dir]}] server has disconnected by reason: '${this.closeReason}' on ${this.host}:${this.port}`); + } + + /** + * Is called after client has connected to the server + * @param {Event} event + * @override + */ + onOpen(event) { + super.onOpen(event); + Console.info(`[${NAMES[this._dir]}] server has connected on ${this.host}:${this.port}`); + } +} + +module.exports = Client; \ No newline at end of file diff --git a/server/src/server/Connections.js b/server/src/server/Connections.js index 26e7f25..715e0ef 100644 --- a/server/src/server/Connections.js +++ b/server/src/server/Connections.js @@ -8,6 +8,7 @@ * * @author flatline */ +const DIR = require('./../../../common/src/Directions').DIR; /** * {String} Separator string, which separates id parts. Like * 'X' + ID_SEPARATOR + 'X' @@ -31,17 +32,13 @@ class Connections { * @returns {Array} Array of numbers (region) */ static toRegion(id) { - return id.split(ID_SEPARATOR).map(Number); + return id && id.split(ID_SEPARATOR).map(Number); } constructor(amount) { - /** - * {Number} amount Maximum amount of connections for current server. Should - * be quadratic (x^2) e.g.: 4, 9, 16,... This value will be extended - * with additional "around" rows and columns for connecting with sibling - * servers. So, result amount will be e.g.: 100 + 2 rows + 2 columns. - */ - this._amount = amount; + const side = +Math.sqrt(amount).toFixed(); + if (amount < 1) {throw `Incorrect amount of connections in class Connections - ${amount}`} + if (side * side !== amount) {throw `Incorrect amount of connections in class Connections - ${amount}. Should be a pow of two`} /** * {Number} Size of one side of MAX_CONNECTIONS qub. Contains additional * "around" rows and columns. For qub == 16, it's 4. @@ -50,16 +47,21 @@ class Connections { /** * {Number} Size of one full side of the connections squire */ - this._side = +Math.sqrt(amount).toFixed() + 2; + this._side = side; - for (let col = 0, conns = this.conns; col < this._side; col++) { - conns[col] = (new Array(this._side)).fill(null); - conns[col].forEach((v, i, a) => a[i] = {sock: null}); + for (let row = 0, conns = this.conns; row < this._side; row++) { + conns[row] = (new Array(this._side)).fill(null); + conns[row].forEach((v, i, a) => a[i] = {sock: null}); } } + get side() { + return this._side; + } + destroy() { this.conns = null; + this._side = null; } /** @@ -68,8 +70,8 @@ class Connections { * @returns {Array|null} */ upRegion(region) { - region = region.slice(); - return --region[1] < 0 ? null : region; + (region = region.slice())[1]--; + return this._validRegion(region) && region || null; } /** @@ -78,8 +80,8 @@ class Connections { * @returns {Array|null} */ rightRegion(region) { - region = region.slice(); - return ++region[0] > this._amount + 1 ? null : region; + (region = region.slice())[0]++; + return this._validRegion(region) && region || null; } /** @@ -88,8 +90,8 @@ class Connections { * @returns {Array|null} */ downRegion(region) { - region = region.slice(); - return ++region[1] > this._amount + 1 ? null : region; + (region = region.slice())[1]++; + return this._validRegion(region) && region || null; } /** @@ -98,8 +100,26 @@ class Connections { * @returns {Array|null} */ leftRegion(region) { - region = region.slice(); - return --region[0] < 0 ? null : region; + (region = region.slice())[0]--; + return this._validRegion(region) && region || null; + } + + /** + * Returns opposite region. It means the region on the diagonally + * other side related to specified. up -> down, right -> left,... + * @param {Array} region + * @param {Number} dir Direction + * @return {Array} Opposite region + */ + oppositeRegion(region, dir) { + const side = this._side - 1; + + if (dir === DIR.UP) {region[1] = side; return region} + if (dir === DIR.RIGHT) {region[0] = 0; return region} + if (dir === DIR.DOWN) {region[1] = 0; return region} + if (dir === DIR.LEFT) {region[0] = side; return region} + + return region; } /** @@ -108,7 +128,7 @@ class Connections { * @returns {Object|null} */ getConnection(region) { - return region && this.conns[region[0]][region[1]]; + return this._validRegion(region) && this.conns[region[0]][region[1]] || null; } /** @@ -129,18 +149,22 @@ class Connections { getFreeRegion() { const conns = this.conns; - const side = this._side - 1; + const side = this._side; - for (let col = 1; col < side; col++) { - for (let row = 1; row < side; row++) { - if (conns[col][row].sock === null) { - return [col, row]; + for (let y = 0; y < side; y++) { + for (let x = 0; x < side; x++) { + if (conns[x][y].sock === null) { + return [x, y]; } } } return null; } + + _validRegion(region) { + return region && region[0] > -1 && region[0] < this._side && region[1] > -1 && region[1] < this._side; + } } diff --git a/server/src/server/ConnectionsSpec.js b/server/src/server/ConnectionsSpec.js index 895ed68..bf1dc1e 100644 --- a/server/src/server/ConnectionsSpec.js +++ b/server/src/server/ConnectionsSpec.js @@ -1,11 +1,16 @@ describe("server/src/server/Connections", () => { const Connections = require('./Connections'); - let THelper = require('./../../../common/tests/Helper'); + const eq = require('lodash/isEqual'); it("Checking Connections instance creation", () => { let cons = new Connections(1); cons.destroy(); }); + it("Checking Connections instance creation with wrong amount of connections", () => { + expect(() => new Connections(0)).toThrow(); + expect(() => new Connections(-1)).toThrow(); + expect(() => new Connections(-100)).toThrow(); + }); it("Checking toId() method", () => { expect(Connections.toId([1,1])).toEqual('1-1'); expect(Connections.toId([0,0])).toEqual('0-0'); @@ -14,98 +19,206 @@ describe("server/src/server/Connections", () => { let cons = new Connections(1); let region = cons.getFreeRegion(); - expect(THelper.compare(region, [1, 1])).toEqual(true); - expect(Connections.toId(region)).toEqual('1-1'); + expect(eq(region, [0, 0])).toEqual(true); + expect(Connections.toId(region)).toEqual('0-0'); expect(Connections.toId([1,1])).toEqual('1-1'); expect(Connections.toId([0,0])).toEqual('0-0'); cons.destroy(); }); it("Checking toRegion() method", () => { - expect(THelper.compare(Connections.toRegion('1-1'), [1,1])).toEqual(true); - expect(THelper.compare(Connections.toRegion('1-0'), [1,0])).toEqual(true); + expect(eq(Connections.toRegion('1-1'), [1,1])).toEqual(true); + expect(eq(Connections.toRegion('1-0'), [1,0])).toEqual(true); }); it("Checking upRegion() method", () => { let cons = new Connections(1); - expect(THelper.compare(cons.upRegion([1,1]), [1,0])).toEqual(true); - expect(THelper.compare(cons.upRegion([1,2]), [1,1])).toEqual(true); - expect(cons.upRegion([0,0]) === null).toEqual(true); - expect(cons.upRegion([1,0]) === null).toEqual(true); - expect(cons.upRegion([-1,0]) === null).toEqual(true); - expect(THelper.compare(cons.upRegion([-1,1]), [-1,0])).toEqual(true); + expect(cons.upRegion([0,0] )).toEqual(null); + expect(cons.upRegion([1,2] )).toEqual(null); + expect(cons.upRegion([1,0] )).toEqual(null); + expect(cons.upRegion([1,1] )).toEqual(null); + expect(cons.upRegion([-1,0] )).toEqual(null); + expect(cons.upRegion([-1,1] )).toEqual(null); + expect(cons.upRegion([-1,-1])).toEqual(null); + + cons.destroy(); + }); + it("Checking upRegion() method 2", () => { + let cons = new Connections(4); + + expect(cons.upRegion([0,0] )).toEqual(null); + expect(cons.upRegion([1,2] )).toEqual([1,1]); + expect(cons.upRegion([1,1] )).toEqual([1,0]); + expect(cons.upRegion([0,1] )).toEqual([0,0]); + expect(cons.upRegion([2,1] )).toEqual(null); + expect(cons.upRegion([1,0] )).toEqual(null); + expect(cons.upRegion([-1,0] )).toEqual(null); + expect(cons.upRegion([-1,1] )).toEqual(null); + expect(cons.upRegion([-1,-1])).toEqual(null); + expect(cons.upRegion([2,2] )).toEqual(null); cons.destroy(); }); it("Checking rightRegion() method", () => { let cons = new Connections(1); - expect(THelper.compare(cons.rightRegion([1,1]), [2,1])).toEqual(true); - expect(THelper.compare(cons.rightRegion([1,2]), [2,2])).toEqual(true); - expect(cons.rightRegion([2,0]) === null).toEqual(true); - expect(cons.rightRegion([2,1]) === null).toEqual(true); - expect(cons.rightRegion([2,-1]) === null).toEqual(true); - expect(THelper.compare(cons.rightRegion([1,-1]), [2,-1])).toEqual(true); + expect(cons.rightRegion([0,0] )).toEqual(null); + expect(cons.rightRegion([1,2] )).toEqual(null); + expect(cons.rightRegion([1,0] )).toEqual(null); + expect(cons.rightRegion([1,1] )).toEqual(null); + expect(cons.rightRegion([2,0] )).toEqual(null); + expect(cons.rightRegion([2,1] )).toEqual(null); + expect(cons.rightRegion([2,-1] )).toEqual(null); + expect(cons.rightRegion([1,-1] )).toEqual(null); + expect(cons.rightRegion([-1,-1])).toEqual(null); + + cons.destroy(); + }); + it("Checking rightRegion() method 2", () => { + let cons = new Connections(4); + + expect(cons.rightRegion([0,0] )).toEqual([1,0]); + expect(cons.rightRegion([1,2] )).toEqual(null); + expect(cons.rightRegion([2,0] )).toEqual(null); + expect(cons.rightRegion([2,1] )).toEqual(null); + expect(cons.rightRegion([1,1] )).toEqual(null); + expect(cons.rightRegion([0,1] )).toEqual([1,1]); + expect(cons.rightRegion([-1,1] )).toEqual([0,1]); + expect(cons.rightRegion([2,-1] )).toEqual(null); + expect(cons.rightRegion([1,-1] )).toEqual(null); + expect(cons.rightRegion([-1,-1])).toEqual(null); cons.destroy(); }); it("Checking downRegion() method", () => { let cons = new Connections(1); - expect(THelper.compare(cons.downRegion([1,1]), [1,2])).toEqual(true); - expect(THelper.compare(cons.downRegion([1,0]), [1,1])).toEqual(true); - expect(cons.downRegion([0,2]) === null).toEqual(true); - expect(cons.downRegion([1,2]) === null).toEqual(true); - expect(cons.downRegion([-1,2]) === null).toEqual(true); - expect(THelper.compare(cons.downRegion([-1,1]), [-1,2])).toEqual(true); + expect(cons.downRegion([0,0] )).toEqual(null); + expect(cons.downRegion([1,2] )).toEqual(null); + expect(cons.downRegion([1,0] )).toEqual(null); + expect(cons.downRegion([1,1] )).toEqual(null); + expect(cons.downRegion([2,0] )).toEqual(null); + expect(cons.downRegion([2,1] )).toEqual(null); + expect(cons.downRegion([2,-1] )).toEqual(null); + expect(cons.downRegion([1,-1] )).toEqual(null); + expect(cons.downRegion([-1,-1])).toEqual(null); + + cons.destroy(); + }); + it("Checking downRegion() method 2", () => { + let cons = new Connections(4); + + expect(cons.downRegion([0,0] )).toEqual([0,1]); + expect(cons.downRegion([1,2] )).toEqual(null); + expect(cons.downRegion([1,0] )).toEqual([1,1]); + expect(cons.downRegion([1,1] )).toEqual(null); + expect(cons.downRegion([2,0] )).toEqual(null); + expect(cons.downRegion([2,1] )).toEqual(null); + expect(cons.downRegion([2,-1] )).toEqual(null); + expect(cons.downRegion([1,-1] )).toEqual([1,0]); + expect(cons.downRegion([0,-1] )).toEqual([0,0]); + expect(cons.downRegion([-1,-1])).toEqual(null); cons.destroy(); }); it("Checking leftRegion() method", () => { let cons = new Connections(1); - expect(THelper.compare(cons.leftRegion([1,1]), [0,1])).toEqual(true); - expect(THelper.compare(cons.leftRegion([2,1]), [1,1])).toEqual(true); - expect(cons.leftRegion([0,0]) === null).toEqual(true); - expect(cons.leftRegion([0,1]) === null).toEqual(true); - expect(cons.leftRegion([0,-1]) === null).toEqual(true); - expect(THelper.compare(cons.leftRegion([1,-1]), [0,-1])).toEqual(true); + expect(cons.downRegion([0,0] )).toEqual(null); + expect(cons.downRegion([1,2] )).toEqual(null); + expect(cons.downRegion([1,0] )).toEqual(null); + expect(cons.downRegion([1,1] )).toEqual(null); + expect(cons.downRegion([2,0] )).toEqual(null); + expect(cons.downRegion([2,1] )).toEqual(null); + expect(cons.downRegion([2,-1] )).toEqual(null); + expect(cons.downRegion([1,-1] )).toEqual(null); + expect(cons.downRegion([-1,-1])).toEqual(null); + + cons.destroy(); + }); + it("Checking leftRegion() method 2", () => { + let cons = new Connections(4); + + expect(cons.downRegion([0,0] )).toEqual([0,1]); + expect(cons.downRegion([1,2] )).toEqual(null); + expect(cons.downRegion([1,0] )).toEqual([1,1]); + expect(cons.downRegion([1,1] )).toEqual(null); + expect(cons.downRegion([2,0] )).toEqual(null); + expect(cons.downRegion([2,1] )).toEqual(null); + expect(cons.downRegion([2,-1] )).toEqual(null); + expect(cons.downRegion([1,-1] )).toEqual([1,0]); + expect(cons.downRegion([-1,-1])).toEqual(null); cons.destroy(); }); it('Checking getConnection() method', () => { const cons = new Connections(1); - expect(cons.getConnection([1,1]).sock).toEqual(null); + expect(cons.getConnection([1,1])).toEqual(null); expect(cons.getConnection([0,0]).sock).toEqual(null); - expect(cons.getConnection([2,2]).sock).toEqual(null); + expect(cons.getConnection([2,2])).toEqual(null); cons.destroy(); }); it('Checking setData() method', () => { const cons = new Connections(1); + cons.setData([0,0], 'active', true); + expect(cons.getConnection([0,0]).active).toEqual(true); + expect(cons.getConnection([0,0]).sock).toEqual(null); + expect(cons.getConnection([1,1])).toEqual(null); + expect(cons.getConnection([0,1])).toEqual(null); + expect(cons.getConnection([1,0])).toEqual(null); + expect(cons.getConnection([2,1])).toEqual(null); + expect(cons.getConnection([2,2])).toEqual(null); + expect(cons.getConnection([1,2])).toEqual(null); + + cons.setData([0,0], 'active', false); + expect(cons.getConnection([1,1])).toEqual(null); + expect(cons.getConnection([0,1])).toEqual(null); + expect(cons.getConnection([0,0]).active).toEqual(false); + expect(cons.getConnection([0,0]).sock).toEqual(null); + expect(cons.getConnection([1,0])).toEqual(null); + expect(cons.getConnection([2,1])).toEqual(null); + expect(cons.getConnection([2,2])).toEqual(null); + expect(cons.getConnection([1,2])).toEqual(null); + + cons.clearData([0,0]); + expect(cons.getConnection([1,1])).toEqual(null); + expect(cons.getConnection([0,1])).toEqual(null); + expect(cons.getConnection([0,0]).active).toEqual(undefined); + expect(cons.getConnection([0,0]).sock).toEqual(null); + expect(cons.getConnection([1,0])).toEqual(null); + expect(cons.getConnection([2,1])).toEqual(null); + expect(cons.getConnection([2,2])).toEqual(null); + expect(cons.getConnection([1,2])).toEqual(null); + + cons.destroy(); + }); + it('Checking setData() method 2', () => { + const cons = new Connections(9); + cons.setData([1,1], 'active', true); + expect(cons.getConnection([0,0]).active).toEqual(undefined); expect(cons.getConnection([1,1]).active).toEqual(true); expect(cons.getConnection([0,1]).active).toEqual(undefined); - expect(cons.getConnection([0,0]).active).toEqual(undefined); expect(cons.getConnection([1,0]).active).toEqual(undefined); expect(cons.getConnection([2,1]).active).toEqual(undefined); expect(cons.getConnection([2,2]).active).toEqual(undefined); expect(cons.getConnection([1,2]).active).toEqual(undefined); cons.setData([1,1], 'active', false); + expect(cons.getConnection([0,0]).active).toEqual(undefined); expect(cons.getConnection([1,1]).active).toEqual(false); expect(cons.getConnection([0,1]).active).toEqual(undefined); - expect(cons.getConnection([0,0]).active).toEqual(undefined); expect(cons.getConnection([1,0]).active).toEqual(undefined); expect(cons.getConnection([2,1]).active).toEqual(undefined); expect(cons.getConnection([2,2]).active).toEqual(undefined); expect(cons.getConnection([1,2]).active).toEqual(undefined); cons.clearData([1,1]); + expect(cons.getConnection([0,0]).active).toEqual(undefined); expect(cons.getConnection([1,1]).active).toEqual(undefined); expect(cons.getConnection([0,1]).active).toEqual(undefined); - expect(cons.getConnection([0,0]).active).toEqual(undefined); expect(cons.getConnection([1,0]).active).toEqual(undefined); expect(cons.getConnection([2,1]).active).toEqual(undefined); expect(cons.getConnection([2,2]).active).toEqual(undefined); @@ -116,25 +229,25 @@ describe("server/src/server/Connections", () => { it('Checking clearData() method', () => { const cons = new Connections(1); - cons.setData([1,1], 'prop', true); - expect(cons.getConnection([1,1]).prop).toEqual(true); - cons.clearData([1,1]); - expect(cons.getConnection([1,1]).prop).toEqual(undefined); - cons.setData([1,1], 'prop', true); - cons.setData([1,1], 'prop', 1); - expect(cons.getConnection([1,1]).prop).toEqual(1); + cons.setData([0,0], 'prop', true); + expect(cons.getConnection([0,0]).prop).toEqual(true); + cons.clearData([0,0]); + expect(cons.getConnection([0,0]).prop).toEqual(undefined); + cons.setData([0,0], 'prop', true); + cons.setData([0,0], 'prop', 1); + expect(cons.getConnection([0,0]).prop).toEqual(1); cons.destroy(); }); it('Checking getFreeRegion() method', () => { const cons = new Connections(1); - expect(THelper.compare(cons.getFreeRegion(), [1,1])).toEqual(true); - expect(THelper.compare(cons.getFreeRegion(), [1,1])).toEqual(true); - cons.setData([1,1], 'sock', true); + expect(cons.getFreeRegion()).toEqual([0,0]); + expect(cons.getFreeRegion()).toEqual([0,0]); + cons.setData([0,0], 'sock', true); expect(cons.getFreeRegion()).toEqual(null); - cons.clearData([1,1]); - expect(THelper.compare(cons.getFreeRegion(), [1,1])).toEqual(true); + cons.clearData([0,0]); + expect(cons.getFreeRegion()).toEqual([0,0]); cons.destroy(); }); diff --git a/server/src/server/Server.js b/server/src/server/Server.js index 8a4e162..78c318a 100644 --- a/server/src/server/Server.js +++ b/server/src/server/Server.js @@ -29,6 +29,7 @@ * * @author flatline */ +const OS = require('os'); const WebSocket = require('./../../../node_modules/ws/index'); const Connection = require('./../../../common/src/net/Connection').Connection; const EVENTS = require('./../../../common/src/net/Connection').EVENTS; @@ -37,6 +38,8 @@ const Config = require('./../share/Config').Config; const Plugins = require('./Plugins'); const Console = require('./../share/Console'); const Connections = require('./Connections'); +const DIR = require('./../../../common/src/Directions').DIR; +const NAMES = require('./../../../common/src/Directions').NAMES; /** * {Number} Amount of base events. Is used to extend them by server related */ @@ -68,11 +71,9 @@ class Server extends Connection { constructor(port = Config.port) { super(SERVER_EVENTS_LEN); this.EVENTS = SERVER_EVENTS; + this.cfg = Config; this.conns = new Connections(Config.maxConnections); - // TODO: This field should be used for connections with around servers. - // TODO: We have to connect with all available around servers on start - // TODO: and set them into aroundServers.setSocket() - this.aroundServers = new AroundServers(this); + this.aroundServers = Config.modeDistributed ? new AroundServers(this) : null; this._server = null; this._port = port; @@ -107,14 +108,18 @@ class Server extends Connection { Server.ports[this._port] = true; try { this._server = new WebSocket.Server({port: this._port}, () => { - this._server.on('connection', this.onConnect.bind(this)); - this.active = true; - this._running = false; - this.fire(RUN); - Console.info('Server is ready'); + const onDone = () => { + this._server.on('connection', this.onConnect.bind(this)); + this.active = true; + this._running = false; + this.fire(RUN); + Console.info(`Server is ready on ${this._getIp()}:${this._port}`); + }; + this.aroundServers && this.aroundServers.run(onDone) || onDone(); }); + this._server.on('error', (e) => Console.error(`Can\'t run server on port ${this._port}. Error: '${e.message}'`)); } catch (e) { - Console.warn(`Can\'t run server on port ${this._port}. Error: ${e.message}`); + Console.error(`Can\'t run server on port ${this._port}. Error: '${e.message}'`); return false; } @@ -148,14 +153,17 @@ class Server extends Connection { try { me._stopping = true; me._server.close(() => { - delete Server.ports[me._port]; - me._server.removeAllListeners('connection'); - me.active = false; - me._stopping = false; - this._server = null; - me.fire(STOP); - Console.info('Server has stopped. All clients have disconnected'); - if (me._destroying) {me.destroy()} + const onDone = () => { + delete Server.ports[me._port]; + me._server.removeAllListeners('connection'); + me.active = false; + me._stopping = false; + this._server = null; + me.fire(STOP); + Console.info('Server has stopped. All clients have disconnected'); + if (me._destroying) {me.destroy()} + }; + this.aroundServers && this.aroundServers.stop(onDone) || onDone(); }); } catch(e) { Console.error('Server.stop() failed: ', e); @@ -190,16 +198,23 @@ class Server extends Connection { * regions for new connections, then places current one in a connection * cub and sends unique id to the client. * @param {WebSocket} sock Client's socket + * @param {Object} req Request info object */ - onConnect(sock) { + onConnect(sock, req) { const region = this.conns.getFreeRegion(); const clientId = Connections.toId(region); - if (region === null) { + if (region === null && this._server.clients.length > Config.maxConnections + 4) { // 4 extra connections for near servers sock.terminate(); this.fire(OVERFLOW, sock); Console.warn('This server is overloaded by clients. Try another server to connect.'); return; } + // + // This is small hack. We have to bind remote client address and + // it's socket. We use this info in a terminal to show remote + // connected client address + // + sock.remoteAddr = req.connection.remoteAddress; sock.on('message', this.onMessage.bind(this, sock)); sock.on('error', this.onError.bind(this, clientId, sock)); @@ -217,12 +232,36 @@ class Server extends Connection { */ onClose(clientId, sock, event) { super.onClose(event); - const region = Connections.toRegion(clientId); - this.conns.clearData(region); + const servers = this.aroundServers; + const region = Connections.toRegion(clientId); + const dir = servers ? servers.getDirection(sock) : clientId; + const isServer = dir < DIR.NO; + + !isServer && region && this.conns.clearData(region); sock.removeAllListeners('message'); sock.removeAllListeners('error'); sock.removeAllListeners('close'); - Console.warn(`Server: client ${clientId} has disconnected by reason: ${this.closeReason}`); + servers && servers.setSocket(sock, dir); + Console.warn(`Client [${isServer ? NAMES[dir] : (clientId || sock.remoteAddr)}] has disconnected by reason: ${this.closeReason}`); + } + + /** + * Returns local IP, which is used by clients to connect + * @return {String} + */ + _getIp() { + const iFaces = OS.networkInterfaces(); + let ip = '127.0.0.1'; + + Object.keys(iFaces).forEach(dev => { + iFaces[dev].filter(details => { + if (details.family === 'IPv4' && details.internal === false) { + ip = details.address; + } + }); + }); + + return ip; } } diff --git a/server/src/server/ServerSpec.js b/server/src/server/ServerSpec.js index fcdc420..d4f7b1f 100644 --- a/server/src/server/ServerSpec.js +++ b/server/src/server/ServerSpec.js @@ -4,8 +4,6 @@ describe("server/src/server/Server", () => { const SConfig = require('./../../../server/src/share/Config').Config; const Observer = require('./../../../common/src/Observer'); const Server = require('./Server').Server; - const OLD_MODE = Config.modeNodeJs; - Config.modeNodeJs = true; const Client = require('./../../../client/src/manager/plugins/client/Client').Client; const SEVENTS = require('./Server').EVENTS; const CEVENTS = require('./../../../client/src/manager/plugins/client/Client').EVENTS; @@ -13,11 +11,9 @@ describe("server/src/server/Server", () => { const SConsole = require('./../share/Console'); const Console = require('./../../../client/src/share/Console'); const Helper = require('./../../../common/tests/Helper'); - const TYPES = require('./../../../common/src/net/Requests').TYPES; - const Api = require('./plugins/Api'); - const Request = require('./../../../common/src/plugins/Request'); const waitEvent = Helper.waitEvent; const host = Config.serverHost; + const port = SConfig.port; const CLIENT_URL = `ws://127.0.0.1:${Config.serverPort}`; @@ -27,9 +23,13 @@ describe("server/src/server/Server", () => { let serror; let swarn; let sinfo; + let dist; beforeAll(() => { Config.serverHost = 'ws://127.0.0.1'; + dist = SConfig.modeDistributed; + SConfig.modeDistributed = false; + SConfig.port = Config.serverPort; error = Console.error; warn = Console.warn; info = Console.info; @@ -52,8 +52,9 @@ describe("server/src/server/Server", () => { Console.error = error; Console.warn = warn; Console.info = info; - Config.modeNodeJs = OLD_MODE; Config.serverHost = host; + SConfig.modeDistributed = dist; + SConfig.port = port; }); it("Checking server creation", () => { @@ -193,6 +194,7 @@ describe("server/src/server/Server", () => { server.stop(); Helper.wait(waitObj, () => { server.destroy(); + ws.close(); done(); }); }); @@ -207,11 +209,17 @@ describe("server/src/server/Server", () => { const ws1 = new WebSocket(CLIENT_URL); const ws2 = new WebSocket(CLIENT_URL); Helper.wait(waitObj, () => { - server.on(SEVENTS.STOP, () => waitObj.done = true); - server.stop(); + cons = 0; + server.on(SEVENTS.CLOSE, () => ++cons === 2 && (waitObj.done = true)); + ws1.close(); + ws2.close(); Helper.wait(waitObj, () => { - server.destroy(); - done(); + server.on(SEVENTS.STOP, () => waitObj.done = true); + server.stop(); + Helper.wait(waitObj, () => { + server.destroy(); + done(); + }); }); }); }); @@ -228,11 +236,12 @@ describe("server/src/server/Server", () => { server.on(SEVENTS.CLOSE, () => waitObj.done = true); ws2.close(); Helper.wait(waitObj, () => { - server.on(SEVENTS.STOP, () => waitObj.done = true); - server.stop(); + server.on(SEVENTS.CLOSE, () => waitObj.done = true); + ws1.close(); Helper.wait(waitObj, () => { + server.on(SEVENTS.DESTROY, () => waitObj.done = true); server.destroy(); - done(); + Helper.wait(waitObj, done); }); }) }); @@ -260,11 +269,15 @@ describe("server/src/server/Server", () => { const ws = new WebSocket(CLIENT_URL); Helper.wait(waitObj, () => { expect(server.active).toEqual(true); - server.on(SEVENTS.STOP, () => waitObj.done = true); - server.destroy(); + server.on(SEVENTS.CLOSE, () => waitObj.done = true); + ws.close(); Helper.wait(waitObj, () => { - expect(server.active).toEqual(false); - done(); + server.on(SEVENTS.DESTROY, () => waitObj.done = true); + server.destroy(); + Helper.wait(waitObj, () => { + expect(server.active).toEqual(false); + done(); + }); }); }); }); @@ -278,6 +291,7 @@ describe("server/src/server/Server", () => { } run() {} stop() {} + resetActive() {} get clientId() {return this._clientId} set clientId(id) {this._clientId = id} } diff --git a/server/src/server/plugins/Api.js b/server/src/server/plugins/Api.js index b987c91..abba91e 100644 --- a/server/src/server/plugins/Api.js +++ b/server/src/server/plugins/Api.js @@ -6,77 +6,104 @@ * * @author flatline */ +const _get = require('lodash/get'); const Helper = require('./../../../../common/src/Helper'); const TYPES = require('./../../../../common/src/net/Requests').TYPES; const Console = require('./../../share/Console'); const Connections = require('./../Connections'); const DIR = require('./../../../../common/src/Directions').DIR; -const DIR_NAMES = require('./../../../../common/src/Directions').NAMES; +const FLIP_DIR = require('./../../../../common/src/Directions').FLIP_DIR; +const NAMES = require('./../../../../common/src/Directions').NAMES; const BaseApi = require('./../../../../common/src/net/Api'); + class Api extends BaseApi { constructor(parent) { super(parent); - this.api[TYPES.REQ_MOVE_ORG] = this._moveOrg.bind(this); - this.api[TYPES.REQ_GET_ID] = this._getId.bind(this); + const servers = parent.aroundServers; + + this.api[TYPES.REQ_MOVE_ORG] = this._onMoveOrgFromClient.bind(this); + this.api[TYPES.REQ_MOVE_ORG_FROM_SERVER] = this._onMoveOrgFromServer.bind(this, false); + this.api[TYPES.REQ_MOVE_ORG_BACK] = this._onMoveOrgFromServer.bind(this, true); + this.api[TYPES.REQ_GET_ID] = this._onGetId.bind(this); + this.api[TYPES.REQ_SET_NEAR_ACTIVE] = this._onSetNearServer.bind(this); - this._onCloseCb = this._onClose.bind(this); + this._onCloseCb = this._onClose.bind(this); + this._onServerOpenCb = this._onServerOpen.bind(this); + this._onServerCloseCb = this._onServerClose.bind(this); Helper.override(parent, 'onClose', this._onCloseCb); + servers && servers.on(servers.EVENTS.OPEN, this._onServerOpenCb); + servers && servers.on(servers.EVENTS.CLOSE, this._onServerCloseCb); } destroy() { + const servers = this.parent.aroundServers; + Helper.unoverride(this.parent, 'onClose', this._onCloseCb); - this._onCloseCb = null; + servers && servers.off(servers.EVENTS.CLOSE, this._onServerCloseCb); + servers && servers.off(servers.EVENTS.OPEN, this._onServerOpenCb); + + this._onCloseCb = null; + this._onServerCloseCb = null; + this._onServerOpenCb = null; super.destroy(); } /** - * Moves organism from one client to another + * Moves organism from near server to current server + * @param {Boolean} back true, if the organism is sent back * @param {Number} reqId Unique request id. Needed for response - * @param {String} clientId Unique client id + * @param {String} clientId Unique source client id * @param {Number} x Current org X position * @param {Number} y Current org Y position * @param {Number} dir Moving direction * @param {String} orgJson Organism's serialized json * @api */ - _moveOrg(reqId, clientId, x, y, dir, orgJson) { - const region = Connections.toRegion(clientId); - - if (dir === DIR.UP) {region[1]--} - else if (dir === DIR.RIGHT) {region[0]++} - else if (dir === DIR.DOWN) {region[1]++} - else if (dir === DIR.LEFT) {region[0]--} - - const con = this.parent.conns.getConnection(region); - if (con.active) { - this.parent.request(con.sock, TYPES.REQ_MOVE_ORG, x, y, dir, orgJson); - } else { - const org = JSON.parse(orgJson); - const backRegion = Connections.toRegion(clientId); - const backCon = this.parent.conns.getConnection(backRegion); - this.parent.request(backCon.sock, TYPES.RES_MOVE_ERR, x, y, dir, orgJson, `Region "${region}" on direction "${DIR_NAMES[dir]}" is not active`); - Console.error(`Destination region ${region} is not active. Organism "${org.id}" will be sent back.`); - } + _onMoveOrgFromServer(back, reqId, clientId, x, y, dir, orgJson) { + const reg = Connections.toRegion(clientId); + const conns = this.parent.conns; + this._moveToClient(back, Connections.toId(conns.oppositeRegion(reg, dir)), x, y, dir, orgJson, false); } /** - * Creates response with unique client id for just connected clients or servers + * Moves organism from one client to another or to nearest server if + * it's connected to current one * @param {Number} reqId Unique request id. Needed for response - * @param {Boolean} isClient true for request from client, false for server + * @param {String} clientId Unique source client id + * @param {Number} x Current org X position + * @param {Number} y Current org Y position + * @param {Number} dir Moving direction + * @param {String} orgJson Organism's serialized json * @api */ - _getId(reqId, isClient = true) { - isClient && this._onGetClientId(reqId) || this._onGetServerId(reqId); + _onMoveOrgFromClient(reqId, clientId, x, y, dir, orgJson) { + const reg = Connections.toRegion(clientId); + const side = this.parent.conns.side - 1; + // + // This organism came from client, served by current server. We have + // to move it to other client on current server + // + if (dir === DIR.UP && reg[1] > 0 || dir === DIR.RIGHT && reg[0] < side || + dir === DIR.DOWN && reg[1] < side || dir === DIR.LEFT && reg[0] > 0) { + this._moveToClient(false, clientId, x, y, dir, orgJson); + return; + } + // + // This organism wants to move outside the current server - to the + // near server + // + this._moveToServer(clientId, x, y, dir, orgJson); } /** - * If it was a request from client, then we have to create unique clientId for him. - * @param {Number} reqId Unique request id + * Creates response with unique client id for just connected clients + * @param {Number} reqId Unique request id. Needed for response + * @api */ - _onGetClientId(reqId) { + _onGetId(reqId) { const sock = this.sock; const region = this.parent.conns.getFreeRegion(); const clientId = Connections.toId(region); @@ -88,23 +115,95 @@ class Api extends BaseApi { return; } this.parent.conns.setData(region, 'sock', sock); - this._setActive(clientId, true); + this._setActive(clientId, true); this.parent.response(sock, TYPES.RES_GET_ID_OK, reqId, clientId); - Console.info(`Client ${clientId} has connected`); + Console.info(`Client ${sock.remoteAddr} has connected on a position [${clientId}]`); + } + + /** + * Sets near server by direction + * @param {Number} reqId Unique request id. Needed for response + * @param {Number} dir Direction of incoming nearest server + * @api + */ + _onSetNearServer(reqId, dir) { + this.parent.aroundServers.setSocket(this.sock, dir); + this.parent.response(this.sock, TYPES.RES_SET_NEAR_ACTIVE_OK, reqId); + this._onServerOpen(dir); + Console.info(`[${NAMES[dir]}] server has connected`); } /** - * If it was a server, then we have to update our "around" servers (this.parent.activeAround) - * @param {Number} reqId Unique request id + * Moves organism's json to nearest client depending on dir parameter. + * This method is called in both client->client or server->client directions. + * If destination client is not active or there is no free space for the + * organism, then it will be sent back to source client. + * @param {Boolean} back true, if organism is sent back + * @param {String} clientId Unique source client id + * @param {Number} x Organism x coordinate + * @param {Number} y Organism y coordinate + * @param {Number} dir Moving direction + * @param {String} orgJson Organism's serialized json + * @param {Boolean} fromClient false if organism came from near server */ - _onGetServerId(reqId) { - // TODO: + _moveToClient(back, clientId, x, y, dir, orgJson, fromClient = true) { + let region = Connections.toRegion(clientId); + + if (fromClient) {region = Helper.flipRegion(region, dir)} + // + // If destination client active, then organism is moved there. + // Otherwise, we have to move it back to source client (possibly + // through near server) + // + const con = this.parent.conns.getConnection(region); + if (con.active) { + this.parent.request(con.sock, TYPES.REQ_MOVE_ORG, clientId, x, y, dir, orgJson, (type) => { + // + // No free space for organism - send it back to source client + // + type === TYPES.RES_MOVE_ERR && !back && this._moveBack(region, clientId, x, y, dir, orgJson, fromClient); + }); + } else { + !back && this._moveBack(region, clientId, x, y, dir, orgJson, fromClient); + } + } + + /** + * Moves organism's json to nearest server depending on dir parameter + * @param {String} clientId Unique source client id + * @param {Number} x Organism x coordinate + * @param {Number} y Organism y coordinate + * @param {Number} dir Moving direction + * @param {String} orgJson Organism's serialized json + */ + _moveToServer(clientId, x, y, dir, orgJson) { + const sock = this.parent.aroundServers.getSocket(dir); + sock && this.parent.request(sock, TYPES.REQ_MOVE_ORG_FROM_SERVER, clientId, x, y, dir, orgJson); + } + + /** + * Moves organism back to source client\server + * @param {Array} region Destination region if fromClient === true + * @param {String} clientId Unique source client id + * @param {Number} x Organism x coordinate + * @param {Number} y Organism y coordinate + * @param {Number} dir Moving direction + * @param {String} orgJson Organism's serialized json + * @param {Boolean} fromClient false if organism came from near server + */ + _moveBack(region, clientId, x, y, dir, orgJson, fromClient) { + const newDir = FLIP_DIR[dir]; + const parent = this.parent; + const sock = fromClient ? parent.conns.getConnection(Helper.flipRegion(region, newDir)).sock : parent.aroundServers.getSocket(newDir); + const newClientId = fromClient ? Connections.toId(region) : clientId; + const flipped = Helper.flip(x, y, newDir); + sock && parent.request(sock, TYPES.REQ_MOVE_ORG_BACK, newClientId, flipped[0], flipped[1], newDir, orgJson); } /** * Sets client active. It means, that sibling active client may * transfer it's organisms to this client - * @param {String} clientId + * @param {String} clientId Unique source client id * @param {Boolean} active */ _setActive(clientId, active) { @@ -123,21 +222,25 @@ class Api extends BaseApi { * @param {Array} activeRegion Region of activated client */ _activateAll(activeRegion) { - const server = this.parent; - const conns = server.conns; - const sock = server.conns.getConnection(activeRegion).sock; - // - // We have to send activate message every nearest client to - // current lying on activeRegion - // this._activateAround(activeRegion); - // - // We should also send around active clients status to the current (sock) - // - server.request(sock, TYPES.REQ_SET_NEAR_ACTIVE, DIR.DOWN, !!conns.getConnection(conns.downRegion(activeRegion)).sock); - server.request(sock, TYPES.REQ_SET_NEAR_ACTIVE, DIR.LEFT, !!conns.getConnection(conns.leftRegion(activeRegion)).sock); - server.request(sock, TYPES.REQ_SET_NEAR_ACTIVE, DIR.UP, !!conns.getConnection(conns.upRegion(activeRegion)).sock); - server.request(sock, TYPES.REQ_SET_NEAR_ACTIVE, DIR.RIGHT, !!conns.getConnection(conns.rightRegion(activeRegion)).sock); + this._activateCentral(activeRegion); + } + + _activateCentral(region) { + const server = this.parent; + const conns = server.conns; + const sock = server.conns.getConnection(region).sock; + const servers = this.parent.aroundServers; + const side = conns.side - 1; + const activeUp = !!_get(conns.getConnection(conns.upRegion (region)), 'sock') || region[1] === 0 && servers && servers.hasSocket(DIR.UP); + const activeRight = !!_get(conns.getConnection(conns.rightRegion(region)), 'sock') || region[0] === side && servers && servers.hasSocket(DIR.RIGHT); + const activeDown = !!_get(conns.getConnection(conns.downRegion(region)), 'sock') || region[1] === side && servers && servers.hasSocket(DIR.DOWN); + const activeLeft = !!_get(conns.getConnection(conns.leftRegion(region)), 'sock') || region[0] === 0 && servers && servers.hasSocket(DIR.LEFT); + + server.request(sock, TYPES.REQ_SET_NEAR_ACTIVE, DIR.UP, activeUp); + server.request(sock, TYPES.REQ_SET_NEAR_ACTIVE, DIR.RIGHT, activeRight); + server.request(sock, TYPES.REQ_SET_NEAR_ACTIVE, DIR.DOWN, activeDown); + server.request(sock, TYPES.REQ_SET_NEAR_ACTIVE, DIR.LEFT, activeLeft); } /** @@ -148,10 +251,10 @@ class Api extends BaseApi { _activateAround(region, activate = true) { const server = this.parent; const conns = server.conns; - const upSock = conns.getConnection(conns.upRegion(region)).sock; - const rightSock = conns.getConnection(conns.rightRegion(region)).sock; - const downSock = conns.getConnection(conns.downRegion(region)).sock; - const leftSock = conns.getConnection(conns.leftRegion(region)).sock; + const upSock = _get(conns.getConnection(conns.upRegion(region)), 'sock'); + const rightSock = _get(conns.getConnection(conns.rightRegion(region)), 'sock'); + const downSock = _get(conns.getConnection(conns.downRegion(region)), 'sock'); + const leftSock = _get(conns.getConnection(conns.leftRegion(region)), 'sock'); upSock && server.request(upSock, TYPES.REQ_SET_NEAR_ACTIVE, DIR.DOWN, activate); rightSock && server.request(rightSock, TYPES.REQ_SET_NEAR_ACTIVE, DIR.LEFT, activate); @@ -163,10 +266,68 @@ class Api extends BaseApi { * On connection close with one of the client we have to update active * state for nearest clients/Managers * @param {String} clientId Deactivated client id - * @private + * @param {WebSocket} sock + */ + _onClose(clientId, sock) { + const servers = this.parent.aroundServers; + const dir = servers ? servers.getDirection(sock) : clientId; + // + // This client was an extra client for current server. It means, that the + // server is full of other clients and there is no free slot for this + // + if (clientId === false) {return} + if (dir === DIR.NO || dir === clientId) { + this._activateAround(Connections.toRegion(clientId), false); + return; + } + + this._onServerClose(dir); + } + + /** + * Handler of opening connection with near server. dir parameter points to + * the direction of near server. If new near server appears, we have to notify + * near clients about it. + * @param {Number} dir Direction of server + */ + _onServerOpen(dir) { + this._updateConnections(dir, true); + } + + /** + * Handler of closing connection with near server. dir parameter points to + * the direction of near server. If near server disappears, we have to notify + * near clients about it. + * @param {Number} dir Direction of server */ - _onClose(clientId) { - this._activateAround(Connections.toRegion(clientId), false); + _onServerClose(dir) { + this._updateConnections(dir, false); + } + + _updateConnections(dir, open) { + const server = this.parent; + const conns = server.conns; + const side = conns.side; + let region; + let line; + let sock; + let offs; + + if (dir === DIR.UP || dir === DIR.DOWN) { + line = dir === DIR.UP ? 0 : side - 1; + region = [0, line]; + offs = 0; + } else { + line = dir === DIR.LEFT ? 0 : side - 1; + region = [line, 0]; + offs = 1; + } + + for (let i = 0; i < side; i++) { + region[offs] = i; + sock = conns.getConnection(region).sock; + sock && server.request(sock, TYPES.REQ_SET_NEAR_ACTIVE, dir, open); + } } } diff --git a/server/src/server/plugins/ApiSpec.js b/server/src/server/plugins/ApiSpec.js index 72fde32..5eb2689 100644 --- a/server/src/server/plugins/ApiSpec.js +++ b/server/src/server/plugins/ApiSpec.js @@ -4,18 +4,15 @@ describe("server/src/server/plugins/Api", () => { const Observer = require('./../../../../common/src/Observer'); const Server = require('./../Server').Server; const SEVENTS = require('./../Server').EVENTS; - const OLD_MODE = Config.modeNodeJs; - Config.modeNodeJs = true; const Client = require('./../../../../client/src/manager/plugins/client/Client').Client; const CEVENTS = require('./../../../../client/src/manager/plugins/client/Client').EVENTS; const EVENT_AMOUNT = require('./../../../../client/src/share/Events').EVENT_AMOUNT; const Console = require('./../../../../client/src/share/Console'); const SConsole = require('./../../share/Console'); const Helper = require('./../../../../common/tests/Helper'); - const Request = require('./../../../../common/src/plugins/Request'); - const Api = require('./Api'); const waitEvent = Helper.waitEvent; const host = Config.serverHost; + const port = SConfig.port; let error; let warn; @@ -23,9 +20,13 @@ describe("server/src/server/plugins/Api", () => { let serror; let swarn; let sinfo; + let dist; beforeAll(() => { Config.serverHost = 'ws://127.0.0.1'; + dist = SConfig.modeDistributed; + SConfig.modeDistributed = false; + SConfig.port = Config.serverPort; error = Console.error; warn = Console.warn; info = Console.info; @@ -48,8 +49,9 @@ describe("server/src/server/plugins/Api", () => { Console.error = error; Console.warn = warn; Console.info = info; - Config.modeNodeJs = OLD_MODE; Config.serverHost = host; + SConfig.modeDistributed = dist; + SConfig.port = port; }); it("Checking unique id on client connect", (done) => { @@ -60,8 +62,9 @@ describe("server/src/server/plugins/Api", () => { this.activeAround = [false,false,false,false]; this._clientId = null; } - run() {} + run() {} stop() {} + resetActive() {} get clientId() {return this._clientId} set clientId(id) {this._clientId = id} } @@ -95,6 +98,7 @@ describe("server/src/server/plugins/Api", () => { } run() {} stop() {} + resetActive() {} set clientId(id) {this._clientId = id} } let maxCon = SConfig.maxConnections; diff --git a/server/src/share/Config.js b/server/src/share/Config.js index 05d3818..4424cc8 100644 --- a/server/src/share/Config.js +++ b/server/src/share/Config.js @@ -31,21 +31,50 @@ ServerConfig.init({ * with additional "around" rows and columns for connecting with sibling * servers. So, result amount of cells will be e.g.: 16 + 2 rows + 2 cols. */ - maxConnections: 100, + maxConnections: 4, /** - * {Number} Port number for connecting with server + * {Number} Port number for connecting with server. This value will be + * passed to the server during creation */ - port: 8099, + port: 8301, /** - * TODO: this config should be obtained from Admin server or from command line - * TODO: parameters in future cmd line parser + * {Boolean} Means, that this server will be run in distributed mode. And + * will be connected with near servers (up...left). false, means, that + * server will be run in single server mode (no near servers). + */ + modeDistributed: true, + /** + * {String} Host address of server above. Shouldn't contain port */ upHost: 'ws://127.0.0.1', /** - * TODO: this config should be obtained from Admin server or from command line - * TODO: parameters in future cmd line parser + * {Number} Port number of server above. + */ + upPort: 8200, + /** + * {String} Host address of server on the right. Shouldn't contain port + */ + rightHost: 'ws://127.0.0.1', + /** + * {Number} Port number of server on the right + */ + rightPort: 8302, + /** + * {String} Host address of server below. Shouldn't contain port + */ + downHost: 'ws://127.0.0.1', + /** + * {Number} Port number of server below + */ + downPort: 8202, + /** + * {String} Host address of server on the left. Shouldn't contain port + */ + leftHost: 'ws://127.0.0.1', + /** + * {Number} Port number of server on the left */ - upPort: 8099 + leftPort: 8203 }); module.exports = {Config: ServerConfig.cfg(), api: ServerConfig}; \ No newline at end of file diff --git a/server/src/share/Console.js b/server/src/share/Console.js index 7ae574b..b4776d1 100644 --- a/server/src/share/Console.js +++ b/server/src/share/Console.js @@ -4,9 +4,14 @@ * @author flatline */ class Console { - static error(...msg) {console.log('\x1b[31m%s\x1b[0m', msg.join(''))} - static warn (...msg) {console.log('\x1b[33m%s\x1b[0m', msg.join(''))} - static info (...msg) {console.log('\x1b[37m%s\x1b[0m', msg.join(''))} + static error(...msg) {console.log('%s \x1b[31m%s\x1b[0m', this._time(), msg.join(''))} + static warn (...msg) {console.log('%s \x1b[33m%s\x1b[0m', this._time(), msg.join(''))} + static info (...msg) {console.log('%s \x1b[32m%s\x1b[0m', this._time(), msg.join(''))} + static _time() { + const date = new Date; + + return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; + } } module.exports = Console; \ No newline at end of file