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.
- package/LISCENSE +7 -0
- package/ReadMe.md +95 -0
- package/docs/Example Benchmark.png +0 -0
- package/docs/Example Usage.png +0 -0
- package/main.js +84 -0
- package/package.json +28 -0
- package/run/benchmark.bash +4 -0
- package/run/install.bash +61 -0
- package/run/solutions.txt +2315 -0
- package/run/wordle.txt +12972 -0
- package/src/lib/entropy.js +123 -0
- package/src/lib/heuristic.js +104 -0
- package/src/lib/lib.js +65 -0
- package/src/lib/wordle.js +250 -0
- package/src/pre/feedback_matrix.js +35 -0
- package/src/pre/filter_words.js +36 -0
- package/src/pre/test.js +26 -0
- package/src/solvers/combined.js +136 -0
|
@@ -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
|
+
}
|
package/src/pre/test.js
ADDED
|
@@ -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
|
+
}
|