Files
xteink-apps/apps/Wordle/main.lua
T
evan 77d123ab1f feat(appstore): browse cached catalogs offline, drop Wi-Fi after use
Also mark Wordle 'present' tiles and keys with an inset ring, since gray
shades alone are hard to distinguish on e-ink.
2026-07-27 14:02:30 -04:00

253 lines
8.2 KiB
Lua

-- 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 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
local needsDraw = 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
needsDraw = 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
needsDraw = true
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)
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, 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
centeredText(FONT_NOTOSANS_16, x, y + math.floor(size / 2) - 10, size, letter, textColor, STYLE_BOLD)
end
end
local function drawGrid(width, height)
local gap = 5
local size = math.min(62, math.floor((width - 60 - gap * (WORD_LENGTH - 1)) / WORD_LENGTH),
math.floor((height * 0.53 - gap * (MAX_GUESSES - 1)) / MAX_GUESSES))
local gridWidth = size * WORD_LENGTH + gap * (WORD_LENGTH - 1)
local startX = math.floor((width - gridWidth) / 2)
local startY = 72
for row = 1, MAX_GUESSES do
local word = guesses[row] or (row == #guesses + 1 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)
end
end
return startY + MAX_GUESSES * size + (MAX_GUESSES - 1) * gap
end
local function drawKeyboard(width, height, top)
local gap = 4
local margin = 12
local keyWidth = math.floor((width - margin * 2 - gap * 9) / 10)
local keyHeight = math.min(48, math.floor((height - top - 70 - gap * 2) / 3))
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, 16, "WORDLE", COLOR_BLACK, STYLE_BOLD)
gui.drawCenteredText(FONT_SMALL, 48, message, COLOR_BLACK, STYLE_REGULAR)
if answer then
local gridBottom = drawGrid(width, height)
drawKeyboard(width, height, gridBottom + 14)
end
gui.drawButtonHints("<<", "Select", "<", ">")
gui.refresh(firstDraw and REFRESH_HALF or REFRESH_FAST)
firstDraw = false
needsDraw = false
end
function init()
startGame()
end
function draw()
if input.wasPressed("back") then
sys.exit()
return
end
if gameOver and answer then
if input.wasPressed("confirm") then startGame() end
elseif answer then
if input.wasPressed("left") then
cursorColumn = cursorColumn == 1 and #keyboard[cursorRow] or cursorColumn - 1
needsDraw = true
elseif input.wasPressed("right") then
cursorColumn = cursorColumn == #keyboard[cursorRow] and 1 or cursorColumn + 1
needsDraw = true
elseif input.wasPressed("up") then
cursorRow = cursorRow == 1 and #keyboard or cursorRow - 1
cursorColumn = math.min(cursorColumn, #keyboard[cursorRow])
needsDraw = true
elseif input.wasPressed("down") then
cursorRow = cursorRow == #keyboard and 1 or cursorRow + 1
cursorColumn = math.min(cursorColumn, #keyboard[cursorRow])
needsDraw = true
elseif input.wasPressed("confirm") then
useKey(keyboard[cursorRow][cursorColumn])
end
end
if needsDraw then render() end
end