From f78493e192daf4639b91352909e4029b9970fcdb Mon Sep 17 00:00:00 2001 From: sanine Date: Wed, 8 Nov 2023 00:47:06 -0600 Subject: implement basic sensing & refactor world/cells -> lattice --- src/world/cell.js | 52 ------------------------ src/world/cell.test.js | 99 ---------------------------------------------- src/world/lattice.js | 52 ++++++++++++++++++++++++ src/world/lattice.test.js | 99 ++++++++++++++++++++++++++++++++++++++++++++++ src/world/proposal.js | 22 +++++------ src/world/proposal.test.js | 20 +++++----- src/world/sense.js | 17 ++++++++ src/world/sense.test.js | 33 ++++++++++++++++ 8 files changed, 222 insertions(+), 172 deletions(-) delete mode 100644 src/world/cell.js delete mode 100644 src/world/cell.test.js create mode 100644 src/world/lattice.js create mode 100644 src/world/lattice.test.js create mode 100644 src/world/sense.js create mode 100644 src/world/sense.test.js diff --git a/src/world/cell.js b/src/world/cell.js deleted file mode 100644 index 8795623..0000000 --- a/src/world/cell.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -// get the proposals for cell updates -export function cells_update(cells, update_rules) { - return cells - .map((row, y) => row.map((cell, x) => [x, y, cell.type])) - .flat() - .reduce((acc, [x, y, type]) => [...acc, update_rules[type](cells, x, y)], []) - .filter(x => x !== undefined) -} - - -// check if, given the current cells configuration, a proposal is valid -export function cells_valid(cells, proposal) { - if (!proposal.world_updates) { return true; } - return proposal.world_updates.reduce( - (acc, update) => { - const valid = - (update.x >= 0 && update.x < cells[0].length) && - (update.y >= 0 && update.y < cells.length) && - (cells[update.y][update.x].type == update.from) - return valid && acc; - }, - true - ); -} - - -// apply a set of proposals, returning the new cells -export function cells_apply(cells, proposals) { - return proposals.reduce( - (acc, prop) => { - const change = (prop.world_updates || []).reduce( - (acc_, update) => { - const cell = acc_[update.y][update.x]; - if (update.to) { cell.type = update.to; } - if (update.flags) { - cell.flags = cell.flags || {} - // this is very side-effect-y but i couldn't think of a nicer compatible way of doing it 😔 - for (let k of Object.keys(update.flags)) { - cell.flags[k] = update.flags[k]; - } - } - return acc_ - }, - [...acc] - ); - return change; - }, - [...cells].map(row => row.map(cell => ({ ...cell, flags: {}, }))) - ); -} diff --git a/src/world/cell.test.js b/src/world/cell.test.js deleted file mode 100644 index fb901eb..0000000 --- a/src/world/cell.test.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -import { cells_update, cells_valid, cells_apply } from './cell.js'; - - -test("growth update rule", () => { - const cells = [[ - { type: 'empty', flags: {} }, - { type: 'empty', flags: {} }, - { type: 'plant', flags: {} }, - ]]; - const update_rules = { - plant: () => {}, - empty: (cells, x, y) => { - if (cells[y][x+1].type === 'plant') { - return { world_updates: [{ x, y, from: 'empty', to: 'plant' }] }; - } - }, - }; - - expect(cells_update(cells, update_rules)).toEqual([ - { world_updates: [{ x: 1, y: 0, from: 'empty', to: 'plant' }] }, - ]); - - cells[0][1] = { type: 'plant' }; - - expect(cells_update(cells, update_rules)).toEqual([ - { world_updates: [{ x: 0, y: 0, from: 'empty', to: 'plant' }] }, - ]); - - cells[0][0] = { type: 'plant' }; - - expect(cells_update(cells, update_rules)).toEqual([]); -}); - - -test("growth update rule applied", () => { - const cells = [[ - { type: 'empty', flags: {} }, - { type: 'empty', flags: {} }, - { type: 'plant', flags: {} }, - ]]; - expect(cells_apply(cells, [{ world_updates:[{ x: 1, y: 0, from: 'empty', to: 'plant' }]}])).toEqual([[ - { type: 'empty', flags: {} }, - { type: 'plant', flags: {} }, - { type: 'plant', flags: {} }, - ]]); - - expect(cells_apply(cells, [ - { world_updates: [{ x: 2, y: 0, from: 'plant', to: 'empty' } ]}, - { world_updates: [{ x: 1, y: 0, from: 'empty', to: 'plant' } ]}, - { world_updates: [{ x: 0, y: 0, from: 'empty', to: 'plant' } ]}, - ])).toEqual([[ - { type: 'plant', flags: {} }, - { type: 'plant', flags: {} }, - { type: 'empty', flags: {} }, - ]]); -}); - - -test("check proposals agains cells for validity", () => { - const cells = [[ { type: 'empty' }, { type: 'empty' }, { type: 'plant' } ]]; - expect(cells_valid(cells, { world_updates: [{ x: -1, y: 0, from: 'blah', to: 'blah' }] })).toBe(false); - expect(cells_valid(cells, { world_updates: [{ x: 0, y: 0, from: 'blah', to: 'blah' }] })).toBe(false); - expect(cells_valid(cells, { world_updates: [{ x: 0, y: 0, from: 'empty', to: 'blah' }] })).toBe(true); - expect(cells_valid(cells, { world_updates: [{ x: 2, y: 0, from: 'empty', to: 'blah' }] })).toBe(false); - expect(cells_valid(cells, { world_updates: [{ x: 2, y: 1, from: 'empty', to: 'blah' }] })).toBe(false); -}); - - -test("proposals update cell flags appropriately", () => { - const cells = [ - [ - { type: 'empty', flags: { step: 1} }, - { type: 'empty', flags: {} }, - { type: 'plant', flags: { foo: 'bar' } }, - ] - ]; - - // flags are reset each time step - expect(cells_apply(cells, [{ world_updates:[{ x: 1, y: 0, from: 'empty', to: 'plant' }]}])).toEqual([[ - { type: 'empty', flags: {} }, - { type: 'plant', flags: {} }, - { type: 'plant', flags: {} }, - ]]); - - // flags are combined when updating - expect(cells_apply(cells, [ - { world_updates: [{ x: 1, y: 0, flags: { foo: 'bar' } } ]}, - { world_updates: [{ x: 1, y: 0, from: 'empty', to: 'plant', flags: { baz: 'baz' } } ]}, - { world_updates: [{ x: 0, y: 0, from: 'empty', to: 'plant', flags: { foo: 'foo' } } ]}, - { world_updates: [{ x: 0, y: 0, flags: { beep: 'boop' } } ]}, - ])).toEqual([[ - { type: 'plant', flags: { foo: 'foo', beep: 'boop' } }, - { type: 'plant', flags: { foo: 'bar', baz: 'baz' } }, - { type: 'plant', flags: {} }, - ]]); - -}); diff --git a/src/world/lattice.js b/src/world/lattice.js new file mode 100644 index 0000000..243a47d --- /dev/null +++ b/src/world/lattice.js @@ -0,0 +1,52 @@ +'use strict'; + +// get the proposals for cell updates +export function lattice_update(lattice, update_rules) { + return lattice + .map((row, y) => row.map((cell, x) => [x, y, cell.type])) + .flat() + .reduce((acc, [x, y, type]) => [...acc, update_rules[type](lattice, x, y)], []) + .filter(x => x !== undefined) +} + + +// check if, given the current lattice configuration, a proposal is valid +export function lattice_valid(lattice, proposal) { + if (!proposal.world_updates) { return true; } + return proposal.world_updates.reduce( + (acc, update) => { + const valid = + (update.x >= 0 && update.x < lattice[0].length) && + (update.y >= 0 && update.y < lattice.length) && + (lattice[update.y][update.x].type == update.from) + return valid && acc; + }, + true + ); +} + + +// apply a set of proposals, returning the new lattice +export function lattice_apply(lattice, proposals) { + return proposals.reduce( + (acc, prop) => { + const change = (prop.world_updates || []).reduce( + (acc_, update) => { + const cell = acc_[update.y][update.x]; + if (update.to) { cell.type = update.to; } + if (update.flags) { + cell.flags = cell.flags || {} + // this is very side-effect-y but i couldn't think of a nicer compatible way of doing it 😔 + for (let k of Object.keys(update.flags)) { + cell.flags[k] = update.flags[k]; + } + } + return acc_ + }, + [...acc] + ); + return change; + }, + [...lattice].map(row => row.map(cell => ({ ...cell, flags: {}, }))) + ); +} diff --git a/src/world/lattice.test.js b/src/world/lattice.test.js new file mode 100644 index 0000000..7c71d04 --- /dev/null +++ b/src/world/lattice.test.js @@ -0,0 +1,99 @@ +'use strict'; + +import { lattice_update, lattice_valid, lattice_apply } from './lattice.js'; + + +test("growth update rule", () => { + const lattice = [[ + { type: 'empty', flags: {} }, + { type: 'empty', flags: {} }, + { type: 'plant', flags: {} }, + ]]; + const update_rules = { + plant: () => {}, + empty: (lattice, x, y) => { + if (lattice[y][x+1].type === 'plant') { + return { world_updates: [{ x, y, from: 'empty', to: 'plant' }] }; + } + }, + }; + + expect(lattice_update(lattice, update_rules)).toEqual([ + { world_updates: [{ x: 1, y: 0, from: 'empty', to: 'plant' }] }, + ]); + + lattice[0][1] = { type: 'plant' }; + + expect(lattice_update(lattice, update_rules)).toEqual([ + { world_updates: [{ x: 0, y: 0, from: 'empty', to: 'plant' }] }, + ]); + + lattice[0][0] = { type: 'plant' }; + + expect(lattice_update(lattice, update_rules)).toEqual([]); +}); + + +test("growth update rule applied", () => { + const lattice = [[ + { type: 'empty', flags: {} }, + { type: 'empty', flags: {} }, + { type: 'plant', flags: {} }, + ]]; + expect(lattice_apply(lattice, [{ world_updates:[{ x: 1, y: 0, from: 'empty', to: 'plant' }]}])).toEqual([[ + { type: 'empty', flags: {} }, + { type: 'plant', flags: {} }, + { type: 'plant', flags: {} }, + ]]); + + expect(lattice_apply(lattice, [ + { world_updates: [{ x: 2, y: 0, from: 'plant', to: 'empty' } ]}, + { world_updates: [{ x: 1, y: 0, from: 'empty', to: 'plant' } ]}, + { world_updates: [{ x: 0, y: 0, from: 'empty', to: 'plant' } ]}, + ])).toEqual([[ + { type: 'plant', flags: {} }, + { type: 'plant', flags: {} }, + { type: 'empty', flags: {} }, + ]]); +}); + + +test("check proposals agains lattice for validity", () => { + const lattice = [[ { type: 'empty' }, { type: 'empty' }, { type: 'plant' } ]]; + expect(lattice_valid(lattice, { world_updates: [{ x: -1, y: 0, from: 'blah', to: 'blah' }] })).toBe(false); + expect(lattice_valid(lattice, { world_updates: [{ x: 0, y: 0, from: 'blah', to: 'blah' }] })).toBe(false); + expect(lattice_valid(lattice, { world_updates: [{ x: 0, y: 0, from: 'empty', to: 'blah' }] })).toBe(true); + expect(lattice_valid(lattice, { world_updates: [{ x: 2, y: 0, from: 'empty', to: 'blah' }] })).toBe(false); + expect(lattice_valid(lattice, { world_updates: [{ x: 2, y: 1, from: 'empty', to: 'blah' }] })).toBe(false); +}); + + +test("proposals update cell flags appropriately", () => { + const lattice = [ + [ + { type: 'empty', flags: { step: 1} }, + { type: 'empty', flags: {} }, + { type: 'plant', flags: { foo: 'bar' } }, + ] + ]; + + // flags are reset each time step + expect(lattice_apply(lattice, [{ world_updates:[{ x: 1, y: 0, from: 'empty', to: 'plant' }]}])).toEqual([[ + { type: 'empty', flags: {} }, + { type: 'plant', flags: {} }, + { type: 'plant', flags: {} }, + ]]); + + // flags are combined when updating + expect(lattice_apply(lattice, [ + { world_updates: [{ x: 1, y: 0, flags: { foo: 'bar' } } ]}, + { world_updates: [{ x: 1, y: 0, from: 'empty', to: 'plant', flags: { baz: 'baz' } } ]}, + { world_updates: [{ x: 0, y: 0, from: 'empty', to: 'plant', flags: { foo: 'foo' } } ]}, + { world_updates: [{ x: 0, y: 0, flags: { beep: 'boop' } } ]}, + ])).toEqual([[ + { type: 'plant', flags: { foo: 'foo', beep: 'boop' } }, + { type: 'plant', flags: { foo: 'bar', baz: 'baz' } }, + { type: 'plant', flags: {} }, + ]]); + +}); diff --git a/src/world/proposal.js b/src/world/proposal.js index 775466c..9f98fd4 100644 --- a/src/world/proposal.js +++ b/src/world/proposal.js @@ -27,16 +27,16 @@ import { pairs, deepEqual } from '../util.js'; /* proposal structure - * all proposed world and agent changes must be included for the proposed action to be valid - * similarly, if any world or agent change creates merge conflicts, the proposal cannot be merged + * all proposed lattice and agent changes must be included for the proposed action to be valid + * similarly, if any lattice or agent change creates merge conflicts, the proposal cannot be merged * and must be removed * { - * world_changes: proposal_world_change[]? + * lattice_changes: proposal_lattice_change[]? * agent_changes: proposal_agent_change[]? * } */ -/* proposal_world_change +/* proposal_lattice_change * { * x, y: number * from: string @@ -69,10 +69,10 @@ function flags_compatible(a, b) { // return a tuple [conflict, merge] -// conflict is true if the two world_changes are incompatible -// merge is true if the two world changes are identical +// conflict is true if the two lattice_changes are incompatible +// merge is true if the two lattice changes are identical // otherwise they are false -function world_change_conflict(a, b) { +function lattice_change_conflict(a, b) { if (deepEqual(a, b)) { // merge return [false, true]; @@ -145,11 +145,11 @@ function agent_change_conflict(a, b) { } -// combine world_change and agent_change conflict/merge tuples for a pair of proposals +// combine lattice_change and agent_change conflict/merge tuples for a pair of proposals function proposal_conflict_merge(a, b) { - const [world_conflict, world_merge] = pairs(a.world_changes || [], b.world_changes || []).reduce( + const [lattice_conflict, lattice_merge] = pairs(a.lattice_changes || [], b.lattice_changes || []).reduce( (acc, [a, b]) => { - const [conflict, merge] = world_change_conflict(a, b); + const [conflict, merge] = lattice_change_conflict(a, b); return [acc[0] || conflict, acc[1] || merge]; }, [false, false] @@ -161,7 +161,7 @@ function proposal_conflict_merge(a, b) { }, [false, false] ); - return [world_conflict || agent_conflict, world_merge || agent_merge]; + return [lattice_conflict || agent_conflict, lattice_merge || agent_merge]; } diff --git a/src/world/proposal.test.js b/src/world/proposal.test.js index a1e1203..2f870e6 100644 --- a/src/world/proposal.test.js +++ b/src/world/proposal.test.js @@ -7,10 +7,10 @@ import { test("proposals changing different tiles don't conflict", () => { const a = { - world_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable' }], + lattice_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable' }], }; const b = { - world_changes: [{ x: 4, y: 4, from: 'empty', to: 'flag' }], + lattice_changes: [{ x: 4, y: 4, from: 'empty', to: 'flag' }], }; expect(proposal_merge([a], b)).toEqual([a, b]); @@ -19,10 +19,10 @@ test("proposals changing different tiles don't conflict", () => { test("proposals changing the same tile to different states conflict", () => { const a = { - world_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable' }], + lattice_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable' }], }; const b = { - world_changes: [{ x: 4, y: 3, from: 'empty', to: 'flag' }], + lattice_changes: [{ x: 4, y: 3, from: 'empty', to: 'flag' }], }; expect(proposal_merge([a], b)).toEqual([]); @@ -31,10 +31,10 @@ test("proposals changing the same tile to different states conflict", () => { test("proposals changing the same tile to the same state merge to a single proposal", () => { const a = { - world_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable' }], + lattice_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable' }], }; const b = { - world_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable' }], + lattice_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable' }], }; expect(proposal_merge([a], b)).toEqual([a]); @@ -43,10 +43,10 @@ test("proposals changing the same tile to the same state merge to a single propo test("proposals with identical tile updates but incompatible tile flags conflict", () => { const a = { - world_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable', flags: { v: 'a' } }], + lattice_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable', flags: { v: 'a' } }], }; const b = { - world_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable', flags: { v: 'b' } }], + lattice_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable', flags: { v: 'b' } }], }; expect(proposal_merge([a], b)).toEqual([]); @@ -55,10 +55,10 @@ test("proposals with identical tile updates but incompatible tile flags conflict test("proposals with identical tile updates but compatible tile flags do not conflict", () => { const a = { - world_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable', flags: { v: 'a', r: 'd' } }], + lattice_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable', flags: { v: 'a', r: 'd' } }], }; const b = { - world_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable', flags: { v: 'a', u: 'b' } }], + lattice_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable', flags: { v: 'a', u: 'b' } }], }; expect(proposal_merge([a], b)).toEqual([a, b]); diff --git a/src/world/sense.js b/src/world/sense.js new file mode 100644 index 0000000..47329b7 --- /dev/null +++ b/src/world/sense.js @@ -0,0 +1,17 @@ +'use strict'; + +/* sense structure: + * { + * size: number + * read: function(lattice, agent) -> number[size] + * } + */ + + +export function sense_read(lattice, agent, sense) { + const result = sense.read(lattice, agent); + if (result.length !== sense.size) { + throw new Error(`Expected result of size ${sense.size}, but got ${result.length} instead.`); + } + return sense.read(lattice, agent); +} diff --git a/src/world/sense.test.js b/src/world/sense.test.js new file mode 100644 index 0000000..1ef7bce --- /dev/null +++ b/src/world/sense.test.js @@ -0,0 +1,33 @@ +import { sense_read } from './sense.js'; + + +test("basic sense works", () => { + const flag_sense = { + size: 1, + read: (lattice, agent) => { + const {x, y} = agent; + return [ lattice[y-1][x].type === 'flag' ? 1.0 : 0.0 ] + }, + }; + + const lattice = [[ { type: 'flag' } ]]; + const agent = { x: 0, y: 1 }; + + expect(sense_read(lattice, agent, flag_sense)).toEqual([1.0]); +}); + + +test("senses throw if the size is incorrect", () => { + const flag_sense = { + size: 2, + read: (lattice, agent) => { + const {x, y} = agent; + return [ lattice[y-1][x].type === 'flag' ? 1.0 : 0.0 ] + }, + } + + const lattice = [[ { type: 'flag' } ]]; + const agent = { x: 0, y: 1 }; + + expect(() => sense_read(lattice, agent, flag_sense)).toThrow(); +}); -- cgit v1.2.1