summaryrefslogtreecommitdiff
path: root/csp.js
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2025-05-26 15:55:29 +0200
committererdgeist <erdgeist@erdgeist.org>2025-05-26 15:55:29 +0200
commit47cb23ce1f991c21ceb9273cf4bed717a09abd9a (patch)
tree90f6e3299abf5ec632123513fc80959f3336e15e /csp.js
Kickoff commit
Diffstat (limited to 'csp.js')
-rw-r--r--csp.js181
1 files changed, 181 insertions, 0 deletions
diff --git a/csp.js b/csp.js
new file mode 100644
index 0000000..139b70d
--- /dev/null
+++ b/csp.js
@@ -0,0 +1,181 @@
1!function() {
2
3var CSP = {},
4 FAILURE = 'FAILURE',
5 stepCounter = 0;
6
7CSP.solve = function solve(csp) {
8 // Solves a constraint satisfaction problem.
9 // `csp` is an object that should have the properties:
10 // `variables` : object that holds variable names and their domain.
11 // `constraints`: list of constraints where each element is an
12 // array of [head node, tail node, constraint function]
13 // `cb`: callback function for visualizing assignments. It is passed in
14 // an "assigned" object, an "unassigned" object, and `csp`.
15 // `timeStep`: milliseconds between invocations of `cb`.
16
17 csp.timeStep = csp.timeStep || 1;
18 var result = backtrack({}, csp.variables, csp);
19 if (result == FAILURE) { return result; }
20 // Unwrap values from array containers.
21 for (var key in result) {
22 result[key] = result[key][0];
23 }
24 return result;
25}
26
27function backtrack(_assigned, unassigned, csp) {
28 // Backtracking search.
29
30 // Copying assigned in necessary because we modify it. Without copying
31 // the object over, modifying assigned would also change values for old
32 // assigned objects (which are used in callbacks).
33 var assigned = {};
34 for (var key in _assigned) { assigned[key] = _assigned[key]; }
35
36 if (finished(unassigned)) { return assigned; } // Base case.
37 var nextKey = selectUnassignedVariable(unassigned),
38 values = orderValues(nextKey, assigned, unassigned, csp);
39 delete unassigned[nextKey];
40
41 for (var i = 0; i < values.length; i++) {
42 stepCounter++;
43 assigned[nextKey] = [values[i]]; // Assign a value to a variable.
44 var consistent = enforceConsistency(assigned, unassigned, csp);
45 var newUnassigned = {}, newAssigned = {};
46 for (var key in consistent) {
47 if (assigned[key]) { newAssigned[key] = assigned[key].slice(); }
48 else { newUnassigned[key] = consistent[key].slice(); }
49 }
50 if (csp.cb) {
51 setTimeout(
52 // Need a closure to fix values of newAssigned and newUnassigned.
53 // Otherwise, _every_ call of the callback takes the on values of the last iteration.
54 (function (newAssigned, newUnassigned) {
55 return function () { csp.cb(newAssigned, newUnassigned, csp); };
56 })(newAssigned, newUnassigned),
57 stepCounter * csp.timeStep
58 );
59 }
60 if (anyEmpty(consistent)) { continue; } // Empty domains means failure.
61 var result = backtrack(newAssigned, newUnassigned, csp);
62 if (result != FAILURE) { return result; }
63 }
64
65 return FAILURE;
66}
67
68function finished(unassigned) {
69 // Checks if there are no more variables to assign.
70 return Object.keys(unassigned).length == 0;
71}
72
73function anyEmpty(consistent) {
74 // Checks if any variable's domain is empty.
75 for (var key in consistent) {
76 if (consistent[key].length == 0) { return true; }
77 }
78 return false;
79}
80
81function partialAssignment(assigned, unassigned) {
82 // Combine unassigned and assigned for use in enforceConsistency.
83 var partial = {};
84 for (var key in unassigned) { partial[key] = unassigned[key].slice(); }
85 for (var key in assigned) { partial[key] = assigned[key].slice(); }
86 return partial;
87}
88
89function enforceConsistency(assigned, unassigned, csp) {
90 // Enforces arc consistency by removing inconsistent values from
91 // every constraint's tail node.
92
93 function removeInconsistentValues(head, tail, constraint, variables) {
94 // Removes inconsistent values from the tail node. A value is
95 // inconsistent when if the `tail` is assigned that value, there are
96 // no values in `head`'s domain that satisfies the constraint.
97 var hv = variables[head], tv = variables[tail];
98 var validTailValues = tv.filter(function (t) {
99 return hv.some(function (h) {
100 return constraint(h, t);
101 });
102 });
103 var removed = tv.length != validTailValues.length;
104 variables[tail] = validTailValues;
105 return removed;
106 }
107
108 function incomingConstraints(node) {
109 // Returns all the constraints where `node` is the head node.
110 return csp.constraints.filter(function (c) {
111 return c[0] == node;
112 });
113 }
114
115 var queue = csp.constraints.slice(),
116 variables = partialAssignment(assigned, unassigned);
117 while (queue.length) { // While there are more constraints to test.
118 var c = queue.shift(), head = c[0], tail = c[1], constraint = c[2];
119 if (removeInconsistentValues(head, tail, constraint, variables)) {
120 // If values from the tail have been removed, incoming constraints
121 // to the tail must be rechecked.
122 queue = queue.concat(incomingConstraints(tail));
123 }
124 }
125 return variables;
126}
127
128function selectUnassignedVariable(unassigned) {
129 // Picks the next variable to assign according to the Minimum
130 // Remaining Values heuristic. Pick the variable with the fewest
131 // values remaining in its domain. This helps identify domain
132 // failures earlier.
133 var minKey = null, minLen = Number.POSITIVE_INFINITY;
134 for (var key in unassigned) {
135 var len = unassigned[key].length;
136 if (len < minLen) { minKey = key, minLen = len; }
137 }
138 return minKey;
139}
140
141function orderValues(nextKey, assigned, unassigned, csp) {
142 // Orders the values of an unassigned variable according to the
143 // Least Constraining Values heuristic. Perform arc consistency
144 // on each possible value, and order variables according to the
145 // how many values were eliminated from all the domains (fewest
146 // eliminated in the front). This helps makes success more likely
147 // by keeping future options open.
148
149 function countValues(vars) {
150 var sum = 0;
151 for (var key in vars) { sum += vars[key].length; }
152 return sum;
153 }
154
155 function valuesEliminated(val) {
156 assigned[nextKey] = [val];
157 var newLength = countValues(enforceConsistency(assigned, unassigned, csp));
158 delete assigned[nextKey];
159 return newLength;
160 }
161
162 // Cache valuesEliminated to be used in sort.
163 var cache = {}, values = unassigned[nextKey];
164 values.forEach(function(val) {
165 cache[val] = valuesEliminated(val);
166 });
167 // Descending order based on the number of domain values remaining.
168 values.sort(function (a, b) { return cache[b] - cache[a]; });
169 return values;
170}
171
172// Taken from d3 source. Makes `csp` usable in other scripts.
173if (typeof define === 'function' && define.amd) {
174 define(CSP);
175} else if (typeof module === 'object' && module.exports) {
176 module.exports = CSP;
177} else {
178 this.csp = CSP;
179}
180
181}();