summaryrefslogtreecommitdiff
path: root/src/world/proposal.js
blob: 09695403d0198ae0b6fdd5ecbfbd2376e8bc8ab5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import { pairs, deepEqual } from '../util.js';

/* agent structure:
 * {
 *   id: string
 *   net: network
 *   state: network_state
 *   x, y: number
 *   flags: object
 * }
 */


/* cell structure:
 * {
 *   type: string
 *   flags: object
 * }
 */

/* action structure:
 * {
 *   name: string,
 *   propose: (agent, output) => proposal[]
 * }
 */


/* proposal structure
 * 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
 * {
 *   lattice_changes: proposal_lattice_change[]?
 *   agent_changes: proposal_agent_change[]?
 * }
 */

/* proposal_lattice_change
 * { 
 *   x, y: number
 *   from: string
 *   to:   string
 *   flags: object?
 * }
 */

/* proposal_agent_change
 * {
 *   agent_id: string
 *   x, y: number?
 *   flags: object?
 * }
 */


// check that two flags objects are compatible
// flags are considered compatible if they do not have any common keys with different values
function flags_compatible(a, b) {
  const keys = [...new Set(Object.keys(a).concat(Object.keys(b)))];
  return keys.reduce(
    (acc, key) => {
      const eq = (a[key] === undefined || b[key] === undefined) ? true : deepEqual(a[key], b[key]);
      return acc && eq;
    },
    true
  );
}


// return a tuple [conflict, merge]
// 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 lattice_change_conflict(a, b) {
  if (deepEqual(a, b)) {
    // merge
    return [false, true];
  } 
  if (
    a.x === b.x && 
    a.y === b.y &&
    (a.to != b.to || !flags_compatible(a.flags || {}, b.flags || {}))
  ) {
    // conflict!
    return [true, false];
  } else {
    // no conflict c:
    return [false, false];
  }
}


// returns true as long as x & y are both defined
function pos_exists(a) {
  if (a.x !== undefined && a.y !== undefined) {
    return true;
  } else {
    return false;
  }
}


// check the equality of two objects with (x,y) keys
function pos_equal(a, b) {
  if (a.x !== b.x) { return false; }
  if (a.y !== b.y) { return false; }
  return true;
}


// agent changes merge if they are identical
// they conflict if two agents are trying to move to the same tile, or
// if the same agent is trying to move to two different tiles, or if an agent
// is being updated to incompatible flags
//
//
function agent_change_conflict(a, b) {
  if (deepEqual(a, b)) {
    // identical: merge
    return [false, true];
  } else if (a.agent_id === b.agent_id) {
    if (
      pos_exists(a) && pos_exists(b) && !pos_equal(a, b)
    ) {
      // same agent, different positions: conflict
      return [true, false];
    } else if (!flags_compatible(a.flags, b.flags)) {
      // same agent, incompatible flags: conflict
      return [true, false];
    } else {
      // no conflict c:
      return [false, false];
    }
  } else {
    // different agents
    if (pos_exists(a) && pos_exists(b) && pos_equal(a, b)) {
      // different agents, same position: conflict
      return [true, false];
    } else {
      // no conflict c:
      return [false, false];
    }
  }
}


// combine lattice_change and agent_change conflict/merge tuples for a pair of proposals
function proposal_conflict_merge(a, b) {
  const [lattice_conflict, lattice_merge] = pairs(a.lattice_changes || [], b.lattice_changes || []).reduce(
    (acc, [a, b]) => {
      const [conflict, merge] = lattice_change_conflict(a, b);
      return [acc[0] || conflict, acc[1] || merge];
    }, 
    [false, false]
  );
  const [agent_conflict, agent_merge] = pairs(a.agent_changes || [], b.agent_changes || []).reduce(
    (acc, [a, b]) => {
      const [conflict, merge] = agent_change_conflict(a, b);
      return [acc[0] || conflict, acc[1] || merge];
    },
    [false, false]
  );
  return [lattice_conflict || agent_conflict, lattice_merge || agent_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];
  }
}