wordulator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,123 @@
1
+ const buckets = new Uint16Array(243).fill(0);
2
+ /**
3
+ * @param {number} guess_index
4
+ * @param {Array<string>} word_list
5
+ * @param {UInt8Array} feedback_matrix
6
+ * @param {number[]} answer_indecies
7
+ * @param {Float64Array} ent_table
8
+ * @returns Number
9
+ */
10
+ function calculateGuessEntropy(guess_index, word_list, feedback_matrix, answer_indecies, ent_table) {
11
+ buckets.fill(0);
12
+ const wl = word_list.length;
13
+ const base = guess_index*wl;
14
+
15
+ for (let ai=0; ai < wl; ai++) {
16
+ const pattern = feedback_matrix[base + answer_indecies[ai]];
17
+ buckets[pattern] += 1;
18
+ }
19
+
20
+ let entropy = 0;
21
+ if (ent_table !== undefined) {
22
+ for (let i = 0; i < 243; i++) {
23
+ const count = buckets[i];
24
+ entropy += ent_table[count];
25
+ }
26
+ } else {
27
+ for (let i = 0; i < 243; i++) {
28
+ const count = buckets[i];
29
+ if (count === 0) continue;
30
+ const p = count / wl;
31
+ entropy -= p * Math.log2(p);
32
+ }
33
+ }
34
+
35
+ return entropy;
36
+ }
37
+
38
+ /**
39
+ * @param {number} pattern
40
+ * @returns
41
+ */
42
+ function encodePattern(pattern) {
43
+ let value = 0
44
+ for (let i = 0; i < 5; i++) {
45
+ value = value * 3 + (pattern.charCodeAt(i) - 48)
46
+ }
47
+ return value
48
+ }
49
+
50
+ /**
51
+ * @param {string} guess
52
+ * @param {string} answer
53
+ * @returns
54
+ */
55
+ function entropyFeedback(guess, answer) {
56
+ let used = 0
57
+ let result = 0
58
+
59
+ // greens
60
+ for (let i = 0; i < 5; i++) {
61
+ if (guess[i] === answer[i]) {
62
+ result += 2 * Math.pow(3, 4 - i)
63
+ used |= 1 << i
64
+ }
65
+ }
66
+
67
+ // yellows
68
+ for (let i = 0; i < 5; i++) {
69
+ if ((result / Math.pow(3, 4 - i) | 0) % 3 !== 0) continue
70
+
71
+ for (let j = 0; j < 5; j++) {
72
+ if (!(used & (1 << j)) && guess[i] === answer[j]) {
73
+ result += 1 * Math.pow(3, 4 - i)
74
+ used |= 1 << j
75
+ break
76
+ }
77
+ }
78
+ }
79
+
80
+ return result
81
+ }
82
+
83
+ /**
84
+ *
85
+ * @param {number} length
86
+ * @returns {Float64Array}
87
+ */
88
+ function genEntropyTable(length) {
89
+ const table = new Float64Array(length + 1);
90
+
91
+ for (let c = 1; c <= length; c++) {
92
+ const p = c / length;
93
+ table[c] = -p * Math.log2(p);
94
+ }
95
+
96
+ return table;
97
+ }
98
+
99
+
100
+ const mmap = require('@luminati-io/mmap-io');
101
+ const fs = require('fs');
102
+
103
+ /**
104
+ * Lazily maps the feedback matrix file and returns a Uint8Array view
105
+ */
106
+ function loadFeedbackMatrix(path) {
107
+ const fd = fs.openSync(path, 'r');
108
+ const stats = fs.fstatSync(fd);
109
+
110
+ const buffer = mmap.map(
111
+ stats.size,
112
+ mmap.PROT_READ,
113
+ mmap.MAP_SHARED,
114
+ fd,
115
+ 0
116
+ );
117
+
118
+ // Uint8Array view over the memory-mapped file
119
+ const feedbackMatrix = new Uint8Array(buffer.buffer, buffer.byteOffset, stats.size / 2);
120
+ return feedbackMatrix;
121
+ }
122
+
123
+ module.exports = { calculateGuessEntropy, encodePattern, entropyFeedback, genEntropyTable, loadFeedbackMatrix };
@@ -0,0 +1,104 @@
1
+ const { normalise } = require('./lib.js')
2
+
3
+ /**
4
+ * Calculates the positional frequencies of letters based on a list of words
5
+ * @param {Array<string>} words
6
+ * @returns
7
+ */
8
+ function calculatePosFreq(words) {
9
+ const pf = Array.from({length: 5}, () => new Array(26).fill(0));
10
+
11
+ for (let i=0; i < words.length; i++) {
12
+ const word = words[i];
13
+ for (let j=0; j < 5; j++) {
14
+ const char_code = word.charCodeAt(j) - 97;
15
+ pf[j][char_code] += 1;
16
+ }
17
+ }
18
+
19
+ for (let i=0; i < 5; i++) {
20
+ pf[i] = normalise(pf[i]);
21
+ }
22
+
23
+ return pf;
24
+ }
25
+
26
+ /**
27
+ * Scores a word based on positional frequencies of letters
28
+ * @param {string} word
29
+ * @param {Array<Array<number>>} pf
30
+ * @returns {number}
31
+ */
32
+ function pfHeuristicScore(word, pf) {
33
+ let score = 0;
34
+ const seen = new Array(26);
35
+ const wl = word.length;
36
+
37
+ for (let i=0; i < wl; i++) {
38
+ let char_code = word.charCodeAt(i) - 97;
39
+ if (seen[char_code] === 0) {
40
+ score += pf[i][char_code];
41
+ seen[char_code] = 1;
42
+ } else {
43
+ const s = pf[i][char_code];
44
+ score += s / 2;
45
+ }
46
+ }
47
+
48
+ return score;
49
+ }
50
+
51
+ /**
52
+ * Scores a word between 1-word.length based on how many unique letters it contains
53
+ * @param {string} word
54
+ * @returns {number}
55
+ */
56
+ function uniquenessHeuristicScore(word) {
57
+ let score = 0;
58
+ const seen = new Set();
59
+ for (let i=0; i<word.length; i++) {
60
+ let c = word[i];
61
+ if (!seen.has(c)) {
62
+ score += 1;
63
+ }
64
+ }
65
+ return score;
66
+ }
67
+
68
+ /**
69
+ *
70
+ * @param {string} guess
71
+ * @param {Array<boolean>} usedLetters
72
+ * @return {number}
73
+ */
74
+ function overlapScore(guess, usedLetters) {
75
+ let score = 0;
76
+ for (let l=0; l<guess.length; l++) {
77
+ const letter_idx = guess.charCodeAt(l) - 97;
78
+ if (usedLetters[letter_idx] === false) {
79
+ score++;
80
+ }
81
+ }
82
+
83
+ return score
84
+ }
85
+
86
+ /**
87
+ *
88
+ * @param {string} guess
89
+ * @param {Array<boolean>} [usedLetters]
90
+ * @return {string}
91
+ */
92
+ function createOverlap(guess, usedLetters) {
93
+ let uL = usedLetters;
94
+ if (usedLetters === undefined) uL = Array.from({ length: 26 }).fill(false);
95
+
96
+ for (let l=0; l<guess.length; l++) {
97
+ const letter_idx = guess.charCodeAt(l) - 97;
98
+ uL[letter_idx] = true;
99
+ }
100
+
101
+ return uL
102
+ }
103
+
104
+ module.exports = { calculatePosFreq, pfHeuristicScore, uniquenessHeuristicScore, overlapScore, createOverlap }
package/src/lib/lib.js ADDED
@@ -0,0 +1,65 @@
1
+ const readline = require('node:readline/promises');
2
+
3
+ const rl = readline.createInterface({
4
+ input: process.stdin,
5
+ output: process.stdout,
6
+ });
7
+
8
+ async function ask(q) {
9
+ let a = await rl.question(q);
10
+ return a.toLowerCase();
11
+ }
12
+
13
+ /**
14
+ * Counts the number of a given character in a word
15
+ * @param {*} word
16
+ * @param {*} char
17
+ * @returns
18
+ */
19
+ function count(word, character) {
20
+ let count = 0;
21
+ for (let letter of word) {
22
+ if (letter === character) { count++ }
23
+ }
24
+
25
+ return count
26
+ }
27
+
28
+ /**
29
+ * Normalises an array of data
30
+ * @param {number[]} arr
31
+ * @returns {number[]}
32
+ */
33
+ function normalise(arr) {
34
+ const max = Math.max(...arr);
35
+ const min = Math.min(...arr);
36
+ const range = Math.abs(max - min);
37
+
38
+ if (range === 0) return arr.map(() => 0);
39
+ return arr.map(v => { return (v - min) / range })
40
+ }
41
+
42
+ function patternToIndex(pattern) {
43
+ let index = 0;
44
+ let multiplier = 1;
45
+
46
+ for (let i = 0; i < 5; i++) {
47
+ const twoBits = (pattern >> (i * 2)) & 0b11;
48
+ index += twoBits * multiplier;
49
+ multiplier *= 3;
50
+ }
51
+
52
+ return index;
53
+ }
54
+
55
+ function randomInt(max) {
56
+ return Math.floor(Math.random() * max);
57
+ }
58
+
59
+ function randomUniform(min, max) {
60
+ return min + Math.random() * (max - min);
61
+ }
62
+
63
+
64
+
65
+ module.exports = { ask, count, normalise, patternToIndex, randomInt, randomUniform }
@@ -0,0 +1,250 @@
1
+ const GREEN = 2;
2
+ const YELLOW = 1;
3
+ const GREY = 0;
4
+
5
+ const NOT_FIXED = -1;
6
+
7
+ const BANNED = 1;
8
+
9
+ class Wordle {
10
+ /**
11
+ * @param {boolean} known
12
+ * @param {string} answer
13
+ */
14
+ constructor(known, answer) {
15
+ // Conditions
16
+ /** @type {Int8Array} */
17
+ this.min_counts = new Int8Array(26);
18
+ /** @type {Int8Array} */
19
+ this.max_counts = new Int8Array(26).fill(5);
20
+ /**
21
+ * character codes of fixed positions if known | -1 stored if no letter yet fixed
22
+ * @type {Int8Array}
23
+ */
24
+ this.fixed_positions = new Int8Array(5).fill(-1);
25
+ /**
26
+ *
27
+ * @type {Int8Array}
28
+ */
29
+ this.banned_positions = new Int8Array(26 * 5); // 0 not banned, 1 banned
30
+
31
+ this.known = known;
32
+
33
+ // Answer, used for automated feedback
34
+ if (known) {
35
+ this.answer = answer;
36
+ this.answer_letter_counts = new Int8Array(26);
37
+ for (let i = 0; i < 5; i++) {
38
+ const char_code = answer.charCodeAt(i) - 97;
39
+ this.answer_letter_counts[char_code] += 1;
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Provides Feedback in the form a 16bit integer:
46
+ * T A R E S
47
+ * 2 1 0 0 0
48
+ * 00_00_00_10_01_00_00_00
49
+ * @param {string} guess
50
+ * @param {string} answer
51
+ * @returns {number}
52
+ */
53
+ evaluateGuess(guess) {
54
+ if (!this.known) MELTDOWN("Can't evaluate guess with unkown answer");
55
+
56
+ const guess_counts = new Int8Array(26);
57
+ let pattern = 0;
58
+
59
+ // Greens
60
+ for (let i = 0; i < 5; i++) {
61
+ const guess_letter = guess.charCodeAt(i) - 97;
62
+ const answer_letter = this.answer.charCodeAt(i) - 97;
63
+ if (guess_letter === answer_letter) {
64
+ pattern = writePatternPosition(pattern, i, GREEN)
65
+ guess_counts[guess_letter] += 1;
66
+ }
67
+ }
68
+
69
+ // Yellows
70
+ for (let i = 0; i < 5; i++) {
71
+ const guess_letter = guess.charCodeAt(i) - 97;
72
+ const answer_letter = this.answer.charCodeAt(i) - 97;
73
+
74
+ if (guess_letter === answer_letter) continue;
75
+ else if (guess_counts[guess_letter] < this.answer_letter_counts[guess_letter]) {
76
+ pattern = writePatternPosition(pattern, i, YELLOW);
77
+ guess_counts[guess_letter] += 1;
78
+ }
79
+ }
80
+
81
+ // Greys are encoded by default. No need to reencode
82
+
83
+ return pattern;
84
+ }
85
+
86
+ /**
87
+ * @param {string} guess
88
+ * @param {number} pattern
89
+ */
90
+ updateConditions(guess, pattern) {
91
+ const min_counts = new Int8Array(26);
92
+ const max_counts = new Int8Array(26).fill(5);
93
+
94
+ // Set Min Using Green & Yellow Input
95
+ for (let word_pos = 0; word_pos < 5; word_pos++) {
96
+ const letter = guess.charCodeAt(word_pos) - 97;
97
+ const letter_colour = readPatternPosition(pattern, word_pos);
98
+
99
+ // Update min counts
100
+ if (letter_colour === YELLOW || letter_colour === GREEN) {
101
+ min_counts[letter] += 1;
102
+ }
103
+
104
+ // set fixed and banned positions
105
+ if (letter_colour === GREEN) {
106
+ if (this.hasFixedCharacter(word_pos)) {
107
+ if (this.fixed_positions[word_pos] !== letter) MELTDOWN("Can't fix 2 letters in 1 position");
108
+ } else {
109
+ this.fixPosition(letter, word_pos);
110
+ }
111
+ } else if (letter_colour === YELLOW) {
112
+ if (this.fixed_positions[word_pos] === letter) {
113
+ MELTDOWN("Can't fix and ban a letter at same position");
114
+ }
115
+ this.banPosition(letter, word_pos);
116
+ }
117
+ }
118
+
119
+ // Set Max Using Grey Input
120
+ for (let word_pos = 0; word_pos < 5; word_pos++) {
121
+ const letter = guess.charCodeAt(word_pos) - 97;
122
+ const letter_colour = readPatternPosition(pattern, word_pos);
123
+ if (letter_colour === GREY) max_counts[letter] = min_counts[letter];
124
+ }
125
+
126
+ for (let letter = 0; letter < 26; letter++) {
127
+ if (min_counts[letter] > max_counts[letter]) MELTDOWN("Can't have min count greater than max count for letter");
128
+ this.min_counts[letter] = Math.max(this.min_counts[letter], min_counts[letter]);
129
+ this.max_counts[letter] = Math.min(this.max_counts[letter], max_counts[letter])
130
+ if (this.min_counts[letter] > this.max_counts[letter]) MELTDOWN("Can't have min count greater than max count for letter");
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Checks if a word meets conditions with the min, max, fixed, and banned position of characters
136
+ * @param {string} word
137
+ * @returns {boolean}
138
+ */
139
+ meetsConditions(word) {
140
+ const letter_counts = new Int8Array(26);
141
+
142
+ for (let i = 0; i < 5; i++) {
143
+ const letter = word.charCodeAt(i) - 97;
144
+ if (this.isBanned(letter, i)) return false;
145
+ letter_counts[letter] += 1;
146
+ }
147
+
148
+ for (let i = 0; i < 26; i++) {
149
+ if (letter_counts[i] > this.max_counts[i]) return false;
150
+ if (letter_counts[i] < this.min_counts[i]) return false;
151
+ }
152
+
153
+ for (let i = 0; i < 5; i++) {
154
+ const letter = word.charCodeAt(i) - 97;
155
+ if (this.hasFixedCharacter(i) && this.fixed_positions[i] !== letter) return false;
156
+ }
157
+
158
+ return true;
159
+ }
160
+
161
+ /**
162
+ * Checks if a character code is banned from a position
163
+ * @param {Number} char_code
164
+ * @param {Number} word_pos
165
+ */
166
+ isBanned(char_code, word_pos) {
167
+ return this.banned_positions[char_code * 5 + word_pos] === BANNED;
168
+ }
169
+
170
+ /**
171
+ * Bans a character code from appearing at a certain word position
172
+ * @param {Number} char_code
173
+ * @param {Number} word_pos
174
+ */
175
+ banPosition(char_code, word_pos) {
176
+ this.banned_positions[char_code * 5 + word_pos] = BANNED;
177
+ }
178
+
179
+ /**
180
+ * Checks if the word position has a fixed character code
181
+ * @param {Number} word_pos
182
+ * @returns
183
+ */
184
+ hasFixedCharacter(word_pos) {
185
+ return this.fixed_positions[word_pos] !== NOT_FIXED;
186
+ }
187
+
188
+ /**
189
+ * Fixes a character code to a word position
190
+ * @param {Number} char_code
191
+ * @param {Number} word_pos
192
+ */
193
+ fixPosition(char_code, word_pos) {
194
+ this.fixed_positions[word_pos] = char_code;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Reads the two bits that each position i that encode green, yellow, or grey
200
+ * @param {number} pattern
201
+ * @param {number} word_pos
202
+ */
203
+ function readPatternPosition(pattern, word_pos) {
204
+ return (pattern >> (word_pos * 2)) & 0b11;
205
+ }
206
+
207
+ /**
208
+ * Writes the two bits that in position i that encode green, yellow, or grey
209
+ * @param {number} pattern
210
+ * @param {number} word_pos
211
+ * @param {number} data
212
+ * @return {number}
213
+ */
214
+ function writePatternPosition(pattern, word_pos, data) {
215
+ return pattern |= data << (word_pos * 2);
216
+ }
217
+
218
+
219
+ /**
220
+ * Certainly one of the ways ever of debugging
221
+ * ! WARNING ! LITERRALY SHUTS THE PROGRAM DOWN AFTER SENDING ERROR MESSAGE
222
+ * @param {string} error
223
+ */
224
+ function MELTDOWN(error) {
225
+ console.error(error);
226
+ process.exit(1);
227
+ }
228
+
229
+ /**
230
+ * Returns an encoded pattern based on user input strings
231
+ * @param {string} green
232
+ * @param {string} yellow
233
+ * @return {number}
234
+ */
235
+ function patternFromUserInput(green, yellow) {
236
+ if (green.length < 5) green = green.padEnd(5, '.')
237
+ if (yellow.length < 5) yellow = yellow.padEnd(5, '.')
238
+
239
+ let pattern = 0;
240
+
241
+ for (let i = 0; i < 5; i++) {
242
+ if (green[i] !== '.') pattern = writePatternPosition(pattern, i, GREEN)
243
+ if (yellow[i] !== '.') pattern = writePatternPosition(pattern, i, YELLOW)
244
+ // Greys are encoded by default. No need to reencode
245
+ }
246
+
247
+ return pattern;
248
+ }
249
+
250
+ module.exports = { Wordle, patternFromUserInput }
@@ -0,0 +1,35 @@
1
+ /** Precomputes a frequency matrix for a given set of words */
2
+ const fs = require('fs');
3
+
4
+ const { entropyFeedback } = require('../lib/entropy.js');
5
+
6
+ try {
7
+ const args = process.argv.slice(2);
8
+ const words_file = fs.existsSync(args[0]) ? args[0] : () => { throw "Words File Not Found" };
9
+ const write_file = args[1];
10
+
11
+ const words = JSON.parse(fs.readFileSync(words_file));
12
+ const matrix = new Uint8Array(words.length**2);
13
+ const wl = words.length;
14
+
15
+ for (let gi=0; gi < wl; gi++) {
16
+ const guess = words[gi];
17
+
18
+ for (let ai = 0; ai < wl; ai++) {
19
+ const answer = words[ai];
20
+ const pattern = entropyFeedback(guess, answer);
21
+ matrix[gi * wl + ai] = pattern;
22
+ }
23
+
24
+ if (gi % 1000 === 0) {
25
+ console.log(`Computed ${gi}/${words.length}`);
26
+ }
27
+ }
28
+
29
+ fs.writeFileSync(`${write_file}`, matrix);
30
+ console.log(`Feedback matrix written to ${write_file}`);
31
+ process.exit(0);
32
+ } catch (err) {
33
+ console.error(err);
34
+ }
35
+
@@ -0,0 +1,36 @@
1
+ /** Filters words down to a valid list based on a given length and preset character set */
2
+ const fs = require('node:fs');
3
+
4
+ try {
5
+ const args = process.argv.slice(2);
6
+ const read_file = fs.existsSync(args[0]) ? args[0] : () => { throw "Read File Not Found" };
7
+ const write_file = args[1];
8
+ const desired_word_length = typeof(args[2]) === Number ? args[2] : 5;
9
+
10
+ const data = fs.readFileSync(read_file, 'utf8');
11
+ const words = data.split('\n');
12
+
13
+ const valid_words = new Array(0);
14
+ const valid_chars = new Array(25);
15
+ for (let i = 97; i <= 122; i++) {
16
+ valid_chars.push(String.fromCharCode(i));
17
+ }
18
+
19
+ for (let word of words) {
20
+ word = word.toLowerCase();
21
+ let word_is_valid = (word.length === desired_word_length);
22
+
23
+ for (const letter of word) {
24
+ if (!word_is_valid) break;
25
+ word_is_valid = word_is_valid && valid_chars.includes(letter);
26
+ }
27
+
28
+ if (word_is_valid) valid_words.push(word);
29
+ }
30
+
31
+ fs.writeFileSync(`${write_file}`, JSON.stringify(valid_words, null, 4));
32
+ console.log(`Wrote ${valid_words.length} Valid Words to ${write_file}`);
33
+ process.exit(0);
34
+ } catch (err) {
35
+ console.error(err);
36
+ }
@@ -0,0 +1,26 @@
1
+ /** Precomuputes Frequencies of Letters in Words */
2
+ const fs = require('node:fs');
3
+
4
+ const { randomInt } = require('../lib/lib.js')
5
+
6
+
7
+ try {
8
+ const args = process.argv.slice(2);
9
+ const words_file = fs.existsSync(args[0]) ? args[0] : () => { throw "Words File Not Found" };
10
+ const write_file = args[1];
11
+ const test_num = args[2] > 0 ? Number.parseInt(args[2]) : () => { throw "Number of Tests Not Found/Specified" };
12
+
13
+ const words = JSON.parse(fs.readFileSync(words_file));
14
+ const wl = words.length;
15
+ const test_words = new Array(test_num);
16
+
17
+ for (let i=0; i<test_num; i++) {
18
+ test_words[i] = words[randomInt(wl)]
19
+ }
20
+
21
+ fs.writeFileSync(`${write_file}`, JSON.stringify(test_words, null, 4));
22
+ console.log(`Tests Written to ${write_file}`);
23
+ process.exit(0);
24
+ } catch (err) {
25
+ console.error(err);
26
+ }