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"
  }
}