From 73d1a29a179a361265edd15dd6a409fe48840b82 Mon Sep 17 00:00:00 2001 From: Edd Porter Date: Tue, 7 Feb 2012 17:27:27 +0000 Subject: [PATCH] Fix async execution and write multi-step searches Resolved an issue with the asynchronous execution: it is now actually asynchronous and not a direct call to another function. There appears to be a bug in the unit testing library that doesn't support usage of `process.nextTick`, but for now this has been worked around by using the less efficient `setTimeout callback, 0` function. The search algorithm also now supports multiple step paths and caveats identified around that have been documented in the code (regarding state copying). Unit tests for the above have been written. --- .gitignore | 1 + lib/search.coffee | 73 +++++++++++++++++++++++++++++++++------------- package.json | 4 ++- test/search.coffee | 56 ++++++++++++++++++++++++++++++++--- 4 files changed, 109 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 06f62bf..08a4c71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.js node_modules +*.swp diff --git a/lib/search.coffee b/lib/search.coffee index 2ef1da4..b5fd887 100644 --- a/lib/search.coffee +++ b/lib/search.coffee @@ -1,3 +1,6 @@ +util = require('util') +_ = require 'underscore' + Search = () -> return @@ -8,33 +11,63 @@ Search.prototype = { next_actions: null + # (state, action) + # This function must not modify the `state` variable that is passed by + # reference. Instead, a fresh object _must_ be returned. apply_action_to_state: null search: (initial_state, callback) -> frontier = [ [ { state: initial_state, action: null } ] ] explored = [] - loop - console.log 'New loop. Frontier length: ' + frontier.length - if frontier.length == 0 - callback false if callback? + if callback != undefined + console.log 'found callback' + self = this + setTimeout () -> + console.log 'Going async' + self.search_loop frontier, explored, callback + , 0 + else + this.search_loop frontier, explored, callback + + search_loop: (frontier, explored, callback) -> + console.log 'New loop. Frontier length: ' + frontier.length + if frontier.length == 0 + if callback? + callback false + return + else return false - path = this.remove_choice frontier - console.log 'Next path choice: ' + require('util').inspect path - s = path[path.length - 1] # s = path.end - explored[explored.length] = s # add s to explored - console.log 'Number of explored states: ' + explored.length - if this.is_goal(s) - console.log 'Goal state found: ' + require('util').inspect s - callback true, path if callback? + [path, frontier] = this.remove_choice frontier + console.log 'Next path choice: ' + util.inspect path + s = path[path.length - 1] # s = path.end + explored[explored.length] = s # add s to explored + console.log 'Number of explored states: ' + explored.length + if this.is_goal(s.state) + console.log 'Goal state found: ' + util.inspect s + if callback? + callback true, path + return + else return path - for a in this.next_actions(s) - result = this.apply_action_to_state s, a - if not result in frontier and not result in explored - new_path = path - new_path[new_path.length - 1] = - state: result - action: a - frontier[frontier.length - 1] = new_path + for a in this.next_actions(s.state) + console.log 'Trying actions: ' + util.inspect a + result = this.apply_action_to_state s.state, a + console.log 'Action state result: ' + util.inspect result + if not (result in frontier) and not (result in explored) + console.log 'New state' + new_path = path.slice(0) + new_path[new_path.length] = + state: result + action: a + frontier[frontier.length] = new_path + if callback != undefined + self = this + setTimeout () -> + console.log 'Looping' + self.search_loop frontier, explored, callback + , 0 + else + this.search_loop frontier, explored, callback } exports.Search = Search diff --git a/package.json b/package.json index 694bf53..94f1d17 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "engines": { "node": ">= 0.6.10" }, - "dependencies": {}, + "dependencies": { + "underscore": ">= 1.3.1" + }, "devDependencies": { "nodeunit": ">= 0.6.4" }, diff --git a/test/search.coffee b/test/search.coffee index 1cd0f42..3fbcb58 100644 --- a/test/search.coffee +++ b/test/search.coffee @@ -67,9 +67,16 @@ exports['search'] = nodeunit.testCase { test.notEqual this.search.search, null test.done() - 'sync_allStatesAreGoal_returnsInitialState': (test) -> + 'async_testLoopingPath_success': (test) -> + this.search.search_loop = (f, e, c) -> + console.log 'looping' + test.done() + this.search.search null, () -> + return + + 'sync_allStatesAreGoals_returnsInitialState': (test) -> this.search.remove_choice = (frontier) -> - frontier[0] + [frontier[0], frontier[1..]] this.search.is_goal = (state) -> true initial_state = @@ -80,9 +87,9 @@ exports['search'] = nodeunit.testCase { test.equal result[0].state, initial_state test.done() - 'async_allStatesAreGoal_returnsInitialState': (test) -> + 'async_allStatesAreGoals_returnsInitialState': (test) -> this.search.remove_choice = (frontier) -> - frontier[0] + [frontier[0], frontier[1..]] this.search.is_goal = (state) -> true initial_state = @@ -92,4 +99,45 @@ exports['search'] = nodeunit.testCase { test.equal result.length, 1 test.equal result[0].state, initial_state test.done() + + 'sync_allButStartStateAreGoals_returnsLengthTwoPath': (test) -> + initial_state = + data1: 'stuff' + data2: 81 + this.search.remove_choice = (frontier) -> + [frontier[0], frontier[1..]] + this.search.is_goal = (state) -> + console.dir state + state != initial_state + this.search.next_actions = (state) -> + ['up'] + this.search.apply_action_to_state = (state, action) -> + { data1: state.data1, data2: state.data2 + 1 } + result = this.search.search initial_state + test.equal result.length, 2 + test.equal result[0].state, initial_state + test.notEqual result[1].state, initial_state + test.done() + + 'async_allButStartStateAreGoals_returnsLengthTwoPath': (test) -> + initial_state = + data1: 'stuff' + data2: 81 + this.search.remove_choice = (frontier) -> + [frontier[0], frontier[1..]] + this.search.is_goal = (state) -> + console.dir state + state != initial_state + this.search.next_actions = (state) -> + ['up'] + this.search.apply_action_to_state = (state, action) -> + { data1: state.data1, data2: state.data2 + 1 } + this.search.search initial_state, (success, result) -> + test.equal result.length, 2 + test.equal result[0].state, initial_state + test.notEqual result[1].state, initial_state + test.done() + + # TODO: Create a test for paths that check back on themselves thus + # testing the array 'contains' code and the rejection of the state }