Source code
Wordle Solver
An Express.js word solver with a JavaScript interface and regex-based dictionary matching.
Runs with: Node.js with Express and EJS
Project files
Download the original files, or browse the code below.
Browse source
wordle/index.js
const express = require("express");
const app = express();
app.set('view engine','ejs')
const path = require('path');
app.set('views', path.join(__dirname,'views'))
app.use(express.static(path.join(__dirname,'static')));
app.use(express.json());
const fs = require('fs'); // fs is built-in. no npm install
// generate a file path (as a string) for the words file
const wordsFilePath = path.join(__dirname,'enable1.txt')
// use fs to read the file; convert bytes to string split on newlines
const words = fs.readFileSync(wordsFilePath).toString().split('\n')
.map(word => word.trim().toLowerCase()).filter(word => /^[a-z]{5}$/.test(word))
// keep the five-letter words from enable1.txt
function matchesGuess(word, guess) {
let letters = word.split('');
let colors = Array(5).fill('grey');
for (let i = 0; i < 5; i++) {
if (word[i] === guess.word[i]) {
colors[i] = 'green';
letters[i] = '';
}
}
// use up green letters first, then check the remaining copies for yellow
for (let i = 0; i < 5; i++) {
if (colors[i] !== 'green') {
let index = letters.indexOf(guess.word[i]);
if (index !== -1) {
colors[i] = 'yellow';
letters[index] = '';
}
}
}
return colors.every((color, i) => color === guess.colors?.[i]);
}
app.get('/', (req,res) => {
res.redirect('home')
})
app.get('/home', (req,res) => {
res.render('home')
})
app.post('/wordfinder', (req, res) => {
let guesses = req.body && req.body.guesses;
if (guesses === undefined) {
let word = req.body && req.body.word;
if (typeof word !== 'string' || word.length !== 5 || !/^[a-z.]{5}$/i.test(word)) {
return res.sendStatus(400);
}
if (req.body.colors !== undefined) {
guesses = [{ word, colors: req.body.colors }];
} else {
let regex = new RegExp('^' + word + '$', 'i');
let filteredWords = words.filter(word => regex.test(word));
return res.json(filteredWords.length ? filteredWords : ["No matching words."]);
}
}
if (!Array.isArray(guesses) || guesses.length === 0 || !guesses.every(guess =>
guess && typeof guess.word === 'string' && guess.word.length === 5 && /^[a-z]{5}$/i.test(guess.word)
)) {
return res.sendStatus(400);
}
guesses = guesses.map(guess => ({ word: guess.word.toLowerCase(), colors: guess.colors }));
let filteredWords = words.filter(word => guesses.every(guess => matchesGuess(word, guess)));
let string = "No matching words."
if (filteredWords.length === 0) {
res.json([string]);
} else {
res.json(filteredWords);
}
});
if (require.main === module) {
const listener = app.listen(
process.env.PORT || 8080,
process.env.HOST || "0.0.0.0",
function() {
console.log("Express server started");
}
);
}
module.exports = app; wordle/views/home.ejs
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Wordle Solver</title>
<link type="text/css" href="style.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="wrapper">
<h1>Wordle Solver</h1>
<div class="content">
<p class="instructions">Enter a guess, set its colors, then submit.</p>
<p class="instructions">Grey: missing. Yellow: wrong position. Green: right position.</p>
<div class="inputs" role="group" aria-label="Current guess">
</div>
<br><br>
<button type="button" class="submit-btn">Submit</button>
<br><br>
<button type="button" class="reset-btn">Reset</button>
<div class="guesses-container" aria-label="Previous guesses"></div>
</div>
</div>
<p class="result-count" role="status" aria-live="polite"></p>
<div class="results-container"></div>
<a href="https://www.codingnepalweb.com/word-guessing-game-html-css-javascript/" target="_blank" rel="noopener noreferrer">Source for CSS and some functionalities</a>
<script src="script.js"></script>
</body>
</html> wordle/static/script.js
const inputs = document.querySelector(".inputs"),
submitButton = document.querySelector(".submit-btn"),
resetButton = document.querySelector(".reset-btn"),
guessContainer = document.querySelector(".guesses-container"),
resultCount = document.querySelector(".result-count");
let guesses = [];
let requests = 0;
const colors = ['grey', 'yellow', 'green'];
let boxes = "";
for (let i = 1; i <= 5; i++) {
boxes += `<div class="letter"><input type="text" id="input${i}" class="grey" data-color="grey" maxlength="1" autocomplete="off" autocapitalize="none" spellcheck="false" aria-label="Letter ${i}"><button type="button" class="color-btn" data-input="input${i}" aria-label="Letter ${i}: Grey. Change color.">Grey</button></div>`;
}
inputs.innerHTML = boxes;
function stuff(e){
if (e.target.tagName !== 'INPUT') return;
let input = e.target;
input.value = input.value.toLowerCase().replace(/[^a-z]/g, '').slice(0, 1);
let next = document.getElementById(`input${Number(input.id.slice(5)) + 1}`);
if (input.value && next) {
next.focus();
next.select();
}
}
function displayWords(words) {
let stuff = document.querySelector('.results-container');
stuff.textContent = words.join(' ');
resultCount.textContent = words.length && words[0] !== 'No matching words.' ? words.length + ' matches' : '';
}
async function fetchstuff(clues) {
let request = ++requests;
const response = await fetch('/wordfinder', {
method: 'POST',
headers: {
'Content-Type': 'application/json', //fetch stuff
},
body: JSON.stringify({ guesses: clues })
});
const data = await response.json();
if (request === requests) {
displayWords(data);
}
}
function resetInputs() {
inputs.querySelectorAll('input').forEach(input => {
input.value = '';
input.className = 'grey';
input.dataset.color = 'grey';
});
inputs.querySelectorAll('button').forEach((button, i) => {
button.textContent = 'Grey';
button.setAttribute('aria-label', `Letter ${i + 1}: Grey. Change color.`);
});
}
inputs.addEventListener("input", stuff);
inputs.addEventListener("click", (e) => {
if (!e.target.classList.contains('color-btn')) return;
let input = document.getElementById(e.target.dataset.input);
let color = colors[(colors.indexOf(input.dataset.color) + 1) % colors.length];
input.dataset.color = color;
input.className = color;
e.target.textContent = color[0].toUpperCase() + color.slice(1);
e.target.setAttribute('aria-label', `Letter ${input.id.slice(5)}: ${e.target.textContent}. Change color.`);
});
inputs.addEventListener("keydown", (e) => {
if (e.target.tagName !== 'INPUT') return;
let index = Number(e.target.id.slice(5));
if (e.key === 'Backspace' && !e.target.value && index > 1) {
let previous = document.getElementById(`input${index - 1}`);
previous.focus();
previous.select();
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
let next = document.getElementById(`input${index + (e.key === 'ArrowLeft' ? -1 : 1)}`);
if (next) {
e.preventDefault();
next.focus();
next.select();
}
} else if (e.key === 'Enter') {
e.preventDefault();
submitButton.click();
}
});
inputs.addEventListener("paste", (e) => {
if (e.target.tagName !== 'INPUT') return;
e.preventDefault();
let word = e.clipboardData.getData('text').toLowerCase().replace(/[^a-z]/g, '');
let index = Number(e.target.id.slice(5));
for (let i = index; i <= 5 && i - index < word.length; i++) {
let input = document.getElementById(`input${i}`);
input.value = word[i - index];
input.focus();
input.select();
}
});
submitButton.addEventListener("click", () => {
let userInput = Array.from(inputs.querySelectorAll("input")).map(input => input.value).join(''); //get the data from the boxes and make it an array
if (!/^[a-z]{5}$/.test(userInput)) {
resultCount.textContent = 'Enter five letters.';
return;
}
let guess = { word: userInput, colors: Array.from(inputs.querySelectorAll('input')).map(input => input.dataset.color) };
guesses.push(guess);
let row = document.createElement('div');
row.className = 'guess-row';
row.setAttribute('role', 'group');
row.setAttribute('aria-label', `Guess ${guesses.length}: ${guess.word}`);
for (let i = 0; i < 5; i++) {
let letter = document.createElement('span');
letter.className = guess.colors[i];
letter.textContent = guess.word[i];
letter.setAttribute('role', 'img');
letter.setAttribute('aria-label', `${guess.word[i]}: ${guess.colors[i]}`);
row.appendChild(letter);
}
guessContainer.appendChild(row);
resetInputs();
fetchstuff(guesses);
document.getElementById('input1').focus();
});
resetButton.addEventListener("click", () => {
guesses = [];
requests++;
guessContainer.textContent = '';
resetInputs();
displayWords([]);
document.getElementById('input1').focus();
}); wordle/static/style.css
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap');
*{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
body{
display: flex;
flex-direction: column;
padding: 25px 10px;
min-height: 100vh;
align-items: center;
justify-content: center;
background: #1BB295;
}
.wrapper{
width: 430px;
background: #fff;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.wrapper h1{
font-size: 25px;
font-weight: 500;
padding: 20px 25px;
border-bottom: 1px solid #ccc;
}
.wrapper .content{
margin: 25px 25px 35px;
}
.content .inputs{
display: flex;
flex-wrap: nowrap;
justify-content: center;
gap: 8px;
}
.letter{
width: 56px;
min-width: 0;
flex: 0 1 56px;
}
.inputs input{
height: 57px;
width: 100%;
margin: 0;
font-size: 24px;
font-weight: 500;
color: #1ba98c;
text-align: center;
border-radius: 5px;
background: none;
text-transform: uppercase;
border: 1px solid #B5B5B5;
}
.color-btn{
width: 100%;
margin-top: 6px;
padding: 5px 0;
border: 1px solid #B5B5B5;
border-radius: 5px;
background: white;
color: #333;
cursor: pointer;
font-size: 12px;
}
.instructions{
margin-bottom: 18px;
color: #444;
font-size: 13px;
line-height: 1.6;
}
.inputs input:focus-visible, button:focus-visible, a:focus-visible{
outline: 3px solid #174b42;
outline-offset: 3px;
}
.inputs input:first-child{
margin-left: 0px;
}
.content .details{
margin: 20px 0 25px;
}
.content .submit-btn{
width: 100%;
border: none;
cursor: pointer;
color: #fff;
outline: none;
padding: 15px 0;
font-size: 17px;
border-radius: 5px;
background: #1BB295;
transition: all 0.3s ease;
}
.content .submit-btn:hover{
background: #18a589;
}
.reset-btn{
width: 100%;
border: none;
cursor: pointer;
color: #fff;
outline: none;
padding: 15px 0;
font-size: 17px;
border-radius: 5px;
background: grey;
transition: all 0.3s ease;
}
.reset-btn:hover{
background: red
}
.results-container {
max-width: 700px;
margin-top: 20px;
padding: 10px;
display:block;
}
.inputs input.green, .guess-row .green {
background-color: #538d4e;
border-color: #538d4e;
color: white;
}
.inputs input.yellow, .guess-row .yellow {
background-color: #c9b458;
border-color: #c9b458;
color: #242424;
}
.inputs input.grey, .guess-row .grey {
background-color: #787c7e;
border-color: #787c7e;
color: white;
}
.guesses-container:not(:empty){
margin-top: 25px;
}
.guess-row{
display: flex;
justify-content: center;
gap: 6px;
margin-top: 8px;
}
.guess-row span{
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 5px;
font-size: 22px;
font-weight: 600;
text-transform: uppercase;
}
.result-count{
text-align: center;
}
@media screen and (max-width: 460px) {
.wrapper {
width: 100%;
}
.wrapper h1{
font-size: 22px;
padding: 16px 20px;
}
.wrapper .content{
margin: 25px 20px 35px;
}
.inputs input{
height: 51px;
font-size: 22px;
}
.letter{
width: 50px;
flex-basis: 50px;
}
.content .inputs{
gap: 6px;
}
.content .submit-btn, .reset-btn{
padding: 14px 0;
font-size: 16px;
}
} wordle/package.json
{
"name": "wordle-solver",
"private": true,
"type": "commonjs",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"ejs": "^7.0.2",
"express": "^5.3.0"
}
}