-- DESCRIPTION: Guess the five-letter word in six tries local WORDS_PATH = "/.apps/Wordle/words.txt" local WORD_LENGTH = 5 local MAX_GUESSES = 6 local SECTION_GAP = 10 local keyboard = { { "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P" }, { "A", "S", "D", "F", "G", "H", "J", "K", "L" }, { "OK", "Z", "X", "C", "V", "B", "N", "M", "DEL" } } local answer = nil local current = "" local guesses = {} local results = {} local keyStates = {} local cursorRow = 1 local cursorColumn = 1 local message = "" local gameOver = false local firstDraw = true -- Word Source - Replace only this function to fetch answers from another source. local function getRandomWord() local size = fs.fileSize(WORDS_PATH) if not size then return nil, "Missing " .. WORDS_PATH end if size == 0 then return nil, "Word list is empty" end for _ = 1, 8 do local word = fs.readLineAt(WORDS_PATH, math.random(0, size - 1)) or fs.readLineAt(WORDS_PATH, 0) if word then word = word:match("^%s*(.-)%s*$") if #word == WORD_LENGTH and word:match("^%a+$") then return word:upper() end end end return nil, "Word list has no five-letter words" end local function startGame() local errorMessage answer, errorMessage = getRandomWord() current = "" guesses = {} results = {} keyStates = {} cursorRow = 1 cursorColumn = 1 gameOver = not answer message = errorMessage or "Pick a letter" firstDraw = true end local function evaluateGuess(guess) local status = {} local remaining = {} for index = 1, WORD_LENGTH do local letter = answer:sub(index, index) if guess:sub(index, index) == letter then status[index] = 2 else remaining[letter] = (remaining[letter] or 0) + 1 end end for index = 1, WORD_LENGTH do if not status[index] then local letter = guess:sub(index, index) if (remaining[letter] or 0) > 0 then status[index] = 1 remaining[letter] = remaining[letter] - 1 else status[index] = 0 end end end return status end local function submitGuess() if #current ~= WORD_LENGTH then message = "Enter five letters" return end local status = evaluateGuess(current) guesses[#guesses + 1] = current results[#results + 1] = status for index = 1, WORD_LENGTH do local letter = current:sub(index, index) keyStates[letter] = math.max(keyStates[letter] or 0, status[index]) end if current == answer then message = "Solved! Press OK for a new word" gameOver = true elseif #guesses == MAX_GUESSES then message = answer .. " - Press OK for a new word" gameOver = true else message = tostring(MAX_GUESSES - #guesses) .. " guesses left" end current = "" end local function useKey(key) if key == "OK" then submitGuess() elseif key == "DEL" then current = current:sub(1, -2) message = "Pick a letter" elseif #current < WORD_LENGTH then current = current .. key message = "Pick a letter" end end local function centeredText(font, x, y, width, text, color, style) local textWidth = gui.getTextWidth(font, text, style) gui.drawText(font, x + math.floor((width - textWidth) / 2), y, text, color, style) end local function drawTile(letter, status, x, y, size, active) local fill = COLOR_WHITE local textColor = COLOR_BLACK if status == 2 then fill = COLOR_BLACK textColor = COLOR_WHITE elseif status == 1 then fill = COLOR_LIGHT_GRAY elseif status == 0 then fill = COLOR_DARK_GRAY textColor = COLOR_WHITE end if status ~= nil then gui.fillRoundedRect(x, y, size, size, 5, fill) end gui.drawRoundedRect(x, y, size, size, active and 3 or 2, 5, COLOR_BLACK) -- Present Marker - Gray shades alone are hard to tell apart on e-ink, so "in word, wrong spot" also gets an inset ring. if status == 1 then gui.drawRoundedRect(x + 5, y + 5, size - 10, size - 10, 2, 3, COLOR_BLACK) end if letter ~= "" then if active then gui.drawRoundedRect(x + 4, y + 4, size - 8, size - 8, 1, 3, COLOR_BLACK) end centeredText(FONT_NOTOSANS_16, x, y + math.floor(size / 2) - 10, size, letter, textColor, STYLE_BOLD) end end local function keyboardMetrics(height) local gap = 4 local keyHeight = math.min(52, math.floor(height * 0.07)) return height - 56 - (keyHeight * 3 + gap * 2), keyHeight end local function drawGrid(width, height) local gap = 6 local startY = 72 local keyboardTop = keyboardMetrics(height) local gridBottom = keyboardTop - SECTION_GAP * 2 - 18 local size = math.min(72, math.floor((width - 40 - gap * (WORD_LENGTH - 1)) / WORD_LENGTH), math.floor((gridBottom - startY - gap * (MAX_GUESSES - 1)) / MAX_GUESSES)) local gridWidth = size * WORD_LENGTH + gap * (WORD_LENGTH - 1) local startX = math.floor((width - gridWidth) / 2) for row = 1, MAX_GUESSES do local active = row == #guesses + 1 local word = guesses[row] or (active and current or "") local status = results[row] for column = 1, WORD_LENGTH do drawTile(word:sub(column, column), status and status[column] or nil, startX + (column - 1) * (size + gap), startY + (row - 1) * (size + gap), size, active) end end return startY + MAX_GUESSES * size + (MAX_GUESSES - 1) * gap end local function drawLegend(width, y) local items = { {"Right", 2}, {"Spot", 1}, {"Miss", 0}, } local swatch = 18 local gap = 12 local itemWidth = 76 local startX = math.floor((width - itemWidth * #items - gap * (#items - 1)) / 2) for index, item in ipairs(items) do local x = startX + (index - 1) * (itemWidth + gap) drawTile("", item[2], x, y, swatch) gui.drawText(FONT_SMALL, x + swatch + 5, y + 2, item[1], COLOR_BLACK, STYLE_REGULAR) end end local function drawKeyboard(width, height) local gap = 4 local margin = 12 local keyWidth = math.floor((width - margin * 2 - gap * 9) / 10) local top, keyHeight = keyboardMetrics(height) for rowIndex, row in ipairs(keyboard) do local rowWidth = #row * keyWidth + (#row - 1) * gap local startX = math.floor((width - rowWidth) / 2) local y = top + (rowIndex - 1) * (keyHeight + gap) for columnIndex, key in ipairs(row) do local x = startX + (columnIndex - 1) * (keyWidth + gap) local status = keyStates[key] local fill = status == 2 and COLOR_BLACK or status == 1 and COLOR_LIGHT_GRAY or status == 0 and COLOR_DARK_GRAY or COLOR_WHITE local textColor = (status == 2 or status == 0) and COLOR_WHITE or COLOR_BLACK gui.fillRoundedRect(x, y, keyWidth, keyHeight, 4, fill) gui.drawRoundedRect(x, y, keyWidth, keyHeight, 1, 4, COLOR_BLACK) if status == 1 then gui.drawRoundedRect(x + 4, y + 4, keyWidth - 8, keyHeight - 8, 1, 3, COLOR_BLACK) end if rowIndex == cursorRow and columnIndex == cursorColumn then gui.drawRoundedRect(x + 2, y + 2, keyWidth - 4, keyHeight - 4, 3, 3, textColor) end centeredText(FONT_SMALL, x, y + math.floor(keyHeight / 2) - 7, keyWidth, key, textColor, STYLE_BOLD) end end end local function render() local width, height = gui.width(), gui.height() gui.clear() gui.drawCenteredText(FONT_NOTOSANS_16, 8, "WORDLE", COLOR_BLACK, STYLE_BOLD) gui.drawCenteredText(FONT_SMALL, 46, message, COLOR_BLACK, STYLE_REGULAR) if answer then local gridBottom = drawGrid(width, height) drawLegend(width, gridBottom + SECTION_GAP) drawKeyboard(width, height) end gui.drawButtonHints("<<", "Select", "<", ">") gui.refresh(firstDraw and REFRESH_HALF or REFRESH_FAST) firstDraw = false end function init() startGame() render() end function on_button(name, event) if event ~= "down" then return end if name == "back" then sys.exit() return end if gameOver and answer then if name ~= "confirm" then return end startGame() elseif answer then if name == "left" then cursorColumn = cursorColumn == 1 and #keyboard[cursorRow] or cursorColumn - 1 elseif name == "right" then cursorColumn = cursorColumn == #keyboard[cursorRow] and 1 or cursorColumn + 1 elseif name == "page_back" then cursorRow = cursorRow == 1 and #keyboard or cursorRow - 1 cursorColumn = math.min(cursorColumn, #keyboard[cursorRow]) elseif name == "page_forward" then cursorRow = cursorRow == #keyboard and 1 or cursorRow + 1 cursorColumn = math.min(cursorColumn, #keyboard[cursorRow]) elseif name == "confirm" then useKey(keyboard[cursorRow][cursorColumn]) else return end else return end render() end