This repository has been archived by the owner on Jun 16, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
282 lines (265 loc) · 9.55 KB
/
index.js
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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
((exports) => {
/**
* Parse a string constraint into a constraint array
*
* A constraint string is a set of numbers followed by a non digit to represent
* the number of that digit, or a space to indicate an arbitrary amount of
* spaces. When followed by a space, the character after a number can be
* omitted to use the default '█'.
*
* @example
* nonogram.parseConstraint('1 3 1')
*
* @example
* nonogram.parseConstraint('2.2@2. 2@')
*
* @param {string} parse - The constraint string.
* @return {constraint}
*/
exports.parseConstraint = (parse) => {
const cs = [];
while (parse) {
const match = parse.match(/^( +|\d+[^\d +]?)/);
if (!match) {
throw new Error(`${parse} is improper format`);
} else {
let spec = match[0];
parse = parse.slice(spec.length);
if (spec.split('').every(c => c === ' ')) {
cs.push({ char: ' ', count: 1, multi: true });
} else {
const last = spec.slice(-1);
const isdig = last >= '0' && last <= '9';
const char = isdig ? '█' : last;
if (!isdig) {
spec = spec.slice(0, -1);
}
cs.push({ char, count: parseInt(spec, 10) });
}
}
}
return cs;
};
/**
* Format a constraint as a string
*
* This is the opposite of `parseConstraint` and will attempt to invert the
* relation. Due to shortcuts in constraint strings, some constraints can't be
* represented as strings, and several strings can represent the same
* constraint.
*
* @example
* nonogram.formatConstraint([
* {char: '█', count: 3},
* {char: ' ', count: 1, multi: true},
* {char: '█', count: 2}])
*
* @param {constraint} cons - The constraint array
* @return {string}
*/
exports.formatConstraint = cons => cons.map((c) => {
if (c.char === ' ') {
if (!c.multi || c.count !== 1) {
throw new Error("string formats don't allow variable single spaces");
}
return ' ';
} else {
if (c.multi) {
throw new Error("string formats don't allow variable length colors");
}
return c.count + c.char;
}
}).join('');
function constraintsHold(char, cons, remaining) {
let valHolds = false;
for (let i = cons.length - 1; i >= 0; i -= 1) {
const c = cons[i];
if (valHolds) {
remaining -= Math.max(c.count, 0);
} else if (c.char === char) {
valHolds = true;
remaining -= Math.max(c.count - 1, 0);
} else if (c.count > 0) {
return false;
}
}
return valHolds && remaining >= 0;
}
function normalizeConstraints(chars, charIndex, cons) {
const count = { ' ': [0, true] };
return [cons.map((con) => {
con = con.map((c) => {
if (!(c.char in charIndex)) {
charIndex[c.char] = chars.push(c.char) - 1;
}
const cnt = (count[c.char] || (count[c.char] = [0, false]));
cnt[0] += c.count;
cnt[1] = cnt[1] || c.multi;
return Object.assign({}, c); // Copy
});
if (!con.slice(1).every((c, i) => c.char !== con[i].char)) {
throw new Error(`constraints can't have consecutive duplicate characters: ${JSON.stringify(con)}`);
}
if (!con.length || con[0].char !== ' ') {
con.unshift({ char: ' ', count: 0, multi: true });
} else {
con[0].multi = true;
}
if (con[con.length - 1].char !== ' ') {
con.push({ char: ' ', count: 0, multi: true });
} else {
con[con.length - 1].multi = true;
}
return con;
}), count];
}
/**
* Verify that counts of a character in rows are compatible with counts of a
* character in columns and vice versa.
*/
function verifyCounts(rowCount, colCount) {
// Unmentioned keys have zero counts and are bounded
Object.keys(rowCount).forEach(k => colCount[k] || (colCount[k] = [0, false]));
Object.keys(colCount).forEach(k => rowCount[k] || (rowCount[k] = [0, false]));
const keys = Object.keys(rowCount);
if (!keys.every(k => (
rowCount[k][1] && colCount[k][1])
|| (colCount[k][1] && colCount[k][0] <= rowCount[k][0])
|| (rowCount[k][1] && rowCount[k][0] <= colCount[k][0])
|| (rowCount[k][0] === colCount[k][0]))) {
throw new Error(`rows and columns had different counts of characters. row: ${JSON.stringify(keys.reduce((o, k) => { o[k] = rowCount[k][0] + (rowCount[k][1] ? '+' : ''); return o; }, {}))}, cols: ${JSON.stringify(keys.reduce((o, k) => { o[k] = colCount[k][0] + (colCount[k][1] ? '+' : ''); return o; }, {}))}`);
}
}
/**
* Verify that the number of characters in each row (column) is less than the
* number of columns (rows).
*/
function verifyNums(constraints, num, current, other) {
constraints.forEach((cons, i) => {
if (Object.keys(cons).reduce((s, k) => s + cons[k].count, 0) > num) {
throw new Error(`${current} ${i + 1} had more characters than the number of ${other}s (${num})`);
}
});
}
/**
* Solve a nonogram
*
* @example
* const img = nonogram.solve(['1', ''].map(nonogram.parseConstraint),
* ['1', ''].map(nonogram.parseConstraint));
* console.log(img.map(l => l.join('')).join('\n'));
*
* @param {constraints} rowConsInit - An array of the row constraints.
* @param {constraints} colConsInit - An array of the column constraints.
* @return {raster}
*/
exports.solve = (rowConsInit, colConsInit) => {
const rows = rowConsInit.length;
const cols = colConsInit.length;
const size = rows * cols;
const chars = [' '];
const charIndex = { ' ': 0 };
const [rowCons, rowCounts] = normalizeConstraints(chars, charIndex, rowConsInit);
const [colCons, colCounts] = normalizeConstraints(chars, charIndex, colConsInit);
verifyNums(rowCons, colCons.length, 'row', 'column');
verifyNums(colCons, rowCons.length, 'column', 'row');
verifyCounts(rowCounts, colCounts);
// Initialize raster
const raster = Array(rows).fill(null).map(() => Array(cols).fill(null).map(() => ({ char: ' ', rc: [], cc: [] })));
let i = size - 1;
while (i >= 0) {
let r = Math.trunc(i / cols);
let c = i % cols;
let rCons = rowCons[r];
let cCons = colCons[c];
let elem = raster[r][c];
if (!elem.char && i + 1 === size) {
// No solutions
throw new Error('no solutions found');
} else if (!elem.char) {
// Finished all values, none satisfy
elem.char = ' ';
i += 1;
r = Math.trunc(i / cols);
c = i % cols;
rCons = rowCons[r];
cCons = colCons[c];
elem = raster[r][c];
// Unwind constraints for previous square
if (rCons[rCons.length - 1].char === elem.char) {
rCons[rCons.length - 1].count += 1;
} else {
elem.rc[elem.rc.length - 1].count += 1;
}
while (elem.rc.length) {
rCons.push(elem.rc.pop());
}
if (cCons[cCons.length - 1].char === elem.char) {
cCons[cCons.length - 1].count += 1;
} else {
elem.cc[elem.cc.length - 1].count += 1;
}
while (elem.cc.length) {
cCons.push(elem.cc.pop());
}
elem.char = chars[charIndex[elem.char] + 1];
} else if (constraintsHold(elem.char, cCons, r) && constraintsHold(elem.char, rCons, c)) {
// Constrains can be satisfied, remove element from constraints
while (rCons[rCons.length - 1].char !== elem.char) {
elem.rc.push(rCons.pop());
}
rCons[rCons.length - 1].count -= 1;
if (!rCons[rCons.length - 1].count && !rCons[rCons.length - 1].multi) {
elem.rc.push(rCons.pop());
}
while (cCons[cCons.length - 1].char !== elem.char) {
elem.cc.push(cCons.pop());
}
cCons[cCons.length - 1].count -= 1;
if (!cCons[cCons.length - 1].count && !cCons[cCons.length - 1].multi) {
elem.cc.push(cCons.pop());
}
i -= 1;
} else {
// Constraints not satisfied, but more more possible chars
elem.char = chars[charIndex[elem.char] + 1];
}
}
return raster.map(row => row.map(e => e.char));
};
/**
* Unsolve a nonogram
*
* Takes a raster (2d array of chars) and converts it into row and column
* constraints. Because the flexible constraints can't be inferred, this will
* produce deterministic constraints without any wiggle room with spaces for
* example. If so desired, one could easily post-process to this format.
*
* @example
* const [rows, cols] = nonogram.unsolve([['█', ' '], [' ', ' ']]);
*
* @param {raster} image - The image to decompose into constraints.
* @return {constraints} two element array of row and column constraints
*/
exports.unsolve = (image) => {
const rowCons = image.map(() => []);
const colCons = image[0].map(() => []);
image.forEach((row, r) => {
const rCon = rowCons[r];
row.forEach((char, c) => {
const cCon = colCons[c];
if (rCon.length && rCon[rCon.length - 1].char === char) {
rCon[rCon.length - 1].count += 1;
} else {
rCon.push({ char, count: 1 });
}
if (cCon.length && cCon[cCon.length - 1].char === char) {
cCon[cCon.length - 1].count += 1;
} else {
cCon.push({ char, count: 1 });
}
});
});
return [rowCons, colCons];
};
})(typeof exports === 'undefined' ? this.nonogram = {} : exports);