From 6f4baca64ff33754c01247a9a3df57fd8695f44b Mon Sep 17 00:00:00 2001 From: Nik Coughlin Date: Thu, 29 Oct 2015 12:33:40 +1300 Subject: [PATCH 1/3] Initial commit for 0.2.0 --- examples/mail-room.js | 24 +++++ hrm-cpu.js | 188 ++++++++++++++++++++++++++++++++++++++ hrm.js | 155 -------------------------------- normalize-options.js | 37 -------- package.json | 4 +- parse-args.js | 205 ++++++++++++++++++++++++++++++++++++++++++ polyfills.js | 28 ++++++ readme.md | 138 ++++++++++++++++++++++++---- runtime-errors.js | 25 ++++++ test/api.js | 175 ++++++++++++++++++++++++++++++++++++ test/levels.js | 6 +- test/runtime.js | 64 +++++++------ 12 files changed, 811 insertions(+), 238 deletions(-) create mode 100644 examples/mail-room.js create mode 100644 hrm-cpu.js delete mode 100644 hrm.js delete mode 100644 normalize-options.js create mode 100644 parse-args.js create mode 100644 polyfills.js create mode 100644 test/api.js diff --git a/examples/mail-room.js b/examples/mail-room.js new file mode 100644 index 0000000..d1ca0e5 --- /dev/null +++ b/examples/mail-room.js @@ -0,0 +1,24 @@ +const fs = require( 'fs' ) + +fs.readFile( '../asm/Mail-Room.asm', 'utf8', ( err, source ) => { + if( err ){ + throw( err ) + return + } + + execute( source ) +}) + +const execute = source => { + const HrmCpu = require( '../hrm-cpu' ) + + const inbox = [ 1, 9, 4 ] + + const program = HrmCpu( source, inbox ) + + console.log( JSON.stringify( program, null, 2 ) ) + + console.log( program.run() ) + + console.log( JSON.stringify( program, null, 2 ) ) +} \ No newline at end of file diff --git a/hrm-cpu.js b/hrm-cpu.js new file mode 100644 index 0000000..351554d --- /dev/null +++ b/hrm-cpu.js @@ -0,0 +1,188 @@ +const parseArgs = require( './parse-args' ) +const runtimeErrors = require( './runtime-errors' ) + +require( './polyfills' ) + +//no rest parameters in node yet :/ +const HrmCpu = function(){ + const setup = parseArgs( Array.from( arguments ) ) + + const options = setup.options + const state = setup.state + + const step = () => { + var line = state.program[ state.counter ] + var instr = line[ 0 ] + var arg = line[ 1 ] + var dereferenced = false + + if( arg && ( String( arg ) ).startsWith( '[' ) ){ + arg = state.memory[ parseInt( arg.substr( 1 ) ) ] + dereferenced = true + } + + var errorState = ErrorState( dereferenced, options, state ) + + try{ + runtimeErrors( instr, arg, errorState ) + } catch( err ){ + state.running = false + + return err + } + + executor( state )[ instr ]( arg ) + + state.running = state.counter < state.program.length + } + + return { + options, + state, + run: cb => { + const isCb = typeof cb === 'function' + + state.running = state.counter < state.program.length + + while( state.running ){ + var err = step() + + if( err ){ + if( isCb ){ + cb( err, state ) + return + } + + throw err + } + } + + if( isCb ){ + cb( null, state.outbox, state ) + return + } + + return state.outbox + }, + step: cb => { + const isCb = typeof cb === 'function' + + state.running = state.counter < state.program.length + + var err = null + + if( state.running ){ + err = step() + } + + if( err ){ + if( isCb ){ + cb( err, state ) + return + } + + throw err + } + + if( isCb ){ + cb( null, state ) + return + } + + return state + } + } +} + +const cpu = { + INBOX: state => + state.accumulator = state.inbox.shift(), + + OUTBOX: state => { + state.outbox.push( state.accumulator ) + state.accumulator = null + }, + + COPYFROM: ( state, address ) => + state.accumulator = state.memory[ address ], + + COPYTO: ( state, address ) => + state.memory[ address ] = state.accumulator, + + ADD: ( state, address ) => + state.accumulator = add( state.accumulator, state.memory[ address ] ), + + SUB: ( state, address ) => + state.accumulator = sub( state.accumulator, state.memory[ address ] ), + + BUMPUP: ( state, address ) => { + state.memory[ address ] = add( state.memory[ address ], 1 ) + state.accumulator = state.memory[ address ] + }, + + BUMPDN: ( state, address ) => { + state.memory[ address ] = sub( state.memory[ address ], 1 ) + state.accumulator = state.memory[ address ] + }, + + JUMP: ( state, line ) => + state.counter = line, + + JUMPZ: ( state, line ) => + state.counter = state.accumulator === 0 ? line : state.counter + 1, + + JUMPN: ( state, line ) => + state.counter = state.accumulator < 0 ? line : state.counter + 1 +} + +const executor = state => + Object.keys( cpu ).reduce( ( execute, instr ) => { + execute[ instr ] = arg => { + if( instr === 'INBOX' && state.inbox.length === 0 ){ + state.counter = Infinity + + return + } + + cpu[ instr ]( state, arg ) + + if( !jumps.includes( instr ) ) + state.counter++ + + state.steps++ + + return + } + + return execute + }, {}) + +const ErrorState = ( dereferenced, options, state ) => { + return { + dereferenced, + commands: options.commands, + dereferencing: options.dereferencing, + maxSteps: options.maxSteps, + memorySize: options.columns * options.rows, + maxSize: options.maxSize, + minValue: options.minValue, + maxValue: options.maxValue, + steps: state.steps, + accumulator: state.accumulator, + memory: state.memory, + size: state.program.length + } +} + +const asNumber = s => + typeof s === 'string' ? s.charCodeAt( 0 ) : s + +const add = ( a, b ) => + asNumber( a ) + asNumber( b ) + +const sub = ( a, b ) => + asNumber( a ) - asNumber( b ) + +const jumps = [ 'JUMP', 'JUMPZ', 'JUMPN' ] + +module.exports = HrmCpu \ No newline at end of file diff --git a/hrm.js b/hrm.js deleted file mode 100644 index 344a2e9..0000000 --- a/hrm.js +++ /dev/null @@ -1,155 +0,0 @@ -const parse = require( 'hrm-parser' ) -const normalizeOptions = require( './normalize-options' ) -const runtimeErrors = require( './runtime-errors' ) - -const asNumber = s => - typeof s === 'string' ? s.charCodeAt( 0 ) : s - -const add = ( a, b ) => - asNumber( a ) + asNumber( b ) - -const sub = ( a, b ) => - asNumber( a ) - asNumber( b ) - -module.exports = ( source, inbox, options, verbose ) => { - const input = inbox.slice() - const output = [] - const memory = [] - - var accumulator = null - var counter = 0 - var steps = 0 - - //ugh implement destructuring already! - const opts = normalizeOptions( options ) - const commands = opts.commands - const dereferencing = opts.dereferencing - const memorySize = opts.memorySize - const tiles = opts.tiles - const maxSteps = opts.maxSteps - - Object.keys( tiles ).forEach( key => - memory[ key ] = tiles[ key ] - ) - - const cpu = { - INBOX: () => { - if( input.length === 0 ){ - counter = Infinity - steps-- - - return - } - - accumulator = input.shift() - - counter++ - }, - - OUTBOX: () => { - output.push( accumulator ) - - accumulator = null - - counter++ - }, - - COPYFROM: address => { - accumulator = memory[ address ] - - counter++ - }, - - COPYTO: address => { - memory[ address ] = accumulator - - counter++ - }, - - ADD: address => { - accumulator = add( accumulator, memory[ address ] ) - - counter++ - }, - - SUB: address => { - accumulator = sub( accumulator, memory[ address ] ) - - counter++ - }, - - BUMPUP: address => { - memory[ address ] = add( memory[ address ], 1 ) - - accumulator = memory[ address ] - - counter++ - }, - - BUMPDN: address => { - memory[ address ] = sub( memory[ address ], 1 ) - - accumulator = memory[ address ] - - counter++ - }, - - JUMP: line => - counter = line, - - JUMPZ: line => - counter = accumulator === 0 ? line : counter + 1, - - JUMPN: line => - counter = accumulator < 0 ? line : counter + 1 - } - - const execute = ( program, i ) => { - if( i >= program.length ){ - return output - } - - const line = program[ i ] - const instr = line[ 0 ] - var arg = null - var dereferenced = false - - if( line.length > 1 ){ - var arg = line[ 1 ] - - if( ( String( arg ) ).startsWith( '[' ) ){ - arg = memory[ parseInt( arg.substr( 1 ) ) ] - dereferenced = true - } - } - - const state = { - accumulator, - memory, - memorySize, - dereferenced, - dereferencing, - steps, - maxSteps - } - - runtimeErrors( instr, arg, state ) - cpu[ instr ]( arg ) - - steps++ - - return execute( program, counter ) - } - - const program = parse( source ) - - const result = execute( program, 0 ) - - return verbose ? { - accumulator, - memory, - outbox: result, - size: program.length, - steps - } : result -} \ No newline at end of file diff --git a/normalize-options.js b/normalize-options.js deleted file mode 100644 index 7db33ee..0000000 --- a/normalize-options.js +++ /dev/null @@ -1,37 +0,0 @@ -const defaults = { - tiles: {}, - memorySize: Infinity, - commands: [ 'INBOX', 'OUTBOX', 'COPYFROM', 'COPYTO', 'ADD', 'SUB', 'BUMPUP', 'BUMPDN', 'JUMP', 'JUMPZ', 'JUMPN' ], - dereferencing: true, - maxSteps: 5000 -} - -const regex = { - digit: /^\d+$/ -} - -const isTiles = obj => - Object.keys( obj ).every( key => - regex.digit.test( key ) - ) - -module.exports = options => { - //nothing - if( !options ) - return defaults - - //{ 0: 9 } - if( isTiles( options ) ){ - return Object.assign( {}, defaults, { tiles: options } ) - } - - //{ columns, rows, tiles } - if( options.columns && options.rows ){ - return Object.assign( {}, defaults, { - memorySize: options.columns * options.rows, - tiles: options.tiles || {} - }) - } - - return Object.assign( {}, defaults, options ) -} \ No newline at end of file diff --git a/package.json b/package.json index 094ae08..f1274f9 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "hrm-cpu", - "version": "0.1.14", + "version": "0.2.0", "description": "Run Human Resource Machine programs in JavaScript", - "main": "hrm.js", + "main": "hrm-cpu.js", "directories": { "example": "examples" }, diff --git a/parse-args.js b/parse-args.js new file mode 100644 index 0000000..2d03073 --- /dev/null +++ b/parse-args.js @@ -0,0 +1,205 @@ +const parse = require( 'hrm-parser' ) + +const parseArgs = args => { + const arity = args.length + + if( arity in argsToSetup ){ + const handlers = argsToSetup[ arity ] + + const handlerKey = Object.keys( handlers ).find( key => { + const handler = handlers[ key ] + + return handler.test( args ) + }) + + if( handlerKey ){ + const handler = handlers[ handlerKey ] + + return handler.setup( args ) + } + + throw ArgumentError( 'Unexpected arguments' ) + } + + throw ArgumentError( 'Expected no more than 3 arguments' ) +} + +const ArgumentError = message => { + return { + name: 'Argument Error', + message + } +} + +const regex = { + digit: /^\d+$/ +} + +const isSource = obj => + typeof obj === 'string' || Array.isArray( obj ) + +const isOptions = obj => + !Array.isArray( obj ) && typeof obj === 'object' + +const isState = obj => + Object.keys( State() ).every( key => + key in obj + ) + +const isTiles = obj => + typeof obj === 'object' && + Object.keys( obj ).every( key => + regex.digit.test( key ) + ) + +const isFloor = obj => + obj.columns && obj.rows + +const optionsToState = ( options ) => { + const state = State() + + state.inbox = options.inbox.slice() + + Object.keys( options.tiles ).forEach( key => + state.memory[ key ] = options.tiles[ key ] + ) + + state.program = Array.isArray( options.source ) ? + options.source : + parse( options.source ) + + return state +} + +const argsToSetup = { + 1: { + state: { + test: args => + isState( args[ 0 ] ), + + setup: args => { + const state = args[ 0 ] + + return { options: Options(), state } + } + }, + + options: { + test: args => + isOptions( args[ 0 ] ), + + setup: args => { + const options = Object.assign( Options(), args[ 0 ] ) + const state = optionsToState( options ) + + return { options, state } + } + } + }, + 2: { + sourceInbox: { + test: args => + isSource( args[ 0 ] ) && + Array.isArray( args[ 1 ] ), + + setup: args => { + const options = Object.assign( Options(), { + source: args[ 0 ], + inbox: args[ 1 ] + }) + + const state = optionsToState( options ) + + return { options, state } + } + }, + + sourceOptions: { + test: args => + isSource( args[ 0 ] ) && + isOptions( args[ 1 ] ), + + setup: args => { + const options = Object.assign( Options(), args[ 1 ], { + source: args[ 0 ] + }) + + const state = optionsToState( options ) + + return { options, state } + } + } + }, + + 3: { + sourceInboxTiles: { + test: args => + isSource( args[ 0 ] ) && + Array.isArray( args[ 1 ] ) && + isTiles( args[ 2 ] ), + + setup: args => { + const options = Object.assign( Options(), { + source: args[ 0 ], + inbox: args[ 1 ], + tiles: args[ 2 ] + }) + + const state = optionsToState( options ) + + return { options, state } + } + }, + + sourceInboxFloor: { + test: args => + isSource( args[ 0 ] ) && + Array.isArray( args[ 1 ] ) && + isFloor( args[ 2 ] ), + + setup: args => { + const floor = args[ 2 ] + + const options = Object.assign( Options(), { + source: args[ 0 ], + inbox: args[ 1 ] + }, floor ) + + const state = optionsToState( options ) + + return { options, state } + } + } + } +} + +const Options = () => { + return { + source: '', + inbox: [], + tiles: [], + columns: Infinity, + rows: Infinity, + commands: [ 'INBOX', 'OUTBOX', 'COPYFROM', 'COPYTO', 'ADD', 'SUB', 'BUMPUP', 'BUMPDN', 'JUMP', 'JUMPZ', 'JUMPN' ], + dereferencing: true, + maxSteps: 5000, + maxSize: 255, + minValue: -999, + maxValue: 999 + } +} + +const State = () => { + return { + program: [], + accumulator: null, + inbox: [], + outbox: [], + memory: [], + counter: 0, + steps: 0, + running: false + } +} + +module.exports = parseArgs \ No newline at end of file diff --git a/polyfills.js b/polyfills.js new file mode 100644 index 0000000..8dd9c69 --- /dev/null +++ b/polyfills.js @@ -0,0 +1,28 @@ +if (!Array.prototype.includes) { + Array.prototype.includes = function(searchElement /*, fromIndex*/ ) { + 'use strict'; + var O = Object(this); + var len = parseInt(O.length) || 0; + if (len === 0) { + return false; + } + var n = parseInt(arguments[1]) || 0; + var k; + if (n >= 0) { + k = n; + } else { + k = len + n; + if (k < 0) {k = 0;} + } + var currentElement; + while (k < len) { + currentElement = O[k]; + if (searchElement === currentElement || + (searchElement !== searchElement && currentElement !== currentElement)) { + return true; + } + k++; + } + return false; + }; +} \ No newline at end of file diff --git a/readme.md b/readme.md index 855d757..f6bb925 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,8 @@ # hrm-cpu ## Run Human Resource Machine programs in JavaScript -This is a JavaScript runtime for the programs you write in the game [Human Resource Machine](http://tomorrowcorporation.com/humanresourcemachine) +This is a JavaScript runtime for the programs you write in the game +[Human Resource Machine](http://tomorrowcorporation.com/humanresourcemachine) ![Screenshot](http://tomorrowcorporation.com/blog/wp-content/themes/tcTheme2/images/hrm/screenshots/hrm_04.png) @@ -15,8 +16,10 @@ This is a JavaScript runtime for the programs you write in the game [Human Resou Requires node 4.x - uses ES6 +#### Basic Usage + ```javascript -const hrm = require( 'hrm-cpu' ) +const HrmCpu = require( 'hrm-cpu' ) //source is a string in the Human Resource Machine program format const source = loadSomeSourceFile() @@ -25,47 +28,152 @@ const source = loadSomeSourceFile() const inbox = [ 5, 18 ] //initial memory state - set just slot 9 to 0 -const floor = { +const tiles = { 9: 0 } -const outbox = hrm( source, inbox, floor ) +const outbox = HrmCpu( source, inbox, tiles ).run() console.log( outbox ) ``` +Some levels don't require any floor tiles, in which case you can do: + +```javascript +const outbox = HrmCpu( source, inbox ).run() +``` + +Some levels don't require an inbox, but if this is the case they must have floor +tiles: + +```javascript +const outbox = HrmCpu( source, [], tiles ).run() +``` + +#### Advanced Usage + Also see examples directory or try running the tests with mocha -If you have no initial floor state (like the early levels) you can omit floor: +##### Factory overloads ```javascript -const outbox = hrm( source, inbox ) +HrmCpu( state ) +HrmCpu( options ) +HrmCpu( source, inbox ) +HrmCpu( source, options ) +HrmCpu( source, inbox, tiles ) +HrmCpu( source, inbox, floor ) ``` -You can also get verbose output (for checking your size and steps): +HrmCpu is a factory function that returns an object with some properties and +the functions `run` and `step` ```javascript -const info = hrm( source, inbox, floor, true ) +{ + options: {...}, + state: {...}, + run: ( cb ) => ..., + step: ( cb ) => ... +} ``` -If you want verbose output but have no floor, pass an empty array or object: +Regardless of which overloads you choose, the factory requires a source file, and +either of, or both of, inbox and floor state + +If `run` is called synchronously it runs the whole program and returns the outbox + +If `step` is called synchronously it executes a single line of the program, and +returns the program state + +If `run` is called asynchronously it runs the whole program and then calls the +callback with `( err, outbox, state )` + +If `step` is called asynchronously it executes a single line and calls with +`( err, state )` + +##### Async example ```javascript -const info = hrm( source, inbox, {}, true ) +HrmCpu( source, inbox, tiles ).run( ( err, outbox ) => { + if( err ){ + console.error( err ) + } else { + console.log( outbox ) + } +}) ``` -There is also an alternative floor syntax to allow the use of data from -[hrm-level-data](https://github.com/atesgoral/hrm-level-data/blob/master/hrm-level-data-schema.json): +##### Get program state from run ```javascript -const floor = { - tiles: { - 9: 0 +HrmCpu( source, inbox, tiles ).run( ( err, outbox, state ) => { + if( err ){ + console.error( err ) + } else { + console.log( state ) } +}) +``` + +Produces an object like the following, but populated with the appropriate data: + +```javascript +{ + program: [], + accumulator: null, + inbox: [], + outbox: [], + memory: [], + counter: 0, + steps: 0, + running: false +} +``` + +##### Options + +Passed in options extend the default options, which are: + +``` +{ + source: '', + inbox: [], + tiles: [], + columns: Infinity, + rows: Infinity, + commands: [ 'INBOX', 'OUTBOX', 'COPYFROM', 'COPYTO', 'ADD', 'SUB', 'BUMPUP', 'BUMPDN', 'JUMP', 'JUMPZ', 'JUMPN' ], + dereferencing: true, + maxSteps: 5000, + maxSize: 255, + minValue: -999, + maxValue: 999 } ``` +These match the constraints of the actual game + +`source`, `inbox` and `tiles` are described above + +`columns` and `rows` describe the floor layout - these determine the valid floor +addresses and if set and your program tries to access a tile outside of these, +an error will be thrown or sent to your callback, depending on whether you used +the sync or async call + +`commands` are the commands allowed for the current program - for example, in +the early levels many commands are not yet available + +`dereferencing` is whether or not the program can access memory indirectly eg `[12]` + +`maxSteps` is the maximum number of steps your program can run + +`maxSize` is the maximum length of a program, not counting labels and comments. +A jump only counts as one line + +`minValue` is the minimum value that you can hold in your hand + +`maxValue` is the maximum value that you can hold in your hand + ### Related projects * [hrmsandbox]( https://github.com/sixlettervariables/hrmsandbox ) - has a web interface, command line bin, expression grammar and many other cool features diff --git a/runtime-errors.js b/runtime-errors.js index d527b09..63df6d7 100644 --- a/runtime-errors.js +++ b/runtime-errors.js @@ -1,3 +1,5 @@ +require( './polyfills' ) + const checks = { OUTBOX: { checks: [ 'accumulator' ] @@ -74,7 +76,30 @@ const TooManyStepsError = maxSteps => { } } +const ProgramTooLongError = maxSize => { + return { + name: 'Program Too Long', + message: `Program too long! The maximum program length is ${ maxSize }!` + } +} + +const OverflowError = ( minValue, maxValue ) => { + return { + name: 'Overflow!', + message: `Overflow! Each data unit is restricted to values between ${ minValue } and ${ maxValue }. That should be enough for anybody.` + } +} + module.exports = ( instr, arg, state ) => { + if( state.accumulator < state.minValue || state.accumulator > state.maxValue ) + throw OverflowError( state.minValue, state.maxValue ) + + if( state.size > state.maxSize ) + throw ProgramTooLongError( state.maxSize ) + + if( !state.commands.includes( instr ) ) + throw InstrNotAllowedError( instr ) + if( state.dereferenced && !state.dereferencing ) throw DereferencingNotAllowedError() diff --git a/test/api.js b/test/api.js new file mode 100644 index 0000000..0938dda --- /dev/null +++ b/test/api.js @@ -0,0 +1,175 @@ +const assert = require( 'assert' ) +const HrmCpu = require( '../hrm-cpu' ) + +const fixtures = { + simple: { + source: 'INBOX OUTBOX', + inbox: [ 1 ], + outbox: [ 1 ] + }, + tiles: { + source: 'INBOX ADD 0 OUTBOX', + tiles: { 0: 2 }, + inbox: [ 1 ], + outbox: [ 3 ] + }, + floor: { + source: 'INBOX ADD 0 OUTBOX', + floor: { + columns: 2, + rows: 2, + tiles: { 0: 2 } + }, + inbox: [ 1 ], + outbox: [ 3 ] + }, + state: { + state: { + program: [ [ 'INBOX' ], [ 'OUTBOX' ] ], + accumulator: null, + inbox: [ 1 ], + outbox: [], + memory: [], + counter: 0, + steps: 0, + running: false + }, + outbox: [ 1 ] + } +} + +const ctor = { + "HrmCpu( state )": done => { + const outbox = HrmCpu( fixtures.state.state ).run() + + assert.deepEqual( outbox, fixtures.state.outbox ) + + done() + }, + "HrmCpu( options )": done => { + const options = { + source: fixtures.tiles.source, + inbox: fixtures.tiles.inbox, + tiles: fixtures.tiles.tiles + } + + const outbox = HrmCpu( options ).run() + + assert.deepEqual( outbox, fixtures.tiles.outbox ) + + done() + }, + "HrmCpu( source, inbox )": done => { + const outbox = HrmCpu( fixtures.simple.source, fixtures.simple.inbox ).run() + + assert.deepEqual( outbox, fixtures.simple.outbox ) + + done() + }, + "HrmCpu( source, options )": done => { + const options = { + inbox: fixtures.tiles.inbox, + tiles: fixtures.tiles.tiles + } + + const outbox = HrmCpu( fixtures.tiles.source, options ).run() + + assert.deepEqual( outbox, fixtures.tiles.outbox ) + + done() + }, + "HrmCpu( source, inbox, tiles )": done => { + const outbox = HrmCpu( + fixtures.tiles.source, fixtures.tiles.inbox, fixtures.tiles.tiles + ).run() + + assert.deepEqual( outbox, fixtures.tiles.outbox ) + + done() + }, + "HrmCpu( source, inbox, floor )": done => { + const outbox = HrmCpu( + fixtures.floor.source, fixtures.floor.inbox, fixtures.floor.floor + ).run() + + assert.deepEqual( outbox, fixtures.floor.outbox ) + + done() + } +} + +describe( 'hrm-cpu API', () => { + describe( 'ctor', () => + Object.keys( ctor ).forEach( name => + it( name, ctor[ name ] ) + ) + ) + + describe( 'step', () => { + it( 'HrmCpu( source, inbox ).step()', done => { + const hrm = HrmCpu( fixtures.simple.source, fixtures.simple.inbox ) + + var state = hrm.step() + + assert.equal( fixtures.simple.inbox, state.accumulator ) + + state = hrm.step() + + assert.deepEqual( state.outbox, fixtures.simple.outbox ) + + done() + }) + + it( 'HrmCpu( state ).step()', done => { + const hrm1 = HrmCpu( fixtures.simple.source, fixtures.simple.inbox ) + const state1 = hrm1.step() + + const hrm2 = HrmCpu( state1 ) + const state2 = hrm2.step() + + assert.deepEqual( state2.outbox, fixtures.simple.outbox ) + + done() + }) + }) + + describe( 'async', () => { + it( 'HrmCpu( source, inbox ).run( cb )', done => { + HrmCpu( fixtures.simple.source, fixtures.simple.inbox ).run( ( err, outbox, state ) => { + assert.equal( err, null ) + assert.deepEqual( outbox, fixtures.simple.outbox ) + assert.deepEqual( state.outbox, fixtures.simple.outbox ) + + HrmCpu( 'OUTBOX', fixtures.simple.inbox ).run( ( err ) => { + assert( err !== null ) + + done() + }) + }) + }) + + it( 'HrmCpu( source, inbox ).step( cb )', done => { + const hrm = HrmCpu( fixtures.simple.source, fixtures.simple.inbox ) + + hrm.step( + ( err, state ) => { + assert.equal( err, null ) + assert.equal( fixtures.simple.inbox, state.accumulator ) + + hrm.step( ( err, state ) => { + assert.equal( err, null ) + assert.deepEqual( state.outbox, fixtures.simple.outbox ) + + const hrm2 = HrmCpu( 'OUTBOX', fixtures.simple.inbox ) + + hrm2.step( err => { + assert( err !== null ) + + done() + }) + }) + } + ) + }) + }) +}) \ No newline at end of file diff --git a/test/levels.js b/test/levels.js index 0f4c8fb..7d92e3a 100644 --- a/test/levels.js +++ b/test/levels.js @@ -2,7 +2,7 @@ const assert = require( 'assert' ) const fs = require( 'fs' ) const path = require( 'path' ) const levels = require( 'hrm-level-data' ) -const hrm = require( '../hrm' ) +const HrmCpu = require( '../hrm-cpu' ) //cannot get my head around mocha's order execution, hence sync const levelAsm = level => { @@ -26,7 +26,9 @@ const testLevel = ( level, source ) => { describe( level.number + ' - ' + level.name, () => level.expect.forEach( ( test, i ) => { it( 'produces the correct output for test #' + ( i + 1 ), done => { - const outbox = hrm( source, test.inbox, level.floor ) + const hrm = HrmCpu( source, test.inbox, level.floor || {} ) + + const outbox = hrm.run() assert.deepEqual( test.outbox, outbox ) diff --git a/test/runtime.js b/test/runtime.js index bce1fda..0c42cab 100644 --- a/test/runtime.js +++ b/test/runtime.js @@ -1,29 +1,5 @@ const assert = require( 'assert' ) -const hrm = require( '../hrm' ) - -const level = { - number: 1, - name: 'Runtime Tests', - instructions: 'Fail.', - commands: [ 'OUTBOX', 'COPYFROM', 'COPYTO', 'ADD', 'SUB', 'BUMPUP', 'BUMPDOWN', 'JUMP', 'JUMPN', 'JUMPZ' ], - expect: [ - { - inbox: [ 1 ], - outbox: [] - } - ], - floor: { - tiles: { - 1: 0 - }, - columns: 1, - rows: 2 - }, - challenge: { - size: 1, - speed: 1 - } -} +const HrmCpu = require( '../hrm-cpu' ) const fails = { "OUTBOX: Empty Hands": ` @@ -74,16 +50,50 @@ a: `, "Too Many Steps": ` a: - JUMP A + JUMP a + `, + "Overflow": ` + BUMPUP 1 +a: + COPYFROM 1 + ADD 1 + COPYTO 1 + JUMP a ` } +var tooLong = '' +for( var i = 0; i < 256; i++ ){ + tooLong += ' COPYFROM 1' +} + +fails[ 'Program Too Long' ] = tooLong + +const Options = () => { + return { + commands: [ 'OUTBOX', 'COPYFROM', 'COPYTO', 'ADD', 'SUB', 'BUMPUP', 'BUMPDOWN', 'JUMP', 'JUMPN', 'JUMPZ' ], + tiles: { + 1: 0 + }, + columns: 1, + rows: 2 + } +} + describe( 'hrm-cpu runtime errors', () => Object.keys( fails ).forEach( key => { const source = fails[ key ] it( key, done => { - assert.throws( () => hrm( source, [ 1 ], floor ) ) + const inbox = [ 1 ] + const options = Object.assign( Options(), { source, inbox } ) + + const hrm = HrmCpu( options ) + + assert.throws( () => { + hrm.run() + }) + done() }) }) From b208a0a587ff5a7fbbdb3b93ec2b338a64d89294 Mon Sep 17 00:00:00 2001 From: Nik Coughlin Date: Thu, 29 Oct 2015 12:37:16 +1300 Subject: [PATCH 2/3] readme rejigged --- readme.md | 50 ++++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/readme.md b/readme.md index f6bb925..9eaa9d5 100644 --- a/readme.md +++ b/readme.md @@ -92,31 +92,9 @@ callback with `( err, outbox, state )` If `step` is called asynchronously it executes a single line and calls with `( err, state )` -##### Async example - -```javascript -HrmCpu( source, inbox, tiles ).run( ( err, outbox ) => { - if( err ){ - console.error( err ) - } else { - console.log( outbox ) - } -}) -``` - -##### Get program state from run - -```javascript -HrmCpu( source, inbox, tiles ).run( ( err, outbox, state ) => { - if( err ){ - console.error( err ) - } else { - console.log( state ) - } -}) -``` +##### State -Produces an object like the following, but populated with the appropriate data: +The state object when set to the initial defaults looks like: ```javascript { @@ -174,6 +152,30 @@ A jump only counts as one line `maxValue` is the maximum value that you can hold in your hand +##### Async example + +```javascript +HrmCpu( source, inbox, tiles ).run( ( err, outbox ) => { + if( err ){ + console.error( err ) + } else { + console.log( outbox ) + } +}) +``` + +##### Get program state from run + +```javascript +HrmCpu( source, inbox, tiles ).run( ( err, outbox, state ) => { + if( err ){ + console.error( err ) + } else { + console.log( state ) + } +}) +``` + ### Related projects * [hrmsandbox]( https://github.com/sixlettervariables/hrmsandbox ) - has a web interface, command line bin, expression grammar and many other cool features From 4733db28d08c516338405087d9527d89a02f94c6 Mon Sep 17 00:00:00 2001 From: Nik Coughlin Date: Thu, 29 Oct 2015 12:45:42 +1300 Subject: [PATCH 3/3] Fixes #24 --- readme.md | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 9eaa9d5..43e4606 100644 --- a/readme.md +++ b/readme.md @@ -178,9 +178,34 @@ HrmCpu( source, inbox, tiles ).run( ( err, outbox, state ) => { ### Related projects -* [hrmsandbox]( https://github.com/sixlettervariables/hrmsandbox ) - has a web interface, command line bin, expression grammar and many other cool features -* [hrm-level-data]( https://github.com/atesgoral/hrm-level-data ) - metadata for each level in the game, hrm-cpu uses this in its tests -* [hrm-solutions]( https://github.com/atesgoral/hrm-solutions ) - solutions and size/speed hacks for each level +#### Runtimes + +* [C++ assembler/runtime](https://github.com/LRFLEW/HRM-CCPU) +* [JavaScript runtime](https://github.com/sixlettervariables/hrmsandbox) + +#### Parsers + +* [Expression Grammar](https://github.com/sixlettervariables/hrm-grammar) +* [ES6 parser](https://github.com/nrkn/hrm-parser) + +#### Comment/label decoders & encoders + +* [JavaScript decoder](https://github.com/nrkn/hrm-image-decoder) +* [Java decoder](https://gist.github.com/sendow/4df27a857000deb18cf9) +* [Java encoder](https://gist.github.com/sendow/045f01668f976691ff6b) + +#### Viewers + +* [Program Viewer, web](https://github.com/AlanDeSmet/human-resource-machine-viewer) + +#### Solutions +* [Solutions and speed/size hacks, exploits etc](https://github.com/atesgoral/hrm-solutions) + +#### Utils/data + +* [Level data](https://github.com/atesgoral/hrm-level-data) +* [Inbox Generator](https://github.com/atesgoral/hrm-level-inbox-generator) +* [Outbox Generator](https://github.com/atesgoral/hrm-level-outbox-generator) Please let me know if you know of any others!