diff --git a/README.md b/README.md index e730d44..8b4dab7 100644 --- a/README.md +++ b/README.md @@ -482,6 +482,30 @@ Finally, we run 50 iterations of the scenario: environment.run(50) ``` +## Messaging +JS-son agents can send "private" messages to any other JS-son agent, which the environment will then relay to this agent only. +Agents can send these messages in the same way they register the execution of an action as the result of a plan. +For example, in the plan below, an agent sends the message ``'Hi!'`` to the agent with the ID ``alice``: + +```JavaScript +const messagePlans = [ + Plan(_ => true, () => ({ messages: [{ message: 'Hi!', agentId: 'alice' }] })) +] +``` + +Assuming that the sending agent has the ID ``bob``, the agent ``alice`` will receive the following belief update: + +```JavaScript +beliefs = { + ...beliefs, + messages: { + bob: ['Hi!'] + } +} +``` + +Note that messages do not need to be strings, but can be of any type, for example objects. + ## Further Examples ### Data Science diff --git a/spec/integration/beliefPlan.spec.js b/spec/integration/beliefPlan.spec.js index ca90e8e..633e2c9 100644 --- a/spec/integration/beliefPlan.spec.js +++ b/spec/integration/beliefPlan.spec.js @@ -23,6 +23,7 @@ const { updateMAS } = require('../mocks/environment') describe('Integration: belief-plan approach', () => { const human = new Agent('human', beliefs, desires, plans, preferenceFunctionGen) const dog = new Agent('dog', dogBeliefs, dogDesires, dogPlans, dogPreferenceFunctionGen) + const emptyMessageObject = { human: {}, dog: {} } const state = { dogNice: true, @@ -39,12 +40,14 @@ describe('Integration: belief-plan approach', () => { dogNice: true, dogHungry: true, foodAvailable: false, - dogRecentlyPraised: false + dogRecentlyPraised: false, + messages: emptyMessageObject }, { dogNice: true, dogHungry: false, foodAvailable: false, - dogRecentlyPraised: false + dogRecentlyPraised: false, + messages: emptyMessageObject }] expect(history).toEqual(expectedHistory) }) diff --git a/spec/integration/full.spec.js b/spec/integration/full.spec.js index 3bd13e9..3c8b8a7 100644 --- a/spec/integration/full.spec.js +++ b/spec/integration/full.spec.js @@ -23,6 +23,7 @@ const { updateMAS } = require('../mocks/environment') describe('Environment / run()', () => { const human = new Agent('human', beliefs, desires, plans, preferenceFunctionGen) const dog = new Agent('dog', dogBeliefs, dogDesires, dogPlans, dogPreferenceFunctionGen) + const emptyMessageObject = { human: {}, dog: {} } const state = { dogNice: true, @@ -39,12 +40,15 @@ describe('Environment / run()', () => { dogNice: true, dogHungry: true, foodAvailable: false, - dogRecentlyPraised: false + dogRecentlyPraised: false, + messages: emptyMessageObject + }, { dogNice: true, dogHungry: false, foodAvailable: false, - dogRecentlyPraised: false + dogRecentlyPraised: false, + messages: emptyMessageObject }] expect(history).toEqual(expectedHistory) }) diff --git a/spec/src/environment/Environment.spec.js b/spec/src/environment/Environment.spec.js index 5368eac..9a8e2e0 100644 --- a/spec/src/environment/Environment.spec.js +++ b/spec/src/environment/Environment.spec.js @@ -1,4 +1,5 @@ const Agent = require('../../../src/agent/Agent') +const Plan = require('../../../src/agent/Plan') const Environment = require('../../../src/environment/Environment') const { @@ -17,8 +18,11 @@ const { console.log = jasmine.createSpy('log') +const emptyMessageObject = { human: {}, dog: {} } + describe('Environment / run()', () => { const human = new Agent('human', beliefs, desires, plans, preferenceFunctionGen) + const dog = new Agent('dog', dogBeliefs, dogDesires, dogPlans, dogPreferenceFunctionGen) const state = { dogNice: true, @@ -26,19 +30,19 @@ describe('Environment / run()', () => { } const update = actions => (actions.some( - action => action.actions.includes('Here, take some food!')) ? { dogHungry: false } : {} + action => action.actions && + action.actions.includes('Here, take some food!')) ? { dogHungry: false } : {} ) it('Should process agent actions', () => { const environment = new Environment([human], state, update) environment.run(1) - expect(console.log).toHaveBeenCalledWith({ dogNice: true, dogHungry: false }) + expect(console.log).toHaveBeenCalledWith( + { dogNice: true, dogHungry: false, messages: { human: {} } } + ) }) it('Should allow agent-to-agent interaction', () => { - const dog = new Agent('dog', dogBeliefs, dogDesires, dogPlans, dogPreferenceFunctionGen) - // console.log(dog) - // const dog = createAgent('dog') const updateMAS = actions => { const stateUpdate = {} actions.forEach(action => { @@ -67,30 +71,32 @@ describe('Environment / run()', () => { dogNice: true, dogHungry: true, foodAvailable: false, - dogRecentlyPraised: false + dogRecentlyPraised: false, + messages: emptyMessageObject }, { dogNice: true, dogHungry: false, foodAvailable: false, - dogRecentlyPraised: false + dogRecentlyPraised: false, + messages: emptyMessageObject }] - expect(JSON.stringify(history)).toEqual(JSON.stringify(expectedHistory)) + expect(history).toEqual(expectedHistory) }) it('Should terminate after the specified number of iterations', () => { const environment = new Environment([human], state, update) const history = environment.run(2) const expectedHistory = [ - { dogNice: true, dogHungry: true }, - { dogNice: true, dogHungry: false }, - { dogNice: true, dogHungry: false } + { dogNice: true, dogHungry: true, messages: { human: {} } }, + { dogNice: true, dogHungry: false, messages: { human: {} } }, + { dogNice: true, dogHungry: false, messages: { human: {} } } ] expect(history).toEqual(expectedHistory) }) it('Should allow for the specification of a custom runner function', () => { const environment = new Environment( - [human], + [human, dog], state, update, state => console.log(state), @@ -98,9 +104,9 @@ describe('Environment / run()', () => { ) const history = environment.run(2) const expectedHistory = [ - { dogNice: true, dogHungry: true }, - { dogNice: true, dogHungry: false }, - { dogNice: true, dogHungry: false } + { dogNice: true, dogHungry: true, messages: emptyMessageObject }, + { dogNice: true, dogHungry: false, messages: emptyMessageObject }, + { dogNice: true, dogHungry: false, messages: emptyMessageObject } ] expect(history).toEqual(expectedHistory) }) @@ -125,30 +131,58 @@ describe('Environment / run()', () => { return state } const history1 = new Environment( - [human], + [human, dog], state, update, state => console.log(state), stateFilter1 ).run(2) const expectedHistory1 = [ - { dogNice: true, dogHungry: true }, - { dogNice: true, dogHungry: true }, - { dogNice: true, dogHungry: true } + { dogNice: true, dogHungry: true, messages: emptyMessageObject }, + { dogNice: true, dogHungry: true, messages: emptyMessageObject }, + { dogNice: true, dogHungry: true, messages: emptyMessageObject } ] expect(history1).toEqual(expectedHistory1) const history2 = new Environment( - [human], + [human, dog], state, update, state => console.log(state), stateFilter2 ).run(2) const expectedHistory2 = [ - { dogNice: true, dogHungry: true }, - { dogNice: true, dogHungry: false }, - { dogNice: true, dogHungry: false } + { dogNice: true, dogHungry: true, messages: emptyMessageObject }, + { dogNice: true, dogHungry: false, messages: emptyMessageObject }, + { dogNice: true, dogHungry: false, messages: emptyMessageObject } ] expect(history2).toEqual(expectedHistory2) }) + + it('Should allow for agent-to-agent message passing', () => { + const messagePlans = [ + Plan(_ => true, () => ({ messages: [{ message: 'Hi!', agentId: 'human' }] })) + ] + const filledMessageObject = { + ...emptyMessageObject, + human2: {}, + human: { human2: ['Hi!'] } + } + const human2 = new Agent('human2', beliefs, desires, messagePlans, preferenceFunctionGen) + const env = new Environment( + [human, human2, dog], + state, + update + ) + const history = env.run(2) + const expectedHistory = [ + { dogNice: true, dogHungry: true, messages: filledMessageObject }, + { dogNice: true, dogHungry: false, messages: filledMessageObject }, + { dogNice: true, dogHungry: false, messages: filledMessageObject } + ] + expect(history).toEqual(expectedHistory) + expect(env.agents.human.beliefs.messages.human2).toContain('Hi!') + expect(env.agents.human.beliefs.messages.human2.length).toEqual(1) + expect(env.agents.human2.beliefs.messages.human2.length).toEqual(0) + expect(env.agents.dog.beliefs.messages.human2.length).toEqual(0) + }) }) diff --git a/src/environment/Environment.js b/src/environment/Environment.js index 4201bcd..44a3b88 100644 --- a/src/environment/Environment.js +++ b/src/environment/Environment.js @@ -22,9 +22,13 @@ function Environment ( } } ) { - this.agents = {} - agents.forEach(agent => (this.agents[agent.id] = agent)) + state.messages = {} this.state = state + this.agents = {} + agents.forEach(agent => { + this.agents[agent.id] = agent + this.state.messages[agent.id] = {} + }) this.update = update this.render = render this.stateFilter = stateFilter @@ -35,9 +39,34 @@ function Environment ( this.history.push(this.state) const run = () => { Object.keys(this.agents).forEach(agentKey => { + const messages = {} + Object.keys(this.state.messages).forEach(recipient => { + Object.keys(this.state.messages[recipient]).forEach(sender => { + if (recipient !== agentKey) { + messages[sender] = [] + return + } + messages[sender] = this.state.messages[recipient][sender] + }) + }) + const preFilteredState = { ...this.state, messages } const proposedUpdate = this.agents[agentKey].next( - this.stateFilter(this.state, agentKey, this.agents[agentKey].beliefs) - ) + this.stateFilter(preFilteredState, agentKey, this.agents[agentKey].beliefs)) + proposedUpdate.forEach(action => { + Object.keys(this.state.messages).forEach(key => { + if (this.state.messages[key][agentKey]) this.state.messages[key][agentKey] = [] + }) + if (action.messages) { + action.messages.forEach( + message => { + if (!this.state.messages[[message.agentId]][agentKey]) { + this.state.messages[[message.agentId]][agentKey] = [] + } + this.state.messages[message.agentId][agentKey].push(message.message) + } + ) + } + }) const stateUpdate = this.update(proposedUpdate, agentKey, this.state) this.state = { ...this.state,