77d123ab1f
Also mark Wordle 'present' tiles and keys with an inset ring, since gray shades alone are hard to distinguish on e-ink.
253 lines
8.2 KiB
Lua
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
|