summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/util.js7
-rw-r--r--src/world/agent.js83
-rw-r--r--src/world/agent.test.js44
3 files changed, 134 insertions, 0 deletions
diff --git a/src/util.js b/src/util.js
index 4e23d9d..8b85ba7 100644
--- a/src/util.js
+++ b/src/util.js
@@ -14,3 +14,10 @@ export function random_choice(collection, r) {
const idx = Math.floor(collection.length * r);
return collection[idx];
}
+
+
+export function pairs(arr1, arr2) {
+ return arr1
+ .map((x, i) => arr2.map(y => [x, y]))
+ .flat();
+}
diff --git a/src/world/agent.js b/src/world/agent.js
new file mode 100644
index 0000000..7ab501c
--- /dev/null
+++ b/src/world/agent.js
@@ -0,0 +1,83 @@
+import { pairs } from '../util.js';
+
+/* agent structure:
+ * {
+ * id: string
+ * net: network
+ * state: network_state
+ * x, y: number
+ * flags: object
+ * }
+ */
+
+/* action structure:
+ * {
+ * name: string,
+ * propose: (agent) => proposal[]
+ * }
+ */
+
+
+/* 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
+ * and must be removed
+ * {
+ * world_changes: proposal_world_change[]?
+ * agent_changes: proposal_agent_change[]?
+ * }
+ */
+
+/* proposal_world_change
+ * {
+ * x, y: number
+ * from: string
+ * to: string
+ * }
+ */
+
+/* proposal_agent_change
+ * {
+ * agent_id: string
+ * x, y: number?
+ * flags: object?
+ * }
+ */
+
+
+function world_change_conflict(a, b) {
+ if (a.x != b.x) { return [false, false]; }
+ if (a.y != b.y) { return [false, false]; }
+ if (a.to != b.to) { return [true, false]; }
+ // x, y, and to all match -- merge
+ return [false, true];
+}
+
+
+function proposal_conflict_merge(a, b) {
+ const [world_conflict, world_merge] = pairs(a.world_changes, b.world_changes).reduce(
+ (acc, [a, b]) => {
+ const [conflict, merge] = world_change_conflict(a, b);
+ return [acc[0] || conflict, acc[1] || merge];
+ },
+ [false, false]
+ )
+ return [world_conflict, world_merge];
+}
+
+
+// merge proposals
+// if two sub-updates conflict, they are both omitted from the final merged proposal
+export function proposal_merge(arr, proposal) {
+ const conflict_merge = arr.map(x => proposal_conflict_merge(x, proposal));
+
+ // if any conflicts are detected then don't merge
+ if (conflict_merge.reduce((acc, [c, m]) => acc || c, false)) {
+ const conflict_free = arr.filter((x, i) => !conflict_merge[i][0]);
+ return conflict_free;
+ } else {
+ // no conflicts, but need to merge identical actions
+ const no_merge = arr.filter((x, i) => !conflict_merge[i][1]);
+ return [...no_merge, proposal];
+ }
+}
diff --git a/src/world/agent.test.js b/src/world/agent.test.js
new file mode 100644
index 0000000..ea64fcb
--- /dev/null
+++ b/src/world/agent.test.js
@@ -0,0 +1,44 @@
+import {
+ proposal_merge,
+} from './agent.js';
+
+
+// --===== proposal conflicts =====--
+
+test("proposals changing different tiles don't conflict", () => {
+ const a = {
+ world_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable' }],
+ };
+
+ const b = {
+ world_changes: [{ x: 4, y: 4, from: 'empty', to: 'flag' }],
+ };
+
+ expect(proposal_merge([a], b)).toEqual([a, b]);
+});
+
+
+test("proposals changing the same tile to different states conflict", () => {
+ const a = {
+ world_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable' }],
+ };
+
+ const b = {
+ world_changes: [{ x: 4, y: 3, from: 'empty', to: 'flag' }],
+ };
+
+ expect(proposal_merge([a], b)).toEqual([]);
+});
+
+
+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' }],
+ };
+
+ const b = {
+ world_changes: [{ x: 4, y: 3, from: 'empty', to: 'mutable' }],
+ };
+
+ expect(proposal_merge([a], b)).toEqual([a]);
+});