← Back to projects
griddle-dark

I'm not quite sure where the idea for griddle came from. It is kinda inspired by wordle but the gameplay is totally different. The idea is that the user is presented with a grid of letters and has to find a word by selecting one letter from each row. The word is scored based on how many valid words are left matching each letter selection. You can either aim for a high score by picking a word that has commonalities with many other words, or a low score by picking a more unique word. The low score mode is generally harder.

I'd count this as one of my "lo-fi" projects like A million digits or Covid vaccinations if each state was a horse where I eschew frameworks and libraries and embrace writing code in a old school sort of way. I find this style of coding somewhat liberating as I can make decisions knowing that it isn't something that I have to maintain, throwing conventions like DRY out the window in favour of whatever strategy pops into my head. This leads to what could be considered pretty "bad" code but for this sort of project the end goal is what matters, and I can know that if I do want to tweak it in 5 years time I won't have to worry about node 16 not compiling on my new Apple MX™ processor because only node 47 is supported now.

That said, there were some fun little algorithms that came out of writing this, particularly the function to get all the possible "words" in the grid. I thought this was a pretty good concise and fast solution:

function getStringsInGrid(grid) {
    const gridSize = grid.length;
    const count = Math.pow(gridSize, gridSize);
    const wordsInGrid = [];

    for(let i = 0; i < count; i ++) {
        let word = '';
        for(let j = 0; j < gridSize; j ++) {
            const divisor = Math.pow(gridSize, gridSize - j - 1);
            const index = Math.floor(i / divisor) % gridSize;
            word = word + grid[j][index];
        }
        wordsInGrid.push(word);
    }
    return wordsInGrid;
}

The other interesting problem was choosing letters for the grid. A naive approach of just picking random letters from A-Z meant that there often were not many words available in the grid. So I wrote a script to do a sort of "pseudo weighting" of letters. It works by looking at the words list and seeing how many words there are that start with each letter and appending that many of that letter to the list of possible letters to choose from.

So for the first row of the grid, it would look at all the words in the list and see for example that there are 100 words that start with a, the list of letters to choose randomly from for the first row of the grid would become abcdefghijklmnopqrstuvwxyzaaaaaaaaaaaaaaaaaaa...(with 100 a's) and so on for all the other letters. This has the effect of increasing the chances that a relevant letter is chosen, while still retaining a good amount of randomness. There's also a bit of code in there to ensure that there are no duplicate letters in rows.

function createGrid(gridSize, words, randomItem) {
    const grid = [];
    for(let i = 0; i < gridSize; i++) {
        const letterWeights = getLetterWeights(words, grid);
        grid[i] = [];

        for(let j = 0; j < gridSize; j++) {
            const getUniqueLetter = () => {
                const letter = randomItem(letterWeights);
                if(grid[i].includes(letter)) return getUniqueLetter();
                return letter;
            }
            const uniqueLetter = getUniqueLetter();
            grid[i][j] = uniqueLetter;
        }
    }
    return grid;
}


function getLetterWeights(words, prevRows) {
    const index = prevRows.length;
    const prevRowLetters = prevRows.map(row => row.join(''));
    let prefixes = new Set();
    if(prevRows.length > 0) {
        const gridSize = prevRows[0].length;
        const rowsToFill = gridSize - prevRows.length;
        const rows = new Array(rowsToFill).fill(new Array(gridSize).fill('.'));
        const fullGrid = [...prevRows, ...rows];
        const words = getStringsInGrid(fullGrid);
        prefixes = new Set(words.map(word => word.substring(0, prevRows.length, 2)));
    }

    const weightedLetters = [];

    words.forEach(word => {
        if(prevRows.length === 0) weightedLetters.push(word[index]);
        const wordPrefix = word.substring(0, prevRows.length, 2);
        if(prefixes.has(wordPrefix)) {
            weightedLetters.push(word[index]);
        }
    });


    return alphabet.split('').concat(weightedLetters);
}

I'm still not sure about the final game, there's something a bit off about the gameplay and the scoring is pretty hard to grok, and hard to explain, but there's still something weird and fun about it 🤷