Files

283 lines
9.0 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 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