Add a WASM version of the wordle solver; it seems to be fairly faster

This commit is contained in:
Kelvin Ly 2023-05-13 03:53:02 -04:00
parent 1e31f023bd
commit d7c2c6249a
4 changed files with 361 additions and 47 deletions

296
wordle-wasm.htm Normal file
View File

@ -0,0 +1,296 @@
<!doctype html>
<html>
<head>
<style>
.letter {
padding: 0.1em;
}
.match {
background: lightgreen;
}
.exists {
background: yellow;
}
.notincluded {
color: grey;
}
.log {
padding: 1em;
border: solid;
}
</style>
<script type="text/javascript" src="wordle_shim.js"></script>
<script type="text/javascript">
// converts the base 3 number into something that's readable
function dump_match(v) {
var ret = []
for (var i = 0; i < 5; i++) {
ret.push(v % 3)
v = Math.floor(v/3)
}
return ret
}
function undump_match(l) {
var mul = 1
var ret = 0
for (var i = 0; i < 5; i++) {
ret += l[i]*mul
mul *= 3
}
return ret
}
function status(s) {
document.getElementById('status').innerText = s
}
function log(s) {
const elem = document.createElement('p')
elem.innerText = s
document.getElementById('log').appendChild(elem)
}
var valid_words = null;
(() => {
var req = new XMLHttpRequest()
req.open('GET', 'valid-wordle-words.txt')
req.addEventListener('load', e => {
const text = req.responseText
init_wasm_solver(text)
status("words loaded")
})
req.send()
})()
var solver = null
const cur_status = [0, 0, 0, 0, 0]
var cur_guess = null
var num_guess = 0
var chosen_words = [];
(() => {
window.addEventListener('load', () => {
document.getElementById('init').addEventListener('click', e => {
if (solver == null) {
solver = init_solver()
var num_loops = 100
const precalc_start = Date.now()
const precompute_loop = () => {
if (solver.precalc_done()) return
const start = Date.now()
solver.precalc(num_loops)
const end = Date.now()
const millis = end - start
num_loops = Math.floor(num_loops - 0.5*(millis - 50))
num_loops = Math.max(num_loops, 1)
status("computing word " + solver.precalc_idx() + "/" + word_count + " " + solver.lookup_word(solver.precalc_idx()))
if (!solver.precalc_done()) {
setTimeout(precompute_loop, 0)
} else {
const precalc_end = Date.now()
log("took " + (precalc_end - precalc_start) + " ms")
console.log("took " + (precalc_end - precalc_start) + " ms")
}
}
precompute_loop(solver)
}
})
document.getElementById('reset').addEventListener('click', e => {
cur_guess = null
num_guess = 0
chosen_words.length = 0
if (solver != null) {
solver.reset()
}
const history_elem = document.getElementById('history')
while (history_elem.firstChild) {
history_elem.removeChild(history_elem.lastChild)
}
const cur_word_elem = document.getElementById('cur-word')
while (cur_word_elem.firstChild) {
cur_word_elem.removeChild(cur_word_elem.lastChild)
}
const log_elem = document.getElementById('log')
while (log_elem.firstChild) {
log_elem.removeChild(log_elem.lastChild)
}
})
document.getElementById('find').addEventListener('click', e => {
if (solver == null) {
status("please initialize the solver first")
return
}
if (solver.words_left() == 1) {
const word = solver.lookup_valid_word(0)
status("the word is " + word)
log("the word is " + word)
return
}
const [idx, entropy, word] = solver.best_word()
status("best word is " + word + " , " + entropy + " bits")
log("best word " + word + " , " + entropy + " bits")
})
document.getElementById('submit').addEventListener('click', e => {
if (solver == null) {
status("please initialize the solver first")
return
}
const input_elem = document.getElementById('guess')
const input = input_elem.value.trim()
if (input.length != 5) {
status(input + " is not a valid word")
return
}
const idx = solver.find_word(input)
if (idx == -1) {
status(input + " is not a valid word")
return
}
cur_guess = input
num_guess++
const my_num_guess = num_guess
for (var i = 0; i < 5; i++) {
cur_status[i] = 0
}
const cur_word = document.getElementById('cur-word')
while (cur_word.firstChild) {
cur_word.removeChild(cur_word.lastChild)
}
for (var i = 0; i < 5; i++) {
const elem = document.createElement('span')
elem.textContent = input[i].toUpperCase()
elem.classList.add('letter')
elem.classList.add('notincluded')
const char_idx = i
elem.addEventListener('click', e => {
if (num_guess != my_num_guess) return
cur_status[char_idx]++
if (cur_status[char_idx] >= 3) {
cur_status[char_idx] = 0
}
elem.classList.remove('match')
elem.classList.remove('exists')
elem.classList.remove('notincluded')
switch (cur_status[char_idx]) {
case 0:
elem.classList.add('notincluded')
break
case 1:
elem.classList.add('match')
break
case 2:
elem.classList.add('exists')
}
})
cur_word.appendChild(elem)
}
})
document.getElementById('confirm').addEventListener('click', e => {
if (solver == null) {
status("please initialize the solver first")
return
}
if (cur_guess != null) {
var nodes = []
const cur_word = document.getElementById('cur-word')
while (cur_word.firstChild) {
nodes.push(cur_word.firstChild)
cur_word.removeChild(cur_word.firstChild)
}
const div_wrapper = document.createElement('div')
for (var i = 0; i < nodes.length; i++) {
div_wrapper.appendChild(nodes[i])
}
document.getElementById('history').appendChild(div_wrapper)
const guess_idx = solver.find_word(cur_guess)
if (guess_idx == -1) return
chosen_words.push([guess_idx, [...cur_status]])
solver.eliminate_words(guess_idx, undump_match(cur_status))
log("guessed " + cur_guess + " " + solver.words_left() + " words left")
status("guessed " + cur_guess + " " + solver.words_left() + " words left")
cur_guess = null
}
})
document.getElementById('back').addEventListener('click', e => {
if (chosen_words.length == 0) return
if (solver == null) {
status("please initialize the solver first")
return
}
chosen_words.pop()
const history = document.getElementById('history')
history.removeChild(history.lastChild)
// replay the game up to this point
solver.reset()
for (var i = 0; i < chosen_words.length; i++) {
solver.eliminate_words(
chosen_words[i][0],
undump_match(chosen_words[i][1]))
}
const num_valid = solver.words_left()
log("removed last added word, " + num_valid + " words left")
status("removed last added word, " + num_valid + " words left")
})
})
})()
</script>
</head>
<body>
<div class="history" id="history"></div>
<div id="cur-word"></div>
<form>
<p>
<input id="guess" minlength=5 maxlength=5 size=5></input>
</p>
<p>
<input type=button id="init" value="Initialize"></input>
<input type=button id="reset" value="Reset game"></input>
<input type=button id="submit" value="Submit word"></input>
<input type=button id="confirm" value="Confirm word"></input>
<input type=button id="back" value="Back"></input>
<input type=button id="find" value="Find best"></input>
</p>
</form>
<div id=status>loading words...</div>
log:
<!-- TODO show a sorted list of all the valid words or all the best words to guess with -->
<div id=log class=log></div>
<h1>How to use</h1>
<p>1. Initialize the solver by pressing the Initialize button. This causes it to precompute this large (~220 MB) array of values</p>
<p>2. Use "Find best" to choose a word for you or enter in your own choice of word, and press Submit word. Submit this word in Wordle as well</p>
<p>3. Click on the letters of the word to match the results of what was matching according to Wordle. Hit confirm when it matches the colors of the word in Wordle. This will cause the solver to eliminate words that are no longer possible. If there was an error in the color pattern you can hit Back to remove the last confirmed word, and reenter it. </p>
<p>4. Optimally win Wordle. </p>
</body>
</html>

View File

@ -53,8 +53,6 @@ var valid_words = null;
console.log("found " + words.length+ " words")
status("found " + words.length + " words")
valid_words = gen_all_words_list()
init_wasm_solver(text)
})
req.send()

View File

@ -65,6 +65,7 @@ var all_matches = null
const precompute_all = (() => {
var idx = 0
var init_start = null
var num_loops = 10
return () => {
const l = words.length
const start = Date.now()
@ -73,7 +74,7 @@ const precompute_all = (() => {
all_matches = new Uint8Array(all_matches_buffer)
init_start = start
}
for (; (Date.now() - start) < 40 && idx < l; idx++) {
for (var i = 0; i < num_loops && idx < l; i++, idx++) {
for (var j = 0; j < l; j++) {
all_matches[idx*l + j] = compute_match(words[idx], words[j])
}
@ -81,9 +82,12 @@ const precompute_all = (() => {
if (idx < l) {
status("computing word " + idx + "/" + words.length + " " + words[idx])
}
const end = Date.now()
const millis = end - start
num_loops = Math.floor(num_loops - 0.5*(millis - 50))
num_loops = Math.max(num_loops, 1)
if (idx >= l) {
const end = Date.now()
inited = true
console.log("precompute done, mem = " + all_matches.length + " bytes")
log("precompute done, mem = " + all_matches.length + " bytes")

View File

@ -1,7 +1,8 @@
var wasm_solver = null
var solver = null
var word_count = 0
var words_str_len = 0
function init_wasm_solver(words_str) {
words_str_len = words_str.length
const fill_string = (offset) => {
console.log("fill strings called " + offset)
const str_buf = new Uint8Array(wasm_solver.exports.memory.buffer, offset)
@ -9,6 +10,7 @@ function init_wasm_solver(words_str) {
str_buf.set(buf, 0)
}
const log_num_idxs = i => {
word_count = i
console.log("wasm: found " + i + " words")
}
WebAssembly.instantiateStreaming(fetch("wordle_opt.wasm"), {
@ -18,47 +20,61 @@ function init_wasm_solver(words_str) {
}
}).then(wm => {
wasm_solver = wm.instance
const exports = wasm_solver.exports
const solver_ptr = wasm_solver.exports.init(words_str.length)
solver = {
ptr: solver_ptr,
precalc: (nw) => exports.precalc(solver_ptr, nw),
precalc_done: () => exports.precalc_done(solver_ptr),
reset: () => exports.reset(solver_ptr),
eliminate_words: (guess_idx, guess_result) =>
exports.eliminate_words(solver_ptr, guess_idx, guess_result),
words_left: () => exports.words_left(solver_ptr),
calc_entropy: (idx) => exports.calc_entropy(solver_ptr, idx),
find_word: (w) => {
const ary = new TextEncoder().encode(w, "utf8")
const offset = exports.find_word_load(solver_ptr)
const dst = new Uint8Array(exports.memory.buffer, offset, 5)
dst.set(ary)
return exports.find_word(solver_ptr)
},
best_word: () => {
const idx = exports.best_word(solver_ptr)
const entropy = exports.best_word_entropy(solver_ptr)
var word = null
if (idx != -1) {
const offset = exports.lookup_word(solver_ptr, idx)
word_ary = new Uint8Array(exports.memory.buffer, offset, 5)
word = new TextDecoder().decode(word_ary)
}
return [idx, entropy, word]
},
lookup_valid_word: (i) => {
const idx = exports.get_valid_word(solver_ptr, i)
if (idx != -1) {
const offset = exports.lookup_word(solver_ptr, idx)
word_ary = new Uint8Array(exports.memory.buffer, offset, 5)
return new TextDecoder().decode(word_ary)
} else {
return null
}
}
}
})
}
function init_solver() {
const exports = wasm_solver.exports
console.log("wsl = ", words_str_len)
const solver_ptr = wasm_solver.exports.init(words_str_len)
return {
ptr: solver_ptr,
precalc: (nw) => exports.precalc(solver_ptr, nw),
precalc_idx: () => exports.get_precalc(solver_ptr),
precalc_done: () => exports.precalc_done(solver_ptr),
reset: () => exports.reset(solver_ptr),
eliminate_words: (guess_idx, guess_result) =>
exports.eliminate_words(solver_ptr, guess_idx, guess_result),
words_left: () => exports.words_left(solver_ptr),
calc_entropy: (idx) => exports.calc_entropy(solver_ptr, idx),
find_word: (w) => {
const ary = new TextEncoder().encode(w, "utf8")
const offset = exports.find_word_load(solver_ptr)
const dst = new Uint8Array(exports.memory.buffer, offset, 5)
dst.set(ary)
return exports.find_word(solver_ptr)
},
best_word: () => {
const idx = exports.best_word(solver_ptr)
const entropy = exports.best_word_entropy(solver_ptr)
console.log(idx)
var word = null
if (idx != -1) {
const offset = exports.lookup_word(solver_ptr, idx)
word_ary = new Uint8Array(exports.memory.buffer, offset, 5)
word = new TextDecoder().decode(word_ary)
}
return [idx, entropy, word]
},
lookup_word: (idx) => {
const offset = exports.lookup_word(solver_ptr, idx)
if (offset == -1) return null
word_ary = new Uint8Array(exports.memory.buffer, offset, 5)
return new TextDecoder().decode(word_ary)
},
lookup_valid_word: (i) => {
const idx = exports.get_valid_word(solver_ptr, i)
if (idx != -1) {
const offset = exports.lookup_word(solver_ptr, idx)
word_ary = new Uint8Array(exports.memory.buffer, offset, 5)
return new TextDecoder().decode(word_ary)
} else {
return null
}
}
}
}