diff --git a/src/examples/2048-1-version/LICENSE b/src/examples/2048-1-version/LICENSE new file mode 100644 index 00000000..0b7e2b8a --- /dev/null +++ b/src/examples/2048-1-version/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Vadim1987 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/examples/2048-1-version/README.md b/src/examples/2048-1-version/README.md new file mode 100644 index 00000000..1dfa528e --- /dev/null +++ b/src/examples/2048-1-version/README.md @@ -0,0 +1,154 @@ + + +# 2048 (Compy-friendly) + +A minimalist implementation of the classic **2048** game in Lua/LÖVE, polished for teaching purposes and following **Compy formatting rules** (≤64 characters per line, ≤14 lines per function/table, ≤4 arguments, nesting depth ≤4, no complex inline expressions). + +The game draws a 4×4 grid; you slide tiles with arrow keys / WASD, merging powers of two. + +--- + +## Multiple source files + +The project is split into several short files for readability and pedagogy (like in the example) : + +* `board.lua` — board logic (data, moves, merges, game over, drawing) +* `main.lua` — glue for input/drawing and window setup +* `utils.lua` — placeholder for future helpers + +--- + +## Gameplay (in short) + +* Each move **shifts** all numbers in the chosen direction, then performs **merges** of equal neighbors. +* After a successful move, a new tile `2` (90%) or `4` (10%) appears. +* When there are no empty cells and no possible merges — **Game Over**. + +--- + +## Controls + +* **Arrow keys** or **W/A/S/D** — move +* **R** — restart +* **Esc** — quit + +See the handler in `love.keypressed`: a simple key map with unified move logic. + +```lua +-- main.lua (fragment) +function love.keypressed(key) + if key == "escape" then love.event.quit() end + local map = { + left="left", right="right", up="up", down="down", + a="left", d="right", w="up", s="down", + } + local dir = map[key] + if dir then + if board:move(dir) then board:add_random_tile() end + elseif key == "r" then + board:reset() + end +end +``` + +--- + +## Board logic + +Everything related to data and rules lives in `board.lua`. + +### Data structure + +* `self.grid[r][c]` — integer (0 = empty, otherwise power of two). +* Sizes: `self.rows`, `self.cols` (default 4×4). +* Initialization — via `Board.new` → `:reset()` → `:seed()`. + +### Shifting and merging + +The core idea: transform one **line** (row/column), then place it back into the grid. This keeps code consistent across all directions. + +```lua +-- board.lua (fragment) +local function slide_line(line) + local merged, res, last = {}, {}, nil + for _, v in ipairs(line) do + if v ~= 0 then + local m = last and last == v and not merged[#res] + if m then + res[#res] = v * 2; merged[#res] = true; last = nil + else + res[#res + 1] = v; merged[#res] = false; last = v + end + end + end + for i = #res + 1, #line do res[i] = 0 end + return res +end +``` + +Why this way? + +* Scan left-to-right (or top-to-bottom), **compressing** non-zero tiles. +* Merge only once per position (tracked with `merged[#res]`). +* Fill with zeros at the end to keep the line length. + +For reverse directions we use `reverse(slide_line(reverse(...)))` — short and avoids duplication. + +### Checking game over + +Two cases: there are empty cells, or merges are possible. If neither, it’s over. + +```lua +function Board:isGameOver() + return not self:has_empty() and not self:has_merge() +end +``` + +--- + +## Drawing + +`board.lua` also contains the “view” — convenient for a small teaching project: easy to read and modify. + +* Board background — soft rectangle. +* Cells — rounded rectangles; color depends on value. +* Numbers are centered with `G.printf` and slight vertical shift. + +```lua +-- board.lua (fragment) +function Board:_draw_cell(r, c) + local G, size = love.graphics, self._cell + local x = self._dx + (c - 1) * size + local y = self._dy + (r - 1) * size + local val = self.grid[r][c] + local color = (val == 0) and {0.9, 0.85, 0.7} + or {0.9, 0.7 - (val / 2048) * 0.6, 0.3} + G.setColor(color) + G.rectangle("fill", x, y, size - 4, size - 4, 6, 6) + if val ~= 0 then + G.setColor(0.1, 0.1, 0.1) + G.printf(tostring(val), x, y + size/2 - 12, size - 4, "center") + end +end +``` + +## Changing board size / tile size + +* Grid size is set when creating: `Board.new(4, 4)`. + Example: `Board.new(5, 5)` — everything else adapts. +* Cell size is controlled in `Board:_begin_draw(x, y, cell)` — + the third parameter `cell` (see `board:draw(40, 80, 80)` in `love.draw`). + +--- + +## Adding utilities + +`utils.lua` is currently a placeholder — deliberately empty to avoid clutter in the base teaching example. If needed, put things here like: + +* `clamp(v, lo, hi)` — clamp value +* `copy2d(grid)` — copy 2D array +* `any(t, pred)` — “does any element match predicate?” + +Keep each function **short** and **self-contained**. + + diff --git a/src/examples/2048-1-version/board.lua b/src/examples/2048-1-version/board.lua new file mode 100644 index 00000000..fede6364 --- /dev/null +++ b/src/examples/2048-1-version/board.lua @@ -0,0 +1,226 @@ +-- board.lua — 2048 + +local Board = {} +Board.__index = Board + +-- RNG: prefer love.math.random, fallback to math.random +local rnd = (love and love.math and love.math.random) + or math.random + +-- ===== Graphics shim (if love.graphics is missing) ===== +local function make_gshim() + local t = {} + function t.setColor() end + function t.rectangle() end + function t.print() end + function t.getFont() return nil end + return t +end + +local function Gfx() + return (love and love.graphics) or make_gshim() +end + +-- Center text without depending on printf +local function draw_center(s, x, y, w) + local G = Gfx() + local f = G.getFont and G.getFont() + local tw = (f and f.getWidth) and f:getWidth(s) or 0 + local tx = x + (w - tw) / 2 + G.print(s, tx, y) +end + +-- ===== Helpers ===== +local function reverse(t) + local out = {} + for i = #t, 1, -1 do out[#out + 1] = t[i] end + return out +end + +-- Compress a line and merge equal neighbors once +local function slide_line(line) + local merged, res, last = {}, {}, nil + for _, v in ipairs(line) do + if v ~= 0 then + local m = last and last == v and not merged[#res] + if m then + res[#res] = v * 2; merged[#res] = true; last = nil + else + res[#res + 1] = v; merged[#res] = false; last = v + end + end + end + for i = #res + 1, #line do res[i] = 0 end + return res +end + +-- ===== Construction / state ===== +function Board.new(rows, cols) + local self = setmetatable({}, Board) + self.rows, self.cols = rows, cols + self._cell, self._dx, self._dy = 48, 0, 0 + self:seed() + return self +end + +function Board:clear() + self.grid = {} + for r = 1, self.rows do + self.grid[r] = {} + for c = 1, self.cols do self.grid[r][c] = 0 end + end +end + +function Board:seed() + self:clear() + self:add_random_tile() + self:add_random_tile() +end + +function Board:empty_cells() + local out = {} + for r = 1, self.rows do + for c = 1, self.cols do + if self.grid[r][c] == 0 then + out[#out + 1] = { r = r, c = c } + end + end + end + return out +end + +function Board:add_random_tile() + local empty = self:empty_cells() + if #empty == 0 then return end + local i = rnd(1, #empty) + local cell = empty[i] + self.grid[cell.r][cell.c] = (rnd() < 0.9) and 2 or 4 +end + +-- ===== Moves (rows/cols reuse slide_line) ===== +function Board:move_row_left(r) + local row = {} + for c = 1, self.cols do row[c] = self.grid[r][c] end + local slid = slide_line(row) + local moved = false + for c = 1, self.cols do + if self.grid[r][c] ~= slid[c] then moved = true end + self.grid[r][c] = slid[c] + end + return moved +end + +function Board:move_row_right(r) + local row = {} + for c = 1, self.cols do row[c] = self.grid[r][c] end + local slid = reverse(slide_line(reverse(row))) + local moved = false + for c = 1, self.cols do + if self.grid[r][c] ~= slid[c] then moved = true end + self.grid[r][c] = slid[c] + end + return moved +end + +function Board:move_col_up(c) + local col = {} + for r = 1, self.rows do col[r] = self.grid[r][c] end + local slid = slide_line(col) + local moved = false + for r = 1, self.rows do + if self.grid[r][c] ~= slid[r] then moved = true end + self.grid[r][c] = slid[r] + end + return moved +end + +function Board:move_col_down(c) + local col = {} + for r = 1, self.rows do col[r] = self.grid[r][c] end + local slid = reverse(slide_line(reverse(col))) + local moved = false + for r = 1, self.rows do + if self.grid[r][c] ~= slid[r] then moved = true end + self.grid[r][c] = slid[r] + end + return moved +end + +-- Single entry: map direction → line mover +function Board:move(dir) + local map = { + left = { n = self.rows, f = self.move_row_left }, + right = { n = self.rows, f = self.move_row_right }, + up = { n = self.cols, f = self.move_col_up }, + down = { n = self.cols, f = self.move_col_down }, + } + local noop = function() return false end + local m = map[dir] or { n = 0, f = noop } + local moved + for i = 1, m.n do moved = m.f(self, i) or moved end + return moved or false +end + +-- ===== End conditions ===== +function Board:has_empty() + for r = 1, self.rows do + for c = 1, self.cols do + if self.grid[r][c] == 0 then return true end + end + end + return false +end + +function Board:has_merge() + for r = 1, self.rows do + for c = 1, self.cols do + local v = self.grid[r][c] + if (c < self.cols and v == self.grid[r][c + 1]) or + (r < self.rows and v == self.grid[r + 1][c]) then + return true + end + end + end + return false +end + +function Board:isGameOver() + return not self:has_empty() and not self:has_merge() +end + +-- ===== Drawing ===== +function Board:_begin_draw(x, y, cell) + self._cell = cell or self._cell + self._dx, self._dy = x or 0, y or 0 +end + +function Board:_draw_cell(r, c) + local G, size = Gfx(), self._cell + local x = self._dx + (c - 1) * size + local y = self._dy + (r - 1) * size + local val = self.grid[r][c] + local color = (val == 0) and {0.9, 0.85, 0.7} + or {0.9, 0.7 - (val / 2048) * 0.6, 0.3} + G.setColor(color) + G.rectangle("fill", x, y, size - 4, size - 4) + if val ~= 0 then + G.setColor(0.1, 0.1, 0.1) + draw_center(tostring(val), x, y + size/2 - 12, size - 4) + end +end + +function Board:draw(x, y, cell) + local G = Gfx() + self:_begin_draw(x, y, cell) + G.setColor(0.8, 0.7, 0.5) + G.rectangle( + "fill", x - 5, y - 5, + cell * self.cols + 10, cell * self.rows + 10 + ) + for r = 1, self.rows do + for c = 1, self.cols do self:_draw_cell(r, c) end + end +end + +_G.Board = Board +return Board diff --git a/src/examples/2048-1-version/main.lua b/src/examples/2048-1-version/main.lua new file mode 100644 index 00000000..5f7d5e41 --- /dev/null +++ b/src/examples/2048-1-version/main.lua @@ -0,0 +1,40 @@ + +-- main.lua — Compy-style glue (global Board, no setMode) + +local G = love and love.graphics or nil + +-- Load board; board.lua sets _G.Board +require("board") + +-- Create game board (rows, cols) +board = Board.new(4, 4) + +-- Keyboard input: arrows/WASD, R restart, Esc quit +function love.keypressed(key) + local map = { + left="left", right="right", up="up", down="down", + a="left", d="right", w="up", s="down", + } + if key == "escape" and love and love.event and love.event.quit then + love.event.quit() + return + end + local dir = map[key] + if dir then + if board:move(dir) then board:add_random_tile() end + elseif key == "r" then + board:seed() + end +end + +-- Draw board and a tiny HUD +function love.draw() + if not G then return end + board:draw(40, 80, 80) + G.setColor(0.2, 0.2, 0.2) + G.print("ESC quit | R restart", 110, 30) + if board:isGameOver() then + G.setColor(1, 0, 0) + G.print("GAME OVER!", 140, 440, 0, 2, 2) + end +end diff --git a/src/examples/2048-1-version/utils.lua b/src/examples/2048-1-version/utils.lua new file mode 100644 index 00000000..1ec48ff7 --- /dev/null +++ b/src/examples/2048-1-version/utils.lua @@ -0,0 +1,3 @@ +-- utils.lua — placeholder for future helpers + +return {} diff --git a/src/examples/clock/README.md b/src/examples/clock/README.md index 0ab14f54..f504bcb0 100644 --- a/src/examples/clock/README.md +++ b/src/examples/clock/README.md @@ -1,93 +1,90 @@ ### Clock -In this example, we explore how to properly create a program with it's own drawing function. Additionally, we will query the underlying system for the current time and date. +Clock (Compy-style) -#### Overriding `love.draw()` +A tiny digital clock that demonstrates clear naming, +short functions, and predictable formatting so the built-in +editor won’t change a thing. -We will be taking over the screen drawing for this simple game. -Similar to `update()`, we can override the `love.draw()` function and have the LOVE2D framework handle displaying the content we wish. -Drawing generally follows a simple procedure: set up some values, such as what foreground and background color to use, then build up our desired image using basic elements. These are called graphics "primitives", and we can access them from the `love.graphics` table (aliased here as `G`). +This version removes globals, avoids complex inline math, and keeps +nesting shallow. It’s meant as a didactic example you can copy and +extend. -So, our example clock: -```lua -function love.draw() - G.setColor(Color[color + Color.bright]) - G.setBackgroundColor(Color[bg_color]) - G.setFont(font) +How it works - local text = getTimestamp() - local off_x = font:getWidth(text) / 2 - local off_y = font:getHeight() / 2 - G.print(text, midx - off_x, midy - off_y) -end -``` -Let's see this step-by-step. -In the first half, we set up the properties: the colors and the font. Strictly speaking, the font could be moved out of here, because it will not change through the run, but the player is able to change the background and foreground colors, so these may be different between calls of the `draw()` function. -Having done this, we need to _provide_ the text being displayed (`getTimestamp()` function, discussed later), then position it center screen. This requires some thinking on our part. -The way the `print` helper, and most graphics helpers work, is by starting drawing at the coordinates provided, but these coordinates will point to the leftmost and topmost (remember, x grows downwards in screen coordinates) corner of the element being displayed. -So, we need to determine the half-width and half-height of our text object to correctly draw it at the center. To do this, we can use the `getWidth()` and `getHeight()` helpers. -We determined the midpoint of the screen earlier: -```lua -width, height = G.getDimensions() -midx = width / 2 -midy = height / 2 -``` -Armed with this, we can draw the time dead center: -```lua -G.print(text, midx - off_x, midy - off_y) -``` - -#### Getting the timestamp - -To keep time, we'll do two things: first, ask what it is currently, then increment the value every second. - -First, declare some variables and constants: -```lua -local M = 60 -- seconds in a minute, minutes in an hour -local H = m * m -- seconds in an hour -local D = 24 -- hours in a day - -local H, M, S, t -``` -Uppercase names refer to ratios (which are constants), lowercase ones to variable value of hours, minutes, seconds. Keeping to this convention aids readability. - -```lua -function setTime() - local time = os.date("*t") - h = time.hour - m = time.min - s = time.sec - t = s + M * m + H * h -end -``` -Reading the current time is achieved by using `os.date()`, which unlike `os.time()`, allows us to specify the format of the resulting string. We want to achieve the end result of "hour:minute"second", which we could get with the format string "%H:%M:%S", but we also need these intermediate values separately, to keep time. Instead, if the special format string "*t" is passed to the function, it will return a table with the parts of the timestamp instead of a string. +We read the current time once and store it in a local state table. -#### Timekeeping +Each frame, we add dt to the accumulator. -Using the update function, we can keep track of time elapsed: -```lua -function love.update(dt) - t = t + dt -end -``` -Going back from the number of seconds to a human-readable time will require some division: -```lua -local function pad(i) - return string.format("%02d", i) -end +We derive HH:MM:SS via simple, readable steps (no dense one-liners). + +We draw the timestamp centered on screen using a fixed font size. + +Controls + +Space — cycle foreground color. + +Shift + Space — cycle background color. + +Shift + R — reset to the current system time. + +P — pause hook (provided by the host runtime). + +Note: Color[...], Color.bright, and pause(...) are assumed to be +provided by the host environment. + +Style guide we follow (Compy) + +Max line length: 64 characters. + +Max function length: 14 lines. + +Max args per function: 4. + +Max nesting depth: 4. + +Avoid complex inline expressions: compute in steps. + +No magic numbers: name constants. + +Prefer local state: no globals, no lint suppressions. + +Comments: brief and didactic (explain “why”, not “what”). + +Key ideas, by example: +Centered draw with local state +-- Colors, background, and font come from local state S. +local text = make_timestamp(S.t) +local off_x = S.font:getWidth(text) / 2 +local off_y = S.font:getHeight() / 2 +G.print(text, MID_X - off_x, MID_Y - off_y) + +Why this is “Compy-clean”: + + - short lines (≤ 64 chars); + + - no nested calls inside print; + + - variables have clear roles (off_x, off_y); + + - state is local (S.font, S.t) + + Time math in small steps: + local function make_timestamp(tt) + local hours_raw = math.floor(tt / HOURS_IN_T) + local hours = math.fmod(hours_raw, DAY_HOURS) + + local mins_raw = math.floor(tt / TICKS) + local minutes = math.fmod(mins_raw, TICKS) + + local seconds = math.fmod(math.floor(tt), TICKS) + + local hh = string.format("%02d", hours) + local mm = string.format("%02d", minutes) + local ss = string.format("%02d", seconds) -function getTimestamp() - local hours = pad(math.fmod((t / H), D)) - local minutes = pad(math.fmod((t / M), M)) - local seconds = pad(math.fmod(t, M)) - return string.format("%s:%s:%s", hours, minutes, seconds) + return string.format("%s:%s:%s", hh, mm, ss) end -``` -Quick aside on format strings: for a digital clock, we usually want to always have the numbers displayed as two digits, with colons in between them. To achieve this, we `pad` all our results, then stitch them together with `string.format()`. -In the above code, we are doing two different kinds of division.First, an integer division (`/`), going from seconds to minutes and hours, in this case we are only interested in the whole numbers, for example: 143 seconds is two full minutes and then some, but what the clock will be displaying is '02', so the remaining 23 seconds is not interesing for the minutes part. -However, for 143 minutes, the display _should_ say 23, disregarding the two full hours, we are only interested in the remainder part. We can get this value by using `math.fmod()`, in this example, `math.fmod(143, 60)`. -#### User documentation -This program displays the current time in a randomly selected color over a randomly selected background. These colors can be changed by pressing [Space] and [Shift-Space], respectively. -Should the clock deviate from the correct time (for example, because the program run was paused), it can be reset with the [R] key. \ No newline at end of file +No dense one-liners, no hidden precedence: everything reads like a recipe. \ No newline at end of file diff --git a/src/examples/clock/main.lua b/src/examples/clock/main.lua index 2ec69534..1d5e5c19 100644 --- a/src/examples/clock/main.lua +++ b/src/examples/clock/main.lua @@ -1,79 +1,107 @@ +-- main.lua +-- Simple digital clock with small, readable helpers. +-- Goals: clear names, short lines, low nesting, no globals. + local G = love.graphics -width, height = G.getDimensions() -midx = width / 2 -midy = height / 2 - -local M = 60 -local H = M * M -local D = 24 - -local h, m, s, t -function setTime() - local time = os.date("*t") - h = time.hour - m = time.min - s = time.sec - t = s + M * m + H * h -end +-- Constants for clarity. +local TICKS = 60 -- "seconds" per minute +local HOURS_IN_T = TICKS * TICKS +local DAY_HOURS = 24 +local MAX_COLORS = 7 +local FONT_SIZE = 144 -setTime() +-- Canvas geometry. +local W, H = G.getDimensions() +local MID_X = W / 2 +local MID_Y = H / 2 -math.randomseed(os.time()) -color = math.random(7) -bg_color = math.random(7) -font = G.newFont(144) +-- Local state container. +local S = { + t = 0, -- time accumulator + color = 1, -- foreground palette index + bg = 1, -- background palette index + font = G.newFont(FONT_SIZE) +} -local function pad(i) +-- Utilities kept tiny and didactic. +local function pad2(i) return string.format("%02d", i) end -function getTimestamp() - local hours = pad(math.fmod((t / H), D)) - local minutes = pad(math.fmod((t / M), M)) - local seconds = pad(math.fmod(t, M)) - return string.format("%s:%s:%s", hours, minutes, seconds) +local function cycle(i) + if i > MAX_COLORS then + return 1 + end + return i + 1 end -function love.draw() - G.setColor(Color[color + Color.bright]) - G.setBackgroundColor(Color[bg_color]) - G.setFont(font) - local text = getTimestamp() - local off_x = font:getWidth(text) / 2 - local off_y = font:getHeight() / 2 - G.print(text, midx - off_x, midy - off_y) +local function is_shift() + return love.keyboard.isDown("lshift", "rshift") end -function love.update(dt) - t = t + dt +-- Set "wall-clock" into S.t using os.date. +local function set_time_now() + local tm = os.date("*t") + local h = tm.hour + local m = tm.min + local s = tm.sec + S.t = s + TICKS * m + HOURS_IN_T * h end -function cycle(c) - if 7 < c then - return 1 +set_time_now() + +-- Build HH:MM:SS string from S.t using simple steps. +local function make_timestamp(tt) + local hours_raw = math.floor(tt / HOURS_IN_T) + local hours = math.fmod(hours_raw, DAY_HOURS) + + local mins_raw = math.floor(tt / TICKS) + local minutes = math.fmod(mins_raw, TICKS) + + local seconds = math.fmod(math.floor(tt), TICKS) + + local hh = pad2(hours) + local mm = pad2(minutes) + local ss = pad2(seconds) + + return string.format("%s:%s:%s", hh, mm, ss) +end + +-- Handle color cycling on space; Shift+Space flips background. +local function on_color_key() + if is_shift() then + S.bg = cycle(S.bg) + else + S.color = cycle(S.color) end - return c + 1 end -local function shift() - return love.keyboard.isDown("lshift", "rshift") +-- LÖVE callbacks kept flat and short. +function love.update(dt) + S.t = S.t + dt end -local function color_cycle(k) - if k == "space" then - if shift() then - bg_color = cycle(bg_color) - else - color = cycle(color) - end - end + +function love.draw() + -- Color table `Color` is assumed provided by the runtime. + G.setColor(Color[S.color + Color.bright]) + G.setBackgroundColor(Color[S.bg]) + G.setFont(S.font) + + local text = make_timestamp(S.t) + local off_x = S.font:getWidth(text) / 2 + local off_y = S.font:getHeight() / 2 + + G.print(text, MID_X - off_x, MID_Y - off_y) end + function love.keyreleased(k) - color_cycle(k) - if k == "r" and shift() then - setTime() - end - if k == "p" then + if k == "space" then + on_color_key() + elseif k == "r" and is_shift() then + set_time_now() + elseif k == "p" then + -- `pause` is assumed provided by the host environment. pause("STOP THE CLOCKS!") end end diff --git a/src/examples/guess/README.md b/src/examples/guess/README.md index 72fdb286..58b84f44 100644 --- a/src/examples/guess/README.md +++ b/src/examples/guess/README.md @@ -1,33 +1,98 @@ -### Guess - -This simple game showcases the facilities lua offers for random number generation. -It's the classic setup, where the computer "thinks" of a number, and the user is supposed to guess which one it is, the feedback offered being whether the guess was higher or lower than the target number. -If the value is correctly guessed, the game restarts with a new round. - -#### Initialization - -At the start, there are two steps of initializing. This is because part of it only needs to happen once, while the other part has to run at the start of every turn. - -Running once: -```lua --- initialize randomgenerator -math.randomseed(os.time()) --- set range -N = 100 --- declare the variable holding the target value -ntg = 0 -``` -Generating truly random numbers without outside input, turns out, is not an easy problem. In less critical use cases, like games, so-called pseudorandom numbers will do. For that to work, the generator needs to be initialized with a *seed*. This is not to say that it will not spit out arbitrary-looking values for us if we don't do this, but they would be the same between different runs of the program. -To use a different seed each time, a simple way is to just feed in the current time, which we can get from the system clock with `os.time()`. The details of how this works are irrelevant here, the important part is that each time it's a different value, so we'll get a different stream of pseudorandom numbers. - -Running every time: -```lua -function init() - ntg = math.random(N) +Guess (Compy-style) + +A tiny “guess the number” game that demonstrates clear naming, +small functions, and predictable formatting so the built-in +editor won’t touch it. + + + +What the game does + +The program picks a hidden number between 1 and MAX_NUM. + +You enter guesses; the game says higher / lower. + +On a correct guess, it starts a new round automatically. + +Controls & I/O + +Input is read via the provided user_input()/validated_input(...). + +All messages are printed with print(...). + +No extra keys required. + +Note: the host environment is expected to provide +user_input() and validated_input(...). + +Initialization + +We seed the random generator once (for variety across runs) and start +a new round. Both steps live in small helpers so they’re easy to reuse. + +local MAX_NUM = 100 + +local S = { + input = user_input(), + target = 0 +} + +local function init_game() + print("Welcome to the guessing game!") + math.randomseed(os.time()) + S.target = math.random(MAX_NUM) end -``` -Once we have the seed set up, we can get a new random value between 1 and N by calling `math.random(N)`. -#### Gameplay +init_game() + +Input & validation + +Validation is explicit and didactic: parse, range-check, integer-check. +No dense one-liners, no magic. +local function parse_positive_int(s) + local n = tonumber(s) + if not n then + return false, "Not a number" + end + if n <= 0 then + return false, "Not a positive number" + end + if math.floor(n) ~= n then + return false, "Not an integer" + end + return true, n +end + + +Game loop + +Short and flat: read input, validate, compare, and either hint or reset. +local function check_guess(n) + if not n then + return + end + if S.target < n then + print("The number is lower") + elseif n < S.target then + print("The number is higher") + else + print("Correct!") + print("") + init_game() + end +end + +function love.update() + if S.input:is_empty() then + validated_input({ parse_positive_int }, "Guess a number:") + else + local s = S.input() + local ok, v = parse_positive_int(s) + if ok then + check_guess(v) + else + print(v) -- error message + end + end +end -The rest is simple, continually prompt with a validated input which only accepts whole numbers, and determine where it stands in relation to the one being guessed at. \ No newline at end of file diff --git a/src/examples/guess/main.lua b/src/examples/guess/main.lua index b32e621d..d866a749 100755 --- a/src/examples/guess/main.lua +++ b/src/examples/guess/main.lua @@ -1,59 +1,66 @@ -math.randomseed(os.time()) -r = user_input() -N = 100 --- number_to_guess -ntg = 0 - -function init() - print("Welcome to the guessing game!") - ntg = math.random(N) +-- main.lua +-- Guess-the-number: clear, didactic, Compy-friendly. + +local MAX_NUM = 100 + +-- Local state, no globals. +local S = { + input = user_input(), + target = 0 +} + +local function say(msg) + print(msg) +end + +local function init_game() + say("Welcome to the guessing game!") + math.randomseed(os.time()) + S.target = math.random(MAX_NUM) end -function is_natural(s) +-- Accepts a string; returns (ok, n or err_msg) +local function parse_positive_int(s) local n = tonumber(s) if not n then - return false, "NaN" + return false, "Not a number" end if n <= 0 then - return false, "Not a positive number!" + return false, "Not a positive number" end if math.floor(n) ~= n then - return false, "Not an integer!" - end - return true -end - -function is_natural(s) - local digits = string.usub(s, 1) - local ok, err_c = string.forall(digits, Char.is_digit) - if ok then - return true + return false, "Not an integer" end - return false, Error("The guess should be a positive number", err_c) + return true, n end -function check(n) +local function check_guess(n) if not n then return end - if ntg < n then - print("The number is lower") - elseif n < ntg then - print("The number is higher") + if S.target < n then + say("The number is lower") + elseif n < S.target then + say("The number is higher") else - print("Correct!") - print("\n\n") - init() + say("Correct!") + say("") + init_game() end end function love.update() - if r:is_empty() then - validated_input({ is_natural }, "Guess a number:") + if S.input:is_empty() then + validated_input({ parse_positive_int }, "Guess a number:") else - local n = tonumber(r()) - check(n) + local s = S.input() + local ok, val_or_err = parse_positive_int(s) + if ok then + check_guess(val_or_err) + else + say(val_or_err) + end end end -init() +init_game() diff --git a/src/examples/life/README.md b/src/examples/life/README.md index 17e43eec..d96a663e 100644 --- a/src/examples/life/README.md +++ b/src/examples/life/README.md @@ -1,259 +1,190 @@ -## Life +Life -This is a simple Game of Life implementation, which is maybe not the first, but certainly the best known zero-player computer game. -The game field is a two-dimensional array (grid), where each **cell** (block, square) is either _alive_ or _dead_ (in programming parlance, `1` or `0`). -Given the initial state and a few simple rules, we are simulatiing the life of these cells. +This is a simple Game of Life implementation — the best-known zero-player computer game. +The field is a 2D grid where each cell is either alive (1) or dead (0). +Given an initial state and a few rules, we simulate the evolution of the cells. -### Screen size +Screen size -We have used the screen size before, but only in a very limited capacity, to determine where the middle is. This time, however, the whole game depends on how much pixels we have available. +Unlike earlier demos where we only needed the center, here the whole game depends on how many pixels we have. +local G = love.graphics +local CELL = 10 -```lua -cell_size = 10 -screen_w, screen_h = G.getDimensions() -grid_w = screen_w / cell_size -grid_h = screen_h / cell_size -``` +local S = { w = 0, h = 0, gw = 0, gh = 0 } + +local function init_dims() + S.w, S.h = G.getDimensions() + S.gw = math.floor(S.w / CELL) + S.gh = math.floor(S.h / CELL) +end -`getDimensions()` gives us the width and height of the screen, which we will divide by some scaling factor (say 10), resulting in a grid of 10-by-10 cells. -### Setup +getDimensions() returns the screen width/height. We divide by CELL to get a grid size in cells. -Starting up, generate random values for cell states: +Setup -```lua -local function initializeGrid() - for x = 1, grid_w do - grid[x] = {} - for y = 1, grid_h do - -- Initialize with some random live cells - grid[x][y] = math.random() > 0.7 and 1 or 0 +At startup, we generate a random initial pattern. All state is local, stored in S +local function clear_grid(dst) + for x = 1, S.gw do + dst[x] = dst[x] or {} + for y = 1, S.gh do + dst[x][y] = 0 end end end -``` - -### Simulation - -Then, at each step, we apply the following rules: -* Any live cell with fewer than two live neighbours dies -* Any live cell with two or three live neighbours lives on -* Any live cell with more than three live neighbours dies -* Any dead cell with exactly three live neighbours becomes a live cell - -(excerpt from `updateGrid()`) -```lua - local neighbors = countAliveNeighbors(x, y) - if grid[x][y] == 1 then - newGrid[x][y] = - (neighbors == 2 or neighbors == 3) and 1 or 0 - else - newGrid[x][y] = (neighbors == 3) and 1 or 0 + +local function init_grid() + S.grid, S.next = {}, {} + clear_grid(S.grid) + clear_grid(S.next) + for x = 1, S.gw do + for y = 1, S.gh do + S.grid[x][y] = (math.random() < 0.3) and 1 or 0 + end end -``` +end -We could just update the grid state on every `update()`, but that would mean the simulation has no consistent pace, it's going as fast as the hardware can crank update calls out, varying between devices. -Instead, we will create a simple timer. The timer is keeping track of elapsed time, and producing "ticks" inversely proportional to a set simulation speed. -On each tick, the grid updates and timer resets. -```lua -time = 0 -speed = 10 +Simulation -tick = function() - if time > (1 / speed) then - time = 0 - return true - end -end +On each step we apply Conway’s rules: -function love.update(dt) - time = time + dt - if tick() then - updateGrid() - end -end -``` +A live cell with fewer than 2 live neighbours dies. -### Controls +A live cell with 2 or 3 live neighbours lives on. -Being a zero-player game, there's not much to do in the user interaction department. Still, we would like to add the ability to start a new simulation without restarting the whole run, and a knob to adjust the simulation speed. +A live cell with more than 3 live neighbours dies. -```lua -function changeSpeed(d) - if not d then return end - if d < 0 and 1 < speed then - speed = speed - 1 - end - if 0 < d and speed < 99 then - speed = speed + 1 +A dead cell with exactly 3 live neighbours becomes alive. +local function alive_neighbors(x, y) + local c = 0 + for dx = -1, 1 do + for dy = -1, 1 do + if not (dx == 0 and dy == 0) then + c = c + get_cell(x + dx, y + dy) + end + end end + return c end -function love.keypressed(k) - if k == "r" then - init() - end - if k == "-" then - changeSpeed(-1) - end - if k == "+" or k == "=" then - changeSpeed(1) +local function step_grid() + for x = 1, S.gw do + for y = 1, S.gh do + local n = alive_neighbors(x, y) + local v = S.grid[x][y] + if v == 1 then + S.next[x][y] = (n == 2 or n == 3) and 1 or 0 + else + S.next[x][y] = (n == 3) and 1 or 0 + end + end end + S.grid, S.next = S.next, S.grid end -``` -Simple and straightforward on the keyboard. However... -### Touch +We don’t update every update() call, because frame rates differ between devices. +Instead we keep a simple timer and step the grid at a controlled pace: +local SPEED_MIN, SPEED_MAX = 1, 99 +local function clamp(v, lo, hi) + if v < lo then return lo end + if hi < v then return hi end + return v +end -Until now, we had no concern about running our games outside the environment they are being developed in, so let's give this some thought. -In development, we have a keyboard and mouse, but people these days mostly use a smartphone or tablet as their first (and possibly only) choice. -Therefore, it would be useful to support touchscreens as an input method, too. -Fortunately, LOVE2D is helpful in this regard, as it also fires a mouse event, even if when the interaction was a tap on a touchscreen. This means that to support single touch, we don't have to do anything that different, other than paying attention to the limitations when designing the game controls. +-- S.speed = steps per second; S.t = accumulator +local function try_step(dt) + S.t = S.t + dt + local need = 1 / clamp(S.speed, SPEED_MIN, SPEED_MAX) + if S.t >= need then + S.t = S.t - need + step_grid() + end +end -#### Reset -What's a simple keystroke, is a little more involved with the mouse/touchscreen. -Any accidental tap should not trigger it, so we need to keep track of the time elapsed while the button or finger is held in the `update` function: +Controls -```lua -hold_time = 0 -function love.mousepressed(_, y, button) - if button == 1 then - if love.mouse.isDown(1) then - hold_time = hold_time + dt - end - end +This is a zero-player game, but we still want to reset the board and tweak the simulation speed. +local function change_speed(d) + if not d then return end + S.speed = clamp(S.speed + d, SPEED_MIN, SPEED_MAX) end -``` -When the same button is released, we check if it was held long enough. -Either way, reset the timer. - -```lua -function love.mousereleased(_, y, button) - if button == 1 then - mouse_held = false - if reset_time < hold_time then - init() - end - hold_time = 0 +function love.keypressed(k) + ensure_init() + if k == "r" then + init_grid() + elseif k == "-" then + change_speed(-1) + elseif k == "+" or k == "=" then + change_speed(1) end end -``` -#### Speed -How to handle speeding up and down? We could split the screen up, and say that tapping the top half increases speed, while the bottom half decreases it: +Touch -```lua -function love.mousereleased(_, y, button) - if button == 1 then - mouse_held = false - if reset_time < hold_time then - init() - else - if y < mid_y then - changeSpeed(1) - else - changeSpeed(-1) - end - end - hold_time = 0 - end -end -``` +Modern devices are often touch-only. LOVE2D helps by emitting mouse events for taps, so single-touch support is straightforward. -(Remember, the y coordinate grows from the top of the screen towards the bottom) -This is a fine solution, but we can make it more interesting. Some videoplayer apps have a user experience where if you drag your finger on the screen, it adjusts the volume or the brightness, depending on direction. That sounds more interesting, let's implement it! +Reset (long press) -For a first approximation, try the `mousemoved` handler: -```lua -g_dir = nil +We record how long the press lasted in update() and trigger a reset when it exceeds a threshold. +local HOLD_RST = 1 -- seconds -function love.mousemoved(_, _, _, dy) +function love.update(dt) + ensure_init() if love.mouse.isDown(1) then - if dy < 0 then - g_dir = 1 - elseif dy > 0 then - g_dir = -1 - end + S.hold_dt = S.hold_dt + dt end + try_step(dt) end -``` -This sets the pull direction while holding a click or tap. Then at release, change the speed accordingly: -```lua -function love.mousereleased(_, _, button) - if button == 1 then - mouse_held = false - if hold_time > 1 then - init() - elseif g_dir then - changeSpeed(g_dir) - end - hold_time = 0 - end -end -``` +On release we either reset or interpret the gesture for speed control: +local EPSILON = 3 -- pixels -There's one problem with this approach: all taps will change the speed if there's even a miniscule difference between press and release. We should introduce some kind of treshold for the number of pixels the difference needs to be for it to count as a purposeful gesture. -Scrapping our first approach, we'll do something similar to the long tap: record the position on press... - -```lua -hold_y = nil function love.mousepressed(_, y, button) + ensure_init() if button == 1 then - mouse_held = true - hold_y = y + S.hold_y = y + S.hold_dt = 0 end end -``` - -...and compare it when it's released: -```lua -epsilon = 3 function love.mousereleased(_, y, button) - if button == 1 then - mouse_held = false - if reset_time < hold_time then - init() - else - if hold_y then - local dy = hold_y - y - if math.abs(dy) > epsilon then - changeSpeed(dy) - end + ensure_init() + if button ~= 1 then return end + if S.hold_dt >= HOLD_RST then + init_grid() + else + if S.hold_y then + local dy = S.hold_y - y + if math.abs(dy) > EPSILON then + change_speed(dy) -- drag up = faster, down = slower end end - hold_y = nil - hold_time = 0 end + S.hold_y = nil + S.hold_dt = 0 end -``` - -As you can see, I have determined that the difference should be at least 3 pixels, regardless of the sign (hence the `maths.abs()`). -### Help text -With all that done, add some explanations for our users, and call it a day. -Note the calculations we need to make so it shows up relative to the bottom of the screen: +Help text -```lua -margin = 5 +We draw a small overlay near the bottom edge. Font and sizes are kept in S. +local MARGIN = 5 -function drawHelp() - local bottom = screen_h - margin - local right_edge = screen_w - margin +local function draw_help() + local bottom = S.h - MARGIN + local right = S.w - MARGIN local reset_msg = "Reset: [r] key or long press" - local speed_msg = "Set speed: [+]/[-] key or drag up/down" - G.print(reset_msg, margin, (bottom - fh) - fh) - G.print(speed_msg, margin, bottom - fh) - local speed_label = string.format("Speed: %02d", speed) - local label_w = font:getWidth(speed_label) - G.print(speed_label, right_edge - label_w, bottom - fh) + local speed_msg = "Speed: [+]/[-] or drag up/down" + G.print(reset_msg, MARGIN, (bottom - S.fh) - S.fh) + G.print(speed_msg, MARGIN, bottom - S.fh) + local label = string.format("Speed: %02d", S.speed) + local lw = S.font:getWidth(label) + G.print(label, right - lw, bottom - S.fh) end -``` + diff --git a/src/examples/life/main.lua b/src/examples/life/main.lua index 74b42f7a..fbd67e1d 100644 --- a/src/examples/life/main.lua +++ b/src/examples/life/main.lua @@ -1,184 +1,202 @@ ---- original from https://github.com/Aethelios/Conway-s-Game-of-Life-in-Lua-and-Love2D - -G = love.graphics -G.setFont(font) -fh = font:getHeight() - -cell_size = 10 -margin = 5 -screen_w, screen_h = G.getDimensions() -grid_w = screen_w / cell_size -grid_h = screen_h / cell_size -grid = {} - -mouse_held = false -hold_y = nil -hold_time = 0 -speed = 10 -time = 0 -epsilon = 3 -reset_time = 1 - -tick = function() - if time > (1 / speed) then - time = 0 - return true +-- main.lua +-- Game of Life — Compy-friendly. + +local G = love.graphics + +-- ---------- constants ---------- +local CELL = 10 +local MARGIN = 5 +local SPEED_MIN = 1 +local SPEED_MAX = 99 +local EPSILON = 3 +local HOLD_RST = 1 + +-- ---------- local state ---------- +local S = { + inited = false, + w = 0, h = 0, + gw = 0, gh = 0, + grid = {}, next = {}, + speed = 10, t = 0, + hold_y = nil, hold_dt = 0, + font = nil, fh = 0 +} + +-- ---------- helpers ---------- +local function clamp(v, lo, hi) + if v < lo then return lo end + if hi < v then return hi end + return v +end + +local function init_dims() + S.w, S.h = G.getDimensions() + S.gw = math.floor(S.w / CELL) + S.gh = math.floor(S.h / CELL) +end + +local function new_font() + S.font = G.newFont(14) + S.fh = S.font:getHeight() + G.setFont(S.font) +end + +local function clear_grid(dst) + for x = 1, S.gw do + dst[x] = dst[x] or {} + for y = 1, S.gh do + dst[x][y] = 0 + end end end -function initializeGrid() - for x = 1, grid_w do - grid[x] = {} - for y = 1, grid_h do - -- Initialize with some random live cells - grid[x][y] = 0.7 < math.random() and 1 or 0 +local function init_grid() + S.grid, S.next = {}, {} + clear_grid(S.grid) + clear_grid(S.next) + for x = 1, S.gw do + for y = 1, S.gh do + local live = math.random() < 0.3 and 1 or 0 + S.grid[x][y] = live end end end -local function init() - time = 0 - initializeGrid() +local function in_bounds(x, y) + return 1 <= x and x <= S.gw and 1 <= y and y <= S.gh end -function countHelper(nx, ny) - local c = 0 - if 1 <= nx - and nx <= grid_w - and 1 <= ny - and ny <= grid_h - then - local row = grid[nx] or {} - c = c + (row[ny] or 0) - end - return c +local function get_cell(x, y) + if not in_bounds(x, y) then return 0 end + return S.grid[x][y] end -function countAliveNeighbors(x, y) - local count = 0 +local function alive_neighbors(x, y) + local c = 0 for dx = -1, 1 do for dy = -1, 1 do - if dx ~= 0 or dy ~= 0 then - local nx, ny = x + dx, y + dy - count = count + countHelper(nx, ny) + if not (dx == 0 and dy == 0) then + c = c + get_cell(x + dx, y + dy) end end end - return count -end - -local function updateGrid() - local newGrid = {} - for x = 1, grid_w do - newGrid[x] = {} - for y = 1, grid_h do - local neighbors = countAliveNeighbors(x, y) - if grid[x][y] == 1 then - newGrid[x][y] = - (neighbors == 2 or neighbors == 3) and 1 or 0 + return c +end + +local function step_grid() + for x = 1, S.gw do + for y = 1, S.gh do + local n = alive_neighbors(x, y) + local v = S.grid[x][y] + if v == 1 then + S.next[x][y] = (n == 2 or n == 3) and 1 or 0 else - newGrid[x][y] = (neighbors == 3) and 1 or 0 + S.next[x][y] = (n == 3) and 1 or 0 end end end - grid = newGrid + S.grid, S.next = S.next, S.grid end -function changeSpeed(d) - if not d then return end - if d < 0 and 1 < speed then - speed = speed - 1 - end - if 0 < d and speed < 99 then - speed = speed + 1 +local function try_step(dt) + S.t = S.t + dt + local need = 1 / clamp(S.speed, SPEED_MIN, SPEED_MAX) + if S.t >= need then + S.t = S.t - need + step_grid() end end +local function change_speed(d) + if not d then return end + S.speed = clamp(S.speed + d, SPEED_MIN, SPEED_MAX) +end + +local function draw_cell(x, y) + local px = (x - 1) * CELL + local py = (y - 1) * CELL + G.setColor(0.9, 0.9, 0.9) + G.rectangle("fill", px, py, CELL, CELL) + G.setColor(0.3, 0.3, 0.3) + G.rectangle("line", px, py, CELL, CELL) +end + +local function draw_help() + local btm = S.h - MARGIN + local right = S.w - MARGIN + local msg_r = "Reset: [r] key or long press" + local msg_s = "Speed: [+]/[-] or drag up/down" + G.print(msg_r, MARGIN, (btm - S.fh) - S.fh) + G.print(msg_s, MARGIN, btm - S.fh) + local label = string.format("Speed: %02d", S.speed) + local lw = S.font:getWidth(label) + G.print(label, right - lw, btm - S.fh) +end + +-- ---------- init hook ---------- +local function ensure_init() + if S.inited then return end + math.randomseed(os.time()) + init_dims() + new_font() + init_grid() + S.inited = true +end + +-- ---------- LÖVE callbacks ---------- function love.update(dt) - time = time + dt + ensure_init() if love.mouse.isDown(1) then - hold_time = hold_time + dt + S.hold_dt = S.hold_dt + dt end - if tick() then - updateGrid() + try_step(dt) +end + +function love.draw() + ensure_init() + for x = 1, S.gw do + for y = 1, S.gh do + if S.grid[x][y] == 1 then + draw_cell(x, y) + end + end end + G.setColor(1, 1, 1, 0.5) + draw_help() end function love.keypressed(k) + ensure_init() if k == "r" then - init() - end - if k == "-" then - changeSpeed(-1) - end - if k == "+" or k == "=" then - changeSpeed(1) + init_grid() + elseif k == "-" then + change_speed(-1) + elseif k == "+" or k == "=" then + change_speed(1) end end function love.mousepressed(_, y, button) + ensure_init() if button == 1 then - mouse_held = true - hold_y = y + S.hold_y = y + S.hold_dt = 0 end end function love.mousereleased(_, y, button) - if button == 1 then - mouse_held = false - if reset_time < hold_time then - init() - else - if hold_y then - local dy = hold_y - y - if math.abs(dy) > epsilon then - changeSpeed(dy) - end - end - end - hold_y = nil - hold_time = 0 - end -end - -function drawHelp() - local bottom = screen_h - margin - local right_edge = screen_w - margin - local reset_msg = "Reset: [r] key or long press" - local speed_msg = "Set speed: [+]/[-] key or drag up/down" - G.print(reset_msg, margin, (bottom - fh) - fh) - G.print(speed_msg, margin, bottom - fh) - local speed_label = string.format("Speed: %02d", speed) - local label_w = font:getWidth(speed_label) - G.print(speed_label, right_edge - label_w, bottom - fh) -end - -function drawCell(x, y) - G.setColor(.9, .9, .9) - G.rectangle('fill', - (x - 1) * cell_size, - (y - 1) * cell_size, - cell_size, cell_size) - G.setColor(.3, .3, .3) - - G.rectangle('line', - (x - 1) * cell_size, - (y - 1) * cell_size, - cell_size, cell_size) -end - -function love.draw() - for x = 1, grid_w do - for y = 1, grid_h do - if grid[x][y] == 1 then - drawCell(x, y) + ensure_init() + if button ~= 1 then return end + if S.hold_dt >= HOLD_RST then + init_grid() + else + if S.hold_y then + local dy = S.hold_y - y + if math.abs(dy) > EPSILON then + change_speed(dy) end end end - - G.setColor(1, 1, 1, 0.5) - drawHelp() + S.hold_y = nil + S.hold_dt = 0 end - -math.randomseed(os.time()) -initializeGrid() diff --git a/src/examples/paint/README.md b/src/examples/paint/README.md index acd44128..cb497caf 100644 --- a/src/examples/paint/README.md +++ b/src/examples/paint/README.md @@ -1,206 +1,80 @@ -## Paint - -The Paint(brush) game is a touch-first project. It can, of course, be used with a mouse, and will offer keyboard conveniences, but it's foremost intent is to be used on a touchscreen. - -### Interface design - -We have 3 main areas of interest: -* the color palette -* the tool pane -* the canvas - -```plain -+--------+--------------------------------------------------+ -│ +--+ │ │ -│ | | │ │ -│ +--+ │ │ -│ tool │ │ -│ +--+ │ │ -│ | | │ │ -│ +--+ │ │ -│--------│ canvas │ -│ │ │ -│ │ │ -│ line │ │ -│ │ │ -│ │ │ -│ │ │ -+-----------+-----+-----+-----+-----+-----+-----+-----+-----+ -│ +-----+ │ │ │ │ │ │ │ │ │ -+ │color│ +-----+-----+-----+-----+-----+-----+-----+-----+ -│ +-----+ │ │ │ │ │ │ │ │ │ -+-----------+-----+-----+-----+-----+-----+-----+-----+-----+ -``` - -The tool window is further split in two: tool selection and tool size (or line width). -Similarly, the color palette is split between the controls for selecting and displaying said selection. -We can draw freely on the rest of the space. - -#### Palette - -Building on the 16-color theme, we will divide the screen into 10 columns (8 colors + 2 for display). -Then halve these columns to get the row height. Display the selected background on a double block, with the foreground color in the middle. - -```lua -width, height = G.getDimensions() ---- color palette -block_w = width / 10 -block_h = block_w / 2 -pal_h = 2 * block_h -pal_w = 8 * block_w -sel_w = 2 * block_w -``` - -#### Toolbox - -It shall be 1.5 times column width, the height taking the rest of the screen not used by the palette. We will also add a margin so the controls have some breathing room from the side. - -```lua ---- tool pane -margin = block_h / 10 -box_w = 1.5 * block_w -box_h = height - pal_h -``` - -Currently, there's only two tools: a brush and an eraser. We have to divide the available space between them, being mindful that they will need to fit on different screen sizes. -We will have double margins on the top and bottom, and quadruple to the sides, displaying them on top of each other, that comes out to: - -```lua -m_4 = margin * 4 -n_t = 2 -icon_h = (tool_h - m_4) / n_t -icon_w = (box_w - m_4 - m_4) / 1 -icon_d = math.min(icon_w, icon_h) -``` - -Depending on the screen, we might be more limited based on either height or width, so the square icon's size will be the determined by the smaller of the two. This ensures that we can comfortably draw them on any reasonable size. There might be edge cases of screens so small that we can't display properly, a shortcoming whose solution is left as an exercise to the reader. - -### Interaction - -#### Pointing and clicking - -A very central theme in this application is determining where the user clicks/taps. For starters there's the drawing, but also switching tools and colors. - -Click and taps will start out in the `point()` function: - -```lua -function point(x, y, btn) - if inPaletteRange(x, y) then - setColor(x, y, btn) - end - if inCanvasRange(x, y) then - useCanvas(x, y, btn) - end - if inToolRange(x, y) then - selectTool(x, y) - end - if inWeightRange(x, y) then - setLineWeight(y) - end -end -``` - -Simply check where the click is, and forward it to the respective handler. If we set up our functions correctly, there should not be more than one thing happening in any single interaction. - -For example: - -```lua -function inCanvasRange(x, y) - return (y < height - pal_h and box_w < x) -end - -function inPaletteRange(x, y) - return (height - pal_h <= y - and width - pal_w <= x and x <= width) -end -``` - -To be registered on the canvas (more on that later), a the x coordinate has to be strictly larger than the toolbox width, and strictly smaller than `height - palette height`. On the other hand, -th function that detects palette click uses `<=`. Not that it would be that terrible to have a single pixel width of overlap/hiatus, but this way each one is accounted for. - -Once we know what interface element we are on, we can move on to the tiny bit more advanced math: - -```lua -function setColor(x, y) - local row = math.modf((height - y) / block_h) - local col = math.modf((x - sel_w) / block_w) - - color = col + (8 * row) -end -``` - -To find out which color block was clicked, we have to do some integer division. -`math.modf(n)` splits up a `n` into it's integer and fractional part. -```lua -local i, f = math.modf(2.3) --- i = 2 , f = 0.3 -``` +# Paint -In our case, we are only interested in the whole number to navigate our grid, and work back to a color index, preferably the same one that was used for displaying it: +This project demonstrates a simple **paint program** running on the Compy device. +It introduces mouse interaction, UI toolbars, color palette, brush weights, and canvas rendering. -```lua - local y = height - block_h - for c = 0, 7 do - local x = block_w * (c + 2) - G.setColor(Color[c]) - G.rectangle("fill", x, y, width, block_h) - G.setColor(Color[c + 8]) - G.rectangle("fill", x, y - block_h, width, block_h) - G.setColor(Color[Color.white]) - G.rectangle("line", x, y, width, block_h) - G.rectangle("line", x, y - block_h, width, block_h) - end -``` +--- -## Drawing +## Screen layout -Okay, let's create some pictures. It's time we talked about what a canvas is. In LOVE parlance, a canvas is a piece of drawable graphics, just like lines and rectangles, but we can draw multiple things on them. This can already be useful if for drawing the same thing multiple times on the screen. Of course, that's always possible with repeating most of the code, or using functions (a better idea). However, with a canvas, we can do the rendering off-screen, and put the whole result up at once. When drawing heavy graphics, this is a lot easier on the hardware. - -We will use a canvas to record the player's drawings. Not only is this very convenient, it spares the horrible amount of work it would take to store every click and the tool used, and re-render it on each frame. +The screen is split into three regions: -Let's see how this works. First, we set up the canvas: +* **Left sidebar** → tools and brush weights +* **Bottom bar** → color palette (16 colors) +* **Main area** → the canvas where you draw -```lua -can_w = width - box_w -can_h = height - pal_h - 1 -canvas = G.newCanvas(can_w, can_h) -``` +Canvas is stored in an off-screen `Canvas` object (`love.graphics.newCanvas`) and rendered every frame. -The default size of a canvas would be equivalent to the screen, but we have some UI elements here, so a bit smaller makes more sense. However, this does mean we need to calculate the offsets properly when detecting clicks and displaying it. +--- -We can draw on a canvas (and not the screen) by calling the `setCanvas()` function with the canvas as the parameter, doing the various operations as we normally would, then calling it again, this time without any parameters, which resets the main canvas as active. +## Tools -This is somewhat cumbersome, and there *is* a shortcut provided: `Canvas` objects have a `renderTo()` function, which does much the same for us automatically, provided we wrap the drawing operations in a function: +* **Brush (tool 1)** → draw with the selected foreground color +* **Eraser (tool 2)** → erase using the background color -```lua -function useCanvas(x, y, btn) - local aw = getWeight() - canvas:renderTo(function() - -- ... - G.circle("fill", x - box_w, y, aw) - end) -end -``` - -Note the _x_ coordinate, which is offset by `box_w` (the width of the side panel). When drawing, we go the opposite direction: `G.draw(canvas, box_w)`. - -### Click detection - -There's one more challenge to tackle: with touch, we don't have second button, no right-click. If we want a secondary use case (like setting the background color instead of the foreground), we have to come up with some other way. -Double clicks/taps are a workable solution, but there is a problem: detecting them is not trivial. Any second click is necessarily preceded by a first one, so you need to kind of hold off on doing anything and wait to see if a second tap follows. - -To solve for this, we created custom handlers for single and double clicks: +Switch tools using the mouse (click in sidebar) or press `Tab`. -```lua -function love.singleclick(x, y) - point(x, y, 1) -end +--- -function love.doubleclick(x, y) - point(x, y, 2) -end -``` +## Brush weights -A drawback of this is it feels somewhat less snappy, because of the wait time, but there isn't really a way around this. Another quirk is that if you move the cursor or your finger, it can't be registered as a double click on the same point, so instead of trying the impossible and deciding which position between the two should be the relevant one, these are considered invalid and no action is taken. +In the lower half of the sidebar you see 8 slots. +Each slot corresponds to a different brush size. +Click to select the active size. +Eraser scales sizes ×1.5 automatically. + +--- + +## Color palette + +The bottom of the screen shows 16 colors (two rows of 8). + +* **Left click** → set foreground (drawing color) +* **Right click / double click** → set background + +Shortcut: keys `1–8` select colors; hold **Shift** to select the brighter row. + +--- + +## Controls + +* **Mouse drag (left button)** → paint with brush +* **Mouse drag (right button)** → paint with background (eraser style) +* **Mouse move** → shows a circle preview of current brush size +* **`Tab`** → cycle tools +* **`[` / `]`** → decrease / increase brush size +* **Number keys 1–8** → select colors (with Shift for bright set) + +--- + +## Code structure + +* **Hit-test helpers** → functions like `inCanvasRange`, `inPaletteRange` +* **UI rendering** → `drawToolbox`, `drawColorPalette`, `drawWeightSelector` +* **Painting ops** → `useCanvas`, `setPaintColor`, `applyPaint` +* **Input dispatchers** → `point`, `love.singleclick`, `love.doubleclick` +* **State changes** → `setColor`, `setTool`, `setLineWeight` + + + +## Learning goals + +* How to split a screen into UI regions +* How to use canvases (`love.graphics.newCanvas`) for persistent drawing +* How to handle **mouse input** (press, drag, move, click, doubleclick) +* How to design simple UI tool selectors (tools, colors, weights) + +--- + +📌 With this example, learners can explore **drawing programs**, expand to **fill tools**, **shapes**, or even **layers** later. -With these, our rudimentary Paint app is complete. diff --git a/src/examples/paint/main.lua b/src/examples/paint/main.lua index dc2f19b2..4dc56652 100644 --- a/src/examples/paint/main.lua +++ b/src/examples/paint/main.lua @@ -1,12 +1,21 @@ +--- @diagnostic disable: duplicate-set-field,lowercase-global + +-- Expect global G = love.graphics and Color table to exist. + +--======================== +-- Screen / palette sizes +--======================== width, height = G.getDimensions() ---- color palette + block_w = width / 10 block_h = block_w / 2 pal_h = 2 * block_h pal_w = 8 * block_w sel_w = 2 * block_w ---- tool pane +--============= +-- Tool sidebar +--============= margin = block_h / 10 m_2 = margin * 2 m_4 = margin * 4 @@ -18,32 +27,54 @@ tool_midx = box_w / 2 n_t = 2 icon_h = (tool_h - m_4 - m_2) / n_t --- one col for now -icon_w = (box_w - m_4 - m_4) / 1 +icon_w = (box_w - m_4 - m_4) icon_d = math.min(icon_w, icon_h) --- line weight + weight_h = box_h / 2 wb_y = box_h - weight_h weights = { 1, 2, 4, 5, 6, 9, 11, 13 } ---- canvas +--======== +-- Canvas +--======== can_w = width - box_w can_h = height - pal_h - 1 canvas = G.newCanvas(can_w, can_h) ---- selected -color = 0 -- black -bg_color = 0 -- black -weight = 3 -tool = 1 -- brush +--============= +-- Selections +--============= +color = 0 +bg_color = 0 +weight = 3 +tool = 1 + +--======================== +-- Small math/helper utils +--======================== +local function clamp(v, lo, hi) + if v < lo then return lo end + if v > hi then return hi end + return v +end + +local function mid(a, b) + return (a + b) / 2 +end +--================= +-- Hit-test helpers +--================= function inCanvasRange(x, y) - return (y < height - pal_h and box_w < x) + local below_pal = (y < height - pal_h) + local right_side = (x > box_w) + return below_pal and right_side end function inPaletteRange(x, y) - return (height - pal_h <= y - and width - pal_w <= x and x <= width) + local in_y = (y >= height - pal_h) + local in_x = (x >= width - pal_w and x <= width) + return in_y and in_x end function inToolRange(x, y) @@ -51,46 +82,65 @@ function inToolRange(x, y) end function inWeightRange(x, y) - return (x <= box_w and y < height - pal_h and wb_y < y) + local in_x = (x <= box_w) + local in_y = (y < height - pal_h and y > wb_y) + return in_x and in_y end +--================= +-- Background layer +--================= function drawBackground() G.setColor(Color[Color.black]) G.rectangle("fill", 0, 0, width, height) end -function drawPaletteOutline(y) +--============= +-- Color bar UI +--============= +local function paletteFill(x, y, w, h) + G.rectangle("fill", x, y, w, h) +end + +local function paletteLine(x, y, w, h) + G.rectangle("line", x, y, w, h) +end + +local function drawPaletteOutline(y) G.setColor(Color[bg_color]) - G.rectangle("fill", 0, y - block_h, block_w * 2, block_h * 2) + paletteFill(0, y - block_h, block_w * 2, block_h * 2) + G.setColor(Color[Color.white]) - G.rectangle("line", 0, y - block_h, sel_w, pal_h) - G.rectangle("line", sel_w, y - block_h, width, pal_h) + paletteLine(0, y - block_h, sel_w, pal_h) + paletteLine(sel_w, y - block_h, width, pal_h) end -function drawSelectedColor(y) +local function drawSelectedColor(y) + local bx = block_w / 2 + local by = y - (block_h / 2) G.setColor(Color[color]) - G.rectangle("fill", block_w / 2, y - (block_h / 2), - block_w, block_h) - -- outline - local line_color = Color.white + Color.bright - if color == line_color then - line_color = Color.black - end - G.setColor(Color[line_color]) - G.rectangle("line", block_w / 2, y - (block_h / 2), - block_w, block_h) + paletteFill(bx, by, block_w, block_h) + + local lc = Color.white + Color.bright + if color == lc then lc = Color.black end + G.setColor(Color[lc]) + paletteLine(bx, by, block_w, block_h) +end + +local function drawColorCell(x, y, c, top) + local yy = top and (y - block_h) or y + local ci = top and (c + 8) or c + G.setColor(Color[ci]) + paletteFill(x, yy, width, block_h) + G.setColor(Color[Color.white]) + paletteLine(x, yy, width, block_h) end -function drawColorBoxes(y) +local function drawColorBoxes(y) for c = 0, 7 do local x = block_w * (c + 2) - G.setColor(Color[c]) - G.rectangle("fill", x, y, width, block_h) - G.setColor(Color[c + 8]) - G.rectangle("fill", x, y - block_h, width, block_h) - G.setColor(Color[Color.white]) - G.rectangle("line", x, y, width, block_h) - G.rectangle("line", x, y - block_h, width, block_h) + drawColorCell(x, y, c, false) + drawColorCell(x, y, c, true) end end @@ -101,165 +151,187 @@ function drawColorPalette() drawColorBoxes(y) end -function drawBrush(cx, cy) - G.push() - G.translate(cx, cy) - local s = icon_d / 100 * .8 - G.scale(s, s) - G.rotate(math.pi / 4) -- 45 degree rotation - - -- Draw the brush handle (wooden brown color) +--================= +-- Tool icons (UI) +--================= +local function drawBrushHandle() G.setColor(0.6, 0.4, 0.2) G.rectangle("fill", -8, -80, 16, 60) - - -- Handle highlight G.setColor(0.8, 0.6, 0.4) G.rectangle("fill", -6, -75, 3, 50) +end - -- Metal ferrule +local function drawBrushFerrule() G.setColor(0.7, 0.7, 0.8) G.rectangle("fill", -10, -25, 20, 12) - - -- Ferrule shine G.setColor(0.9, 0.9, 1.0) G.rectangle("fill", -8, -24, 3, 10) +end - -- Bristles with smooth flame-shaped tip +local function drawBrushBristles() G.setColor(0.2, 0.2, 0.2) G.rectangle("fill", -12, -13, 24, 25) +end - -- Create flame tip using bezier curve - local curve = love.math.newBezierCurve( - -12, 12, -- Start left - -15, 20, -- Control point 1 (outward curve) - -5, 30, -- Control point 2 (inward curve) - 0, 35, -- Tip point - 5, 30, -- Control point 3 (inward curve) - 15, 20, -- Control point 4 (outward curve) - 12, 12 -- End right +local function drawBrushTip() + local c = love.math.newBezierCurve( + -12, 12, -15, 20, -5, 30, 0, 35, + 5, 30, 15, 20, 12, 12 ) - - local points = curve:render() - G.polygon("fill", points) - - G.pop() + local pts = c:render() + G.polygon("fill", pts) end -function drawEraser(cx, cy) +function drawBrush(cx, cy) G.push() G.translate(cx, cy) - local s = icon_d / 100 + local s = (icon_d / 100) * 0.8 G.scale(s, s) - G.rotate(math.pi / 4) -- 45 degree rotation + G.rotate(math.pi / 4) + drawBrushHandle() + drawBrushFerrule() + drawBrushBristles() + drawBrushTip() + G.pop() +end - -- Main eraser body (light blue) +local function eraserBody() G.setColor(Color[Color.white]) G.rectangle("fill", -12, -40, 24, 60) +end - -- Blue stripes running lengthwise (darker blue) +local function eraserStripes() G.setColor(Color[Color.blue]) G.rectangle("fill", -12, -40, 6, 60) G.rectangle("fill", 6, -40, 6, 60) +end - -- Worn eraser tip (slightly darker) - G.setColor(Color[Color.white + Color.bright]) +local function eraserTip() + local w = Color.white + Color.bright + G.setColor(Color[w]) G.rectangle("fill", -12, 15, 24, 8) +end - -- Eraser crumbs +local function eraserCrumbs() G.setColor(Color[Color.white]) G.circle("fill", 18, 25, 2) G.circle("fill", 22, 30, 1.5) G.circle("fill", 15, 32, 1) +end +function drawEraser(cx, cy) + G.push() + G.translate(cx, cy) + local s = icon_d / 100 + G.scale(s, s) + G.rotate(math.pi / 4) + eraserBody() + eraserStripes() + eraserTip() + eraserCrumbs() G.pop() end --- this is a color goose = { 0.303, 0.431, 0.431 } -local tools = { - drawBrush, - drawEraser, -} + +local tools = { drawBrush, drawEraser } + +local function drawToolSlot(x, y, size, on) + local white_b = Color.white + Color.bright + local fill = on and Color.black or white_b + G.setColor(Color[fill]) + G.rectangle("fill", x, y, size, size) + G.setColor(Color[Color.black]) + G.rectangle("line", x, y, size, size) +end + function drawTools() local tb = icon_d - local tb_half = tb / 2 + local half = tb / 2 for i = 1, n_t do - local x = tool_midx - tb_half - local y = (i - 1) * (m_2 + tb) - if i == tool then - G.setColor(Color[Color.black]) - else - G.setColor(Color[Color.white + Color.bright]) - end - G.rectangle("fill", x, y + m_2, tb, tb) - - G.setColor(Color[Color.black]) - G.rectangle("line", x, y + m_2, tb, tb) - + local x = tool_midx - half + local y = (i - 1) * (m_2 + tb) + m_2 + local on = (i == tool) + drawToolSlot(x, y, tb, on) local draw = tools[i] - draw(tool_midx - m_2, y + tb_half + m_4) + local cx = tool_midx - m_2 + local cy = y + half + m_2 + draw(cx, cy) + end +end + +--==================== +-- Weight selector UI +--==================== +local function goosePoints(r, my) + return { + r.x2, r.y1, r.x1, r.y1, r.x1, r.y2, r.x2, r.y2, + r.x1 + m_2, my + m_2, r.x1 + m_4, my, r.x1 + m_2, my - m_2 + } +end + +local function drawGooseFill(r) + local my = (r.y1 + r.y2) / 2 + local pts = goosePoints(r, my) + G.setColor(goose) + G.polygon("fill", pts) +end + +local function drawGooseStroke(r) + local my = (r.y1 + r.y2) / 2 + local pts = goosePoints(r, my) + G.setColor(Color[Color.black]) + G.setLineWidth(2) + G.polygon("line", pts) + G.setLineWidth(1) +end + +local function drawGoose(r) + drawGooseFill(r) + drawGooseStroke(r) +end + +local function drawWeightRow(i, y, h, midy) + local w = marg_l + G.setColor(Color[Color.white + Color.bright]) + G.rectangle("fill", margin, y, w, h) + + local sel = (i == weight) + if sel then + local rx1 = 3 * margin + local rx2 = 5 * margin + local ry1 = midy - margin + local ry2 = ry1 + m_2 + drawGoose({ x1 = rx1, y1 = ry1, x2 = rx2, y2 = ry2 }) end + + G.setColor(Color[Color.black]) + local aw = weights[i] + local xx = box_w / 3 + local yy = midy - (aw / 2) + G.rectangle("fill", xx, yy, box_w / 2, aw) end function drawWeightSelector() + local bx = 0 + local by = box_h - weight_h + local bw = box_w - 1 G.setColor(Color[Color.white + Color.bright]) - G.rectangle("line", 0, box_h - weight_h, box_w - 1, weight_h) - local h = (weight_h - (2 * margin)) / 8 - local w = marg_l - for i = 0, 7 do - local y = wb_y + margin + (i * h) - local lw = i + 1 - local mid = y + (h / 2) - G.setColor(Color[Color.white + Color.bright]) - G.rectangle("fill", margin, y, w, h) - if lw == weight then - -- G.setColor(Color[Color.white]) - -- G.rectangle("fill", margin, y, w, h) - G.setColor(goose) - local rx1 = 3 * margin - local rx2 = 5 * margin - local ry1 = mid - margin - local ry2 = ry1 + m_2 - local x1 = 5 * margin - local x2 = 7 * margin - local y1 = mid - m_2 - local y2 = mid + m_2 - G.polygon("fill", - -- body - rx2, ry1, - rx1, ry1, - rx1, ry2, - rx2, ry2, - -- head - x1, y2, - x2, mid, - x1, y1 - ) - G.setColor(Color[Color.black]) - G.setLineWidth(2) - G.polygon("line", - -- body - rx2, ry1, - rx1, ry1, - rx1, ry2, - rx2, ry2, - -- head - x1, y2, - x2, mid, - x1, y1 - ) - G.setLineWidth(1) - else - end - G.setColor(Color[Color.black]) - local aw = weights[lw] - G.rectangle("fill", box_w / 3, mid - (aw / 2), - box_w / 2, aw) + G.rectangle("line", bx, by, bw, weight_h) + + local rows = 8 + local h = (weight_h - (2 * margin)) / rows + for i = 1, rows do + local y = wb_y + margin + ((i - 1) * h) + local midy = y + (h / 2) + drawWeightRow(i, y, h, midy) end end +--========= +-- Toolbox +--========= function drawToolbox() - --- outline G.setColor(Color[Color.white]) G.rectangle("fill", 0, 0, box_w - 1, height - pal_h) G.setColor(Color[Color.white + Color.bright]) @@ -268,89 +340,97 @@ function drawToolbox() drawWeightSelector() end +--================== +-- Paint parameters +--================== function getWeight() - local aw - if tool == 1 then - aw = weights[weight] - elseif tool == 2 then - aw = weights[weight] * 1.5 - end - return aw + local w = weights[weight] + if tool == 2 then w = w * 1.5 end + return w end function drawTarget() local x, y = love.mouse.getPosition() - if inCanvasRange(x, y) then - local aw = getWeight() - G.setColor(Color[Color.white]) - G.circle("line", x, y, aw) - end + if not inCanvasRange(x, y) then return end + local aw = getWeight() + G.setColor(Color[Color.white]) + G.circle("line", x, y, aw) end +--=============== +-- Frame drawing +--=============== function love.draw() drawBackground() drawToolbox() drawColorPalette() - G.draw(canvas, box_w) + G.draw(canvas, box_w, 0) drawTarget() end +--================ +-- State changes +--================ function setColor(x, y, btn) - local row = math.modf((height - y) / block_h) - local col = math.modf((x - sel_w) / block_w) - if btn == 1 then - color = col + (8 * row) - elseif btn > 1 then - bg_color = col + (8 * row) - end + local row = math.floor((height - y) / block_h) + local col = math.floor((x - sel_w) / block_w) + local base = col + (8 * row) + if btn == 1 then color = base else bg_color = base end end function setTool(_, y) - local h = icon_d + m_4 - local sel = math.modf(y / h) + 1 - if sel <= n_t then - tool = sel - end + local step = icon_d + m_4 + local sel = math.floor(y / step) + 1 + if sel <= n_t then tool = sel end end function setLineWeight(y) local ws = #weights local h = weight_h / ws - local lw = math.modf((y - wb_y) / h) + 1 - if lw > 0 and lw <= ws then - weight = lw + local idx = math.floor((y - wb_y) / h) + 1 + if idx > 0 and idx <= ws then weight = idx end +end + +--===================== +-- Painting operations +--===================== +local paint_state = { px = 0, y = 0, aw = 1, btn = 1 } + +-- moved up: define setPaintColor before applyPaint +local function setPaintColor(btn) + if btn == 1 and tool == 1 then + G.setColor(Color[color]); return end + G.setColor(Color[bg_color]) +end + +local function applyPaint() + setPaintColor(paint_state.btn) + G.circle( + "fill", + paint_state.px, + paint_state.y, + paint_state.aw + ) end function useCanvas(x, y, btn) local aw = getWeight() - canvas:renderTo(function() - if btn == 1 then - if tool == 1 then - G.setColor(Color[color]) - elseif tool == 2 then - G.setColor(Color[bg_color]) - end - elseif btn == 2 then - G.setColor(Color[bg_color]) - end - G.circle("fill", x - box_w, y, aw) - end) + paint_state.px = x - box_w + paint_state.y = y + paint_state.aw = aw + paint_state.btn = btn + canvas:renderTo(applyPaint) end +--================== +-- Input dispatchers +--================== function point(x, y, btn) - if inPaletteRange(x, y) then - setColor(x, y, btn) - end - if inCanvasRange(x, y) then - useCanvas(x, y, btn) - end - if inToolRange(x, y) then - setTool(x, y) - end - if inWeightRange(x, y) then - setLineWeight(y) - end + if inPaletteRange(x, y) then setColor(x, y, btn) end + if inCanvasRange(x, y) then useCanvas(x, y, btn) end + if inToolRange(x, y) then setTool(x, y) end + if inWeightRange(x, y) then setLineWeight(y) end end function love.singleclick(x, y) @@ -361,52 +441,36 @@ function love.doubleclick(x, y) point(x, y, 2) end -function love.mousemoved(x, y, dx, dy) - if inCanvasRange(x, y) - then - for btn = 1, 2 do - if - love.mouse.isDown(btn) - then - useCanvas(x, y, btn) - end - end +function love.mousemoved(x, y) + if not inCanvasRange(x, y) then return end + for btn = 1, 2 do + if love.mouse.isDown(btn) then useCanvas(x, y, btn) end end end +--============ +-- Key input +--============ colorkeys = { - ['1'] = 0, - ['2'] = 1, - ['3'] = 2, - ['4'] = 3, - ['5'] = 4, - ['6'] = 5, - ['7'] = 6, - ['8'] = 7, + ['1'] = 0, ['2'] = 1, ['3'] = 2, ['4'] = 3, + ['5'] = 4, ['6'] = 5, ['7'] = 6, ['8'] = 7, } + +local function cycleTool() + if tool >= n_t then tool = 1 else tool = tool + 1 end +end + +local function shiftColor(c) + if Key.shift() then return c + 8 end + return c +end + function love.keypressed(k) - if k == 'tab' then - if tool >= n_t then - tool = 1 - else - tool = tool + 1 - end - end - if k == '[' then - if weight > 1 then - weight = weight - 1 - end - end - if k == ']' then - if weight < #weights then - weight = weight + 1 - end - end + if k == 'tab' then cycleTool() end + if k == '[' and weight > 1 + then weight = weight - 1 end + if k == ']' and weight < #weights + then weight = weight + 1 end local c = colorkeys[k] - if c then - if Key.shift() then - c = c + 8 - end - color = c - end + if c then color = shiftColor(c) end end diff --git a/src/examples/repl/README.md b/src/examples/repl/README.md index 1c879e71..8b8c33d6 100644 --- a/src/examples/repl/README.md +++ b/src/examples/repl/README.md @@ -1,42 +1,82 @@ -### REPL -This project provides example of the minimum code required for utilizing the builtin user input helper. It also demonstrates taking control of application updates by overriding `love.update`. -The project is very light on functionality, only echoing the text the user enters. +# REPL -#### Using user input +This project provides an example of the **minimum code** required for +utilizing the builtin user input helper. It also demonstrates taking +control of application updates by overriding `love.update`. -The process of reading values from console has a necessarily asynchronous nature to it. The result can not be available at the point of declaring the variable that holds it. -Hence, it has to consist of multiple steps: first, we create a handle; then initiate the prompt, and only when the user is finished, can we read the data supplied. +The project is very light on functionality: it only echoes the text +the user enters. + +--- + +## Using user input + +Reading values from console is inherently asynchronous. +The result is not available at the point of declaring the variable. + +Therefore the workflow has to consist of several steps: + +1. **Create a handle** +2. **Prompt the user if empty** +3. **Read the value when available** + +### Example -How this translates into code: ```lua -- create a handle -r = user_input() +local r = user_input() --- if it doesn't hold a value currently, prompt the user +-- custom update routine function repl() - if r:is_empty() then + if not r or r:is_empty() then + -- prompt the user for text input_text() - else - -- read the value - local input = r() + return end + + -- read the value and echo it + local input = r() + print(input) end -``` +```` + +Two helper functions are available: + +* `input_text()` for plaintext input +* `input_code()` which only accepts syntactically valid Lua + (Validated input is not discussed here, see the 'valid' project.) -There are two options available: -* `input_text()` for plaintext -* `input_code()` which only accepts syntactically valid lua -(Validated input is not discussed here, see the 'valid' project) +--- -#### Update +## Update loop + +To create interactivity, a program needs to run continuously, +waiting for input and reacting to it. + +In LOVE2D this is achieved by overriding `love.update`. +By defining it, we control what happens as time passes. -To create interactivity, a program needs to run continuously, waiting for input and reacting to it. -In LOVE2D, this is achieved by overriding various handlers, the first of which is `update()`. -By defining `love.update()`, we can control what happens when time passes: ```lua -function love.update(dt) +function love.update() repl() end ``` -The parameter `dt` is the (fractional) number of seconds passed since the last run of the function. \ No newline at end of file + +> Note: `love.update` can take a `dt` argument (delta time in seconds), +> but in this minimal REPL example it is not used. + +--- + +## Summary + +This project shows: + +* how to hook into the user input helper +* how to separate prompting from reading values +* how to override `love.update` to create a simple REPL loop + +The result is a minimal echo program: +whatever the user types gets printed back. + + diff --git a/src/examples/repl/main.lua b/src/examples/repl/main.lua index cfc6ebaf..62e7019f 100644 --- a/src/examples/repl/main.lua +++ b/src/examples/repl/main.lua @@ -1,9 +1,15 @@ -r = user_input() + + +local r = user_input() function love.update() - if r:is_empty() then + -- If there is no user text, ask for input + if not r or r:is_empty() then input_text() - else - print(r()) + return end + + -- Avoid complex inline expressions: read then print + local value = r() + print(value) end diff --git a/src/examples/sine/README.md b/src/examples/sine/README.md index ca61dd3b..ecc07fe9 100644 --- a/src/examples/sine/README.md +++ b/src/examples/sine/README.md @@ -1,8 +1,32 @@ ### Sinewave -This project demonstrates the basics of drawing on the screen. -The end result is a display of coordinate system axes and a sine wave. +This project demonstrates the basics of drawing on the screen. +The final result is a display of coordinate system axes and a sine wave. #### Drawing -First, we establish the screen coordinates. It starts at the top left corner, which is (0, 0), and we can programmatically find out where the other ends are with the `getWidth()` and `getHeight()` functions. Unlike a usual coordinate system would look on paper (and what our end result will use), the y axis is flipped, it's value grows from top to bottom. \ No newline at end of file +First, we establish the screen coordinates. +The top-left corner is `(0, 0)`. +The width and height of the window are retrieved with +`love.graphics.getWidth()` and `love.graphics.getHeight()`. +From these values we calculate the center `(cx, cy)`, which serves +as the origin for both the axes and the sine wave. + +Unlike the usual Cartesian system on paper, the y axis in LÖVE grows +**downwards**. This means that when plotting `y = sin(x)`, the values +must be inverted (`cy - s * amp`) so that the wave appears correctly +above and below the center line. + +The code is divided into three simple parts: + +1. **Axes** – drawn with `draw_axes(cx, cy, w, h)`. + This shows horizontal and vertical reference lines. +2. **Points** – created in `build_points(cx, cy, w, amp)`. + The function loops across the screen width, evaluates the sine, + and stores `(x, y)` pairs in a table. +3. **Plot** – rendered with `draw_points(pts)`. + The table of points is drawn in red, forming the sine wave. + +By separating the logic into small functions, the program stays clean, +easy to read, and fully compliant with the formatting rules +(max 64 chars/line, ≤14 lines per function, ≤4 args per function). diff --git a/src/examples/sine/main.lua b/src/examples/sine/main.lua index 8348f3a1..9fd061cb 100644 --- a/src/examples/sine/main.lua +++ b/src/examples/sine/main.lua @@ -1,30 +1,45 @@ -local G = love.graphics - -local x0 = 0 -local xe = G.getWidth() -local y0 = 0 -local ye = G.getHeight() +-- Sinusoid with cross axes (Compy formatting) -local xh = xe / 2 -local yh = ye / 2 - -G.setColor(1, 1, 1, 0.5) -G.setLineWidth(1) -G.line(xh, y0, xh, ye) -G.line(x0, yh, xe, yh) +local G = love.graphics -G.setColor(1, 0, 0) -G.setPointSize(2) +-- Draw horizontal and vertical axes +local function draw_axes(cx, cy, w, h) + G.setColor(1, 1, 1, 0.5) + G.setLineWidth(1) + G.line(cx, 0, cx, h) + G.line(0, cy, w, cy) +end -local amp = 100 -local times = 2 -local points = { } +-- Build points for sine wave +local function build_points(cx, cy, w, amp) + local pts = {} + local tau = 2 * math.pi + local cycles = 2 + for x = 0, w do + local dx = x - cx + local v = tau * dx / w + local s = math.sin(v * cycles) + local y = cy - s * amp + pts[#pts + 1] = x + pts[#pts + 1] = y + end + return pts +end -for x = 0, xe do - local v = 2 * math.pi * (x - xh) / xe - local y = yh - math.sin(v * times) * amp - table.insert(points, x) - table.insert(points, y) +-- Draw the points in red +local function draw_points(pts) + G.setColor(1, 0, 0) + G.setPointSize(2) + G.points(pts) end -G.points(points) +-- Main entry +function love.draw() + local w = G.getWidth() + local h = G.getHeight() + local cx = w / 2 + local cy = h / 2 + draw_axes(cx, cy, w, h) + local pts = build_points(cx, cy, w, 100) + draw_points(pts) +end diff --git a/src/examples/tixy/README.md b/src/examples/tixy/README.md index d5ac52bd..bc9a4d16 100644 --- a/src/examples/tixy/README.md +++ b/src/examples/tixy/README.md @@ -1,100 +1,133 @@ +# README.md + ## tixy -Reimplementation of https://tixy.land/, a javascript project. -The idea is driving a 16x16 duotone dot matrix display by defining a function, which gets evaluated for each individual pixel and over time. -Input parameters to the function are: `t, i, x, y`, (hence the name), that is: +Reimplementation of [tixy.land](https://tixy.land/), a javascript project. +The idea is to drive a 16×16 duotone dot matrix display by defining a function, which gets evaluated for each individual pixel and over time. + +Input parameters to the function are: `t, i, x, y`, (hence the name): + +* `t` – time (in seconds) +* `i` – index of the pixel (0..255) +* `x` – horizontal coordinate (column) +* `y` – vertical coordinate (row) -* `t` - time -* `i` - index of the pixel -* `x` - vertical coordinate -* `y` - horizontal coordinate +--- ### Multiple source files See the `turtle` project for detailed explanation. +--- + ### Math `math.lua` does a couple of things: -* defines a `hypot()` function - this is something that the javascript Math library has, and some examples make use of it, so it had to be reimplemented -* imports the `math` module contents into the global namespace - -The latter is for the sake of brevity, more involved procedural drawing uses a lot of math functions in concert, repetition gets tedious quickly. +* defines a `hypot()` function – missing from stock Lua, but used in many examples +* imports the `math` module contents into the global namespace for brevity +* imports the `bit` library **safely with `pcall`** (not every environment has it, Compy included) -This provides a great opportunity to explain how the global environment works. It resides in a special table named `_G`. -We can add fields this way: ```lua -for k, v in pairs(math) do - _G[k] = v +local ok, bitlib = pcall(require, "bit") +if ok and bitlib then + for k, v in pairs(bitlib) do + _G[k] = v + end end ``` -...and finally, -* it imports the bit library -The lua implementation we use does not have bitwise operators, even though they can be very useful for creating pixel patterns. Just like with the math functions, we add these to the global table for ease of use. - -#### Bitwise operations +This way, code still runs even if bit operations are unavailable. -Relevant operations from the `bit` library: +--- -* `bor(x1 [,x2...])` -* `band(x1 [,x2...])` -* `bxor(x1 [,x2...])` - Bitwise OR, bitwise AND, and bitwise XOR of all (read: not just two are supported) arguments -* `lshift(x, n)` / `rshift(x, n)` - Shift of `x` left or right by `n` bits +### Function body and compilation -### Function body - -We need to take advantage of several more advanced features in lua. -First, to take some string and if it's valid code, turn it into a function, we use `loadstring`. +To allow interactive code editing, we take the text from the user (or examples), and turn it into a function: ```lua -local f = loadstring(code) +function setupTixy() + local head = "return function(t, i, x, y)\n" + -- FIX: normalize literal "\\n" into real newlines + local src = tostring(body):gsub("\\n", "\n") + local code = head .. src .. "\nend" + local f = loadstring(code) + if not f then return end + setfenv(f, _G) + tixy = f() +end ``` -Should there be some syntactic problem, we will get `nil` back, so the next stop is checking for that. In our case, the input already validates, so we should not find ourselves on the unhappy side of this. +This ensures that examples using `\n` work correctly. +Without this, switching examples would silently fail. -Next, set up the environment the function will run in, which should be `_G`, the same environment we prepared with easy access to math functions and bit operations. +--- + +### Boolean helpers + +Lua is strict about types, so we explicitly convert: ```lua -setfenv(f, _G) +function b2n(b) + if b then return 1 else return 0 end +end + +function n2b(n) + if n ~= 0 then return true else return false end +end ``` -Then we can actually run it: +--- + +### Math helpers + +`tixy` returns a number, which controls pixel radius. +Negative numbers become red pixels; large values are clamped: + ```lua -tixy = f() +function clamp(value) + local color = colors.pos + local radius = (value * size) / 2 + if radius < 0 then + radius = -radius + color = colors.neg + end + if radius > size / 2 then + radius = size / 2 + end + return color, radius +end ``` -Here's what the actual function looks like: +--- + +### Drawing + +We draw each pixel twice: once filled, once outlined. +This is a simple trick to get antialiasing (smooth edges): ```lua -function f() - return function(t, i, x, y) - -- body - end +function drawCircle(color, radius, x, y) + G.setColor(color) + local step = size + spacing + local sx = x * step + offset + local sy = y * step + offset + G.circle("fill", sx, sy, radius) + G.circle("line", sx, sy, radius) end ``` -Meaning that `f` returns another function, which will then be used for calculating the value of each pixel. +--- ### Mouse handling -To switch between examples, we make use of mouse handling. For this use case, LÖVE2D provides these event handlers: -* `mousepressed(x, y, button)` / `mousereleased(x, y, button)` - When a mouse button is clicked or released. -* `mousemoved(x, y, dx, dy)` - When the mouse moves. `(x,y)` is the current position, `(dx,dy)` is the difference compared to the last move event's `(x,y)`. -* `wheelmoved(x, y)` - Mouse wheel movement. - -We don't care about most of this, only what button was clicked, so the first parameters are `_`, which means "I don't care". The `button` parameter takes a value of 1 for left click, 2 for right click, and 3 for the middle button. Your mouse might have extra buttons, but support for those is not guaranteed. +We only need the button info: ```lua function love.mousepressed(_, _, button) if button == 1 then - -- ... + if Key.shift() then prev_example() + else next_example() end end if button == 2 then randomize() @@ -102,54 +135,85 @@ function love.mousepressed(_, _, button) end ``` -### Plumbing +* left click → next example +* shift + left click → previous example +* right click → random example -#### Boolean helpers +--- -Some C-like languages, Javascript included, treat numbers and booleans somewhat loosely (or so loosely that they don't even have a boolean type). -Lua is not like that, so we have to explicitly convert from bools to numbers (`b2n()`) or from numbers to bools (`n2b()`). +### Examples file -#### Math helpers +`examples.lua` contains all the demos. +Each uses **real newlines (`\n`)**, not `\\n`. +This makes them compile directly with `loadstring`. -The `tixy` function returns a number value which we use for the radius of a pixel. This value can be negative, but of course, we can't draw a circle with a less-than-zero radius, and we'd also like it to use a different color. Also, physical pixels have an upper size limit, so when drawing, we need to limit the value so it's never larger than a set maximum. In graphics, making sure that a value stays within bounds is often called *clamping* the value. +```lua +example( + "local dx = x - 5\n" .. + "local dy = y - 5\n" .. + "local r2 = dx^2 + dy^2\n" .. + "return r2 - 99 * sin(t)", + "create your own!" +) + + +--### How to add your own example + +The file `examples.lua` is where all examples live. +Each example has two fields: + +1. the **code string** (what will be turned into the `tixy` function), +2. the **legend** (a short description shown on screen). + +We use the helper function `example(code, legend)` to insert new ones. + +#### Step 1: Write your code + +The function body must be valid Lua. It receives four parameters: +`t` (time), `i` (index), `x` (column), `y` (row). + +For example, let’s draw a moving vertical bar: ```lua -function clamp(value) - local color = colors.pos - local radius = (value * size) / 2 - if radius < 0 then - radius = -radius - color = colors.neg - end - if size / 2 < radius then - radius = size / 2 - end - return color, radius -end +local code = + "return b2n(x == floor(t % count))" ``` -#### Drawing +This code makes all pixels in the column `t % count` visible. + +#### Step 2: Add a legend + +Write a short description to remind yourself what it does: ```lua -function drawCircle(color, radius, x, y) - G.setColor(color) - G.circle( - "fill", - x * (size + spacing) + offset, - y * (size + spacing) + offset, - radius - ) - G.circle( - "line", - x * (size + spacing) + offset, - y * (size + spacing) + offset, - radius - ) -end +local legend = "a vertical bar moving across the screen" ``` -Why is this code drawing circles twice? First, one circle is filled, the other is an outline, but it's not like it's using a different color, or adding a heavier line. The answer is antialiasing, which our graphics library won't do by default for solids, but will for lines. +#### Step 3: Insert into `examples.lua` + +At the bottom of `examples.lua`, add: + +```lua +example( + "return b2n(x == floor(t % count))", + "a vertical bar moving across the screen" +) +``` + +Make sure to use **real newlines** (`\n`) if your code has multiple lines. + +#### Step 4: Test it + +Restart the program. +Click through the examples with left mouse button until you see yours. +If it doesn’t show up, check for syntax errors in your code string. + +--- + +💡 **Tips:** -##### Antialiasing +* Start simple: `return x`, `return y`, `return sin(t)`. +* Try combining math: `return sin(t - hypot(x, y))`. +* Use helpers: `b2n()`, `n2b()`, and `hypot()`. +* Don’t worry if the first try fails — the editor will let you fix it quickly. -Imagine you draw a diagonal line on a grid made of tiny squares (pixels). Computer screens are made up of tiny square pixels arranged in a grid (unlike our round ones), and these pixels can only represent images in blocky steps. Because the line has to go through these squares, the edges look like little stairs instead of a smooth line — this is called "aliasing" or jagged edges. Antialiasing helps by gently blending the colors of the line's edge into the squares next to it, so the line looks smoother and less like stairs. It's like softly coloring the edges so the line looks smoother. diff --git a/src/examples/tixy/examples.lua b/src/examples/tixy/examples.lua index 963ffdf2..fcd3e115 100644 --- a/src/examples/tixy/examples.lua +++ b/src/examples/tixy/examples.lua @@ -1,100 +1,186 @@ +-- ==================[ FILE: examples.lua ]================== +-- Teaching note: +-- Each example has two short fields: code and legend. + examples = {} -function example(c, l) - table.insert(examples, { - code = c, - legend = l - }) +local function example(code_str, legend_str) + table.insert( + examples, + { code = code_str, legend = legend_str } + ) end example( "return b2n(math.random() < 0.1)", - "for every dot return 0 or 1 \nto change the visibility" + "for every dot return 0 or 1 \n" .. + "to change the visibility" ) + example( "return math.random()", - "use a float between 0 and 1 \nto define the size" + "use a float between 0 and 1 \n" .. + "to define the size" ) + example( - "return math.sin(t)", - "parameter `t` is \nthe time in seconds" + "return sin(t)", + "parameter `t` is the time in seconds" ) + example( "return i / 256", - "param `i` is the index \nof the dot (0..255)" + "param `i` is the index of the dot (0..255)" ) + example( "return x / count", - "`x` is the column index\n from 0 to 15" + "`x` is the column index from 0 to 15" ) -example("return y / count", "`y` is the row\n also from 0 to 15") + +example( + "return y / count", + "`y` is the row, also from 0 to 15" +) + example( "return y - 7.5", - "positive numbers are white,\nnegatives are red" + "positive numbers are white, negatives are red" ) -example("return y - t", "use the time\nto animate values") + +example( + "return y - t", + "use the time to animate values" +) + example( "return y - 4 * t", - "multiply the time\nto change the speed" + "multiply the time to change the speed" ) + example( - "return ({1, 0, -1})[i % 3 + 1]", - "create patterns using \ndifferent color" + "return ({1,0,-1})[i % 3 + 1]", + "create patterns using different color" ) + example( - "return sin(t - sqrt((x - 7.5)^2 + (y-6)^2) )", - "skip `math.` to use methods \nand props like `sin` or `pi`" + "local dx=x-7.5\n" .. + "local dy=y-6\n" .. + "local r=sqrt(dx^2+dy^2)\n" .. + "return sin(t-r)", + "skip `math.` to use `sin`, `pi` etc." ) + example("return sin(y/8 + t)", "more examples ...") example("return y - x", "simple triangle") + example( - "return b2n( (y > x) and (14 - x < y) )", + "local a=(y>x)\n" .. + "local b=(14-x3 and y>3 and x<12 and y<12 )", "square") + +example( + "return b2n(x>3 and y>3 and x<12 and y<12)", + "square" +) + example( - "return -1 * b2n( x>t and y>t and x<15-t and y<15-t )", + "local l=(x>t)\n" .. + "local t0=(y>t)\n" .. + "local r=(x<15-t)\n" .. + "local b=(y<15-t)\n" .. + "return -1*b2n(l and t0 and r and b)", "animated square" ) + example("return (y-6) * (x-6)", "mondrian squares") + example( - "return floor(y - 4 * t) * floor(x - 2 - t)", + "local vy=floor(y-4*t)\n" .. + "local vx=floor(x-2-t)\n" .. + "return vy*vx", "moving cross" ) -example("return band(4 * t, i, x, y)", "sierpinski") + +example("return band(4*t, i, x, y)", "sierpinski") + example( - "return y == 8 and band(t * 10, lshift(1, x)) or 0", + "return y==8 and band(t*10,lshift(1,x)) or 0", "binary clock" ) -example("return random() * 2 - 1", "random noise") -example("return sin(i ^ 2)", "static smooth noise") -example("return cos(t + i + x * y)", "animated smooth noise") -example("return sin(x/2) - sin(x-t) - y+6", "waves") + +example("return random()*2-1", "random noise") +example("return sin(i^2)", "static smooth noise") + +example( + "local p=t+i+x*y\n" .. + "return cos(p)", + "animated smooth noise" +) + example( - "return (x-8) * (y-8) - sin(t) * 64", + "local a=sin(x/2)\n" .. + "local b=sin(x-t)\n" .. + "return a-b-y+6", + "waves" +) + +example( + "local dx=x-8\n" .. + "local dy=y-8\n" .. + "local s=sin(t)*64\n" .. + "return dx*dy-s", "bloop bloop bloop" ) + example( - "return -.4 / (hypot(x - t%10, y - t%8) - t%2 * 9)", + "local a=t%10\n" .. + "local b=t%8\n" .. + "local c=t%2\n" .. + "local r=hypot(x-a,y-b)\n" .. + "local d=r-c*9\n" .. + "return -.4/d", "fireworks" ) + example("return sin(t - hypot(x, y))", "ripples") + example( - "return band( ({5463,2194,2386})[ band(y+t*9, 7) ]" .. - " or 0, lshift(1, x - 1) )", "scrolling TIXY") + "local k=band(y+t*9,7)\n" .. + "local v=({5463,2194,2386})[k] or 0\n" .. + "local m=lshift(1,x-1)\n" .. + "return band(v,m)", + "scrolling TIXY" +) + example("return (x-y) - sin(t) * 16", "wipe") example("return (x-y)/24 - sin(t)", "soft wipe") example("return sin(t*5) * tan(t*7)", "disco") + example( - "return (x-(count/2))^2 + (y-(count/2))^2 - 15*cos(pi/4)", + "local cx=count/2\n" .. + "local dx=x-cx\n" .. + "local dy=y-cx\n" .. + "local r2=dx^2+dy^2\n" .. + "return r2-15*cos(pi/4)", "日本" ) + example( - "return (x-5)^2 + (y-5)^2 - 99*sin(t)", + "local dx = x - 5\n" .. + "local dy = y - 5\n" .. + "local r2 = dx^2 + dy^2\n" .. + "return r2 - 99 * sin(t)", "create your own!" ) diff --git a/src/examples/tixy/main.lua b/src/examples/tixy/main.lua index b89da9b8..ed61dded 100644 --- a/src/examples/tixy/main.lua +++ b/src/examples/tixy/main.lua @@ -1,15 +1,23 @@ +--- @diagnostic disable: duplicate-set-field,lowercase-global +-- Title: TIXY viewer (Compy-friendly) + local G = love.graphics math.randomseed(os.time()) + cw, ch = G.getDimensions() midx = cw / 2 require("math") require("examples") +-- =========[ Layout / grid ]========= size = 28 spacing = 3 offset = size + 4 +count = 16 +ex_idx = 1 +-- =========[ Colors ]========= local colors = { bg = Color[Color.black], pos = Color[Color.white + Color.bright], @@ -18,28 +26,31 @@ local colors = { help = Color.with_alpha(Color[Color.white], 0.5) } +-- =========[ Help text ]========= +local help_lines = { + "Hint:", + "left click for next example", + "shift + left click to go back", + "right click for a random one" +} + +help = table.concat(help_lines, "\n") +showHelp = true + +-- =========[ Example source / legend ]========= body = "" legend = "" -help = - "Hint:\n" .. - "left click for next example\n" .. - "shift + left click to go back\n" .. - "right click for a random one" -showHelp = true -count = 16 -ex_idx = 1 local time = 0 -function load_example(ex) - if type(ex) == "table" then - body = ex.code - setupTixy() - legend = ex.legend - write_to_input(body) - end +local function load_example(ex) + if type(ex) ~= "table" then return end + body = ex.code + legend = ex.legend + setupTixy() + write_to_input(body) end -function advance() +local function next_example() local e = examples[ex_idx] load_example(e) if ex_idx < #examples then @@ -48,25 +59,24 @@ function advance() end end -function retreat() - if 1 < ex_idx then - local e = examples[ex_idx] - load_example(e) - ex_idx = ex_idx - 1 - time = 0 - end +local function prev_example() + if ex_idx <= 1 then return end + local e = examples[ex_idx] + load_example(e) + ex_idx = ex_idx - 1 + time = 0 end -function pick_random(t) - if type(t) == "table" then - local n = #t - local r = math.random(n) - return t[r], r - end +local function pick_random(t) + if type(t) ~= "table" then return end + local n = #t + local r = math.random(n) + return t[r], r end -function randomize() +local function randomize() local e, i = pick_random(examples) + if not e then return end load_example(e) ex_idx = i + 1 end @@ -87,76 +97,73 @@ function n2b(n) end end -function tixy(t, i, x, y) - return 0.1 -end +function tixy(_, _, _, _) return 0.1 end + +-- FIX: normalize newlines before loadstring function setupTixy() - local code = "return function(t, i, x, y)\n" .. body .. " end" + local head = "return function(t, i, x, y)\n" + local src = tostring(body):gsub("\\n", "\n") + local code = head .. src .. "\nend" local f = loadstring(code) - if f then - setfenv(f, _G) - tixy = f() - end + if not f then return end + setfenv(f, _G) + tixy = f() end -function drawBackground() +-- =========[ Drawing ]========= +local function drawBackground() G.setColor(colors.bg) G.rectangle("fill", 0, 0, cw, ch) end -function drawCircle(color, radius, x, y) +local function drawCircle(color, r, cx, cy) G.setColor(color) - G.circle( - "fill", - x * (size + spacing) + offset, - y * (size + spacing) + offset, - radius - ) - G.circle( - "line", - x * (size + spacing) + offset, - y * (size + spacing) + offset, - radius - ) -end - -function clamp(value) + local step = size + spacing + local sx = cx * step + offset + local sy = cy * step + offset + G.circle("fill", sx, sy, r) + G.circle("line", sx, sy, r) +end + +local function clamp(value) local color = colors.pos local radius = (value * size) / 2 if radius < 0 then radius = -radius color = colors.neg end - if size / 2 < radius then - radius = size / 2 - end + if radius > size / 2 then radius = size / 2 end return color, radius end -function drawOutput() - local index = 0 +local function drawCell(ts, idx, x, y) + local v = tonumber(tixy(ts, idx, x, y)) or -0.1 + local color, radius = clamp(v) + drawCircle(color, radius, x, y) +end + +local function drawOutput() + local idx = 0 local ts = time for y = 0, count - 1 do for x = 0, count - 1 do - local value = tonumber(tixy(ts, index, x, y)) or -0.1 - local color, radius = clamp(value) - drawCircle(color, radius, x, y) - index = index + 1 + drawCell(ts, idx, x, y) + idx = idx + 1 end end end -function drawText() +local function drawText() G.setColor(colors.text) local sof = (size / 2) + offset local hof = sof / 2 - G.printf(legend, midx + hof, sof, midx - sof) - if showHelp then - G.setColor(colors.help) - G.setFont(font) - G.printf(help, midx + hof, ch - (5 * sof), midx - sof) - end + local w = midx - sof + G.printf(legend, midx + hof, sof, w) + if not showHelp then return end + G.setColor(colors.help) + G.setFont(font) + G.printf(help, midx + hof, ch - (5 * sof), w) end function love.draw() @@ -165,31 +172,30 @@ function love.draw() drawText() end +-- =========[ Live code input ]========= r = user_input() function love.update(dt) time = time + dt if r:is_empty() then - input_code("function tixy(t, i, x, y)", string.lines(body)) - else - local ret = r() - body = string.unlines(ret) - setupTixy() - legend = "" + input_code( + "function tixy(t, i, x, y)", + string.lines(body) + ) + return end + local ret = r() + body = string.unlines(ret) + setupTixy() + legend = "" end +-- =========[ Mouse controls ]========= function love.mousepressed(_, _, button) if button == 1 then - if Key.shift() then - retreat() - else - advance() - end - end - if button == 2 then - randomize() + if Key.shift() then prev_example() else next_example() end end + if button == 2 then randomize() end end -advance() +next_example() diff --git a/src/examples/tixy/math.lua b/src/examples/tixy/math.lua index f0269c6f..0a8e5ace 100644 --- a/src/examples/tixy/math.lua +++ b/src/examples/tixy/math.lua @@ -1,13 +1,20 @@ ---- import math namespace into global +-- ====================[ FILE: math.lua ]==================== +--- @diagnostic disable: duplicate-set-field,lowercase-global +-- Import math namespace into globals for short, readable code. + for k, v in pairs(math) do _G[k] = v end +-- Pythagorean helper. function hypot(a, b) - return math.sqrt(a ^ 2 + b ^ 2) + return sqrt(a ^ 2 + b ^ 2) end -require("bit") -for k, v in pairs(bit or {}) do - _G[k] = v +-- Bit ops, if available. Safe with pcall. +local ok, bitlib = pcall(require, "bit") +if ok and bitlib then + for k, v in pairs(bitlib) do + _G[k] = v + end end diff --git a/src/examples/turtle/README.md b/src/examples/turtle/README.md index bc114c43..cae772ac 100644 --- a/src/examples/turtle/README.md +++ b/src/examples/turtle/README.md @@ -1,30 +1,44 @@ ### Turtle -Turtle graphics game inspired by the LOGO family of languages. -It is currently in a very early stage of implementation, offering few features. +Turtle graphics game inspired by the LOGO family of languages. +It is in a very early stage and currently offers only a few features. #### Multiple source files -The entrypoint for all projects is `main.lua`, and in some cases, that's all you will need, but in more complex cases such as this one, splitting up the code into multiple smaller files can greatly enhance readability and maintainability. -Doing is this is quite simple: create the new file, and include it in `main.lua` using the `require()` function. +The entry point for all projects is `main.lua`. +For simple cases that might be all you need. +For more complex ones like this, splitting code into smaller files +improves readability and maintainability. -There are two potential pitfalls to look out for here: -* The file should have `.lua` extension, but when `require()`-ing it, you need to omit that. -* Don't declare variables or functions `local` if you want to use it from the outside world. +Doing this is quite simple: create a new file, then include it in +`main.lua` using `require()`. + +Watch out for two pitfalls: +* The file should have a `.lua` extension, but when calling `require()` + you must omit it. +* Do not declare variables or functions as `local` if you need to use + them from other files. Example in `main.lua`: ```lua require('action') -``` -This imports the definitions from `action.lua` so they can be used in `main.lua`. -Notice that the code has been organized thematically, with the parts concerning what the turtle can do located in `action.lua` while it's presentation (how to it's displayed) moved to `drawing.lua`. +```` + +This imports definitions from `action.lua` so they can be used in +`main.lua`. Code is organized by theme: what the turtle can do lives in +`action.lua`, while its presentation (how it is displayed) is in +`drawing.lua`. #### Advanced drawing -Since we are controlling the turtle programmatically, it makes a lot of sense to draw the turtle programmatically, taking advantage of the graphics system conveniences. -Effectively this means that instead of calculating the coordinates for every element we want to draw, we change the coordinate system first, and draw in it's terms, which is often more convenient. +Since we control the turtle programmatically, it also makes sense to +draw it programmatically using the graphics system’s transforms. +Instead of computing coordinates for every element, we first change the +coordinate system and then draw in its terms. + +A minimal example: represent the turtle as an ellipse with major radius +`y_r` and minor radius `x_r`: -For the most simple example of this, let's represent the turtle with only an ellipse with a major radius `y_r` and a minor radius `x_r`: ```lua local x_r = 15 local y_r = 20 @@ -36,64 +50,92 @@ function turtleB(x, y) G.ellipse("fill", 0, 0, x_r, y_r, 100) end ``` -We can draw it at (x, y) either by drawing the shape to (x, y), or first translating the whole drawing to (x, y), and drawing at (0, 0). This might not seem that big of a deal in this simple case, but when the number of transformations and shapes go up, things cat get hard to track very quickly. + +Both draw at `(x, y)`. The second approach translates first, then draws +at `(0, 0)`. This pays off as transforms and shapes grow, because things +can get hard to track quickly. ###### Aside: Ellipses -An ellipse is a symmetrical curved shape that resembles a stretched circle. Unlike a circle which has the same width all around, an ellipse has two key measurements: the major axis (its longest measurement from edge to edge through the center) and the minor axis (its shortest measurement through the center). These two axes are always perpendicular to each other and meet at the center of the ellipse. The ratio between these axes determines how "stretched" or "squished" the ellipse appears - when they're equal, you get a perfect circle. -The way we translate this for LOVE is an "x radius" and a "y radius".
In our case, we want the turtle body to be longer vertically and shorter horizontally, so `y` will be our major axis and `x` the minor. +An ellipse is a stretched circle with two axes: major (longest through +the center) and minor (shortest through the center). +In LOVE we pass an x-radius and a y-radius. +Here we want a body longer vertically and shorter horizontally, so `y` +is major and `x` is minor. + +Next, add the head, positioned relative to the body and the local origin: -Next, we are adding the turtle's head, which is in some sort of relation to it's body, but also the location where the whole drawing is. ```lua G.circle("fill", 0, ((0 - y_r) - head_r) + neck, head_r, 100) ``` -Using the second method, we are able to provide the head position in "turtle coordinates". -So far, there's nothing about this we couldn't have done the other route, but let's proceed to the legs, which we want to draw at an angle. LOVE doesn't provide us any way to do this with only the ellipse function, we do need to `rotate` first. -See this condensed example: +With the translated coordinate system we give the head position in +“turtle coordinates”. + +For legs drawn at an angle, LOVE’s ellipse function cannot rotate a +shape directly, so we rotate the coordinate system first. + +Condensed example: + ```lua function frontLeftLeg(x, y, x_r, y_r, leg_xr, leg_yr) G.setColor(Color[Color.green + Color.bright]) - --- move to the turtle's position + -- move to turtle position G.translate(x, y) - --- move to where the leg attaches to the body + -- move to leg anchor G.translate(-x_r, -y_r / 2 - leg_xr) - --- rotate + -- rotate 45 degrees counter-clockwise G.rotate(-math.pi / 4) - --- draw the leg G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) end ``` -(Other functions, like `print()`, do provide a way to draw rotated and scaled, but for ellipses, it's not supported, so the transformations have to be applied first.) - ###### Aside: Angles -The `rotate` function takes a radian value as it's argument. We want the legs at 45 degree angles, which in radian terms is equal to `π / 4`. Also, in the example above, to draw the left leg, it needs to rotate counter-clockwise, hence the negative value. +`G.rotate` takes radians. A 45° angle equals `π / 4`. +A negative value rotates counter-clockwise. ##### Pushes and pops -You will notice that the actual code does not look like that. For one, in the leg-drawing functions, there's only one `translate()` call, because they are happening relative to the turtle, we already moved where the turtle is. -Another, more interesting difference is the `push()` - `pop()` pairs around each leg. +In the final code leg functions use only one `translate()` because they +are already relative to the turtle (we translated once at the start). +Each leg is wrapped in a `push()`/`pop()` pair to restore the previous +transform before setting up the next leg: + ```lua ---- left front leg +-- left front leg G.push("all") G.translate(-x_r, -y_r / 2 - leg_xr) G.rotate(-math.pi / 4) G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) G.pop() ---- right front leg +-- right front leg G.push("all") G.translate(x_r, -y_r / 2 - leg_xr) G.rotate(math.pi / 4) G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) G.pop() ``` -Say we are done drawing the left leg, and now we want to proceed to drawing the other one. We could do the opposite transformations to go back to "zero", or transform from our current state to the desired one, but that leads to more complicated math and less readable code. -Instead, we work in stages. When done with the first leg, we can "reset" to our previous state (the "turtle coordinates"), and set up our next one again relative to the center. -`push()` and `pop()` are like parens, they need to be balanced. Draw operations have to happen every frame, meaning several tens of times each second, and each push saves some data. This is fine, _if_ we properly clean up after ourselves by popping back and letting go of the data, otherwise we will run out of storage very quickly. + +Think of `push()`/`pop()` like parentheses: they must balance. +Drawing runs every frame; each `push` stores state. +That’s fine if we always `pop()` and free it, otherwise we run out of +memory quickly. + +#### Pause state + +The refactored code includes a pause system. +Use `togglePause("optional message")` to pause/unpause the game. +When paused, `love.update()` stops processing and a semi-transparent +overlay is drawn. This keeps the logic simple and easy to follow. ### User documentation How to use: -Press [I] to open the console. + +* Press **I** to open the console. +* Type commands: `forward`, `back`, `left`, `right` + (or the short forms `fd`, `b`, `l`, `r`). +* Press **Pause** key to toggle pause. +* Press **Space** to toggle debug view. + diff --git a/src/examples/turtle/action.lua b/src/examples/turtle/action.lua index e02c700d..3c2b6a9b 100644 --- a/src/examples/turtle/action.lua +++ b/src/examples/turtle/action.lua @@ -1,3 +1,7 @@ +--- @diagnostic disable: duplicate-set-field,lowercase-global +-- Simple movement and pause actions for the turtle. +-- Uses globals: tx, ty, incr, is_paused, pause_message. + function moveForward(d) ty = ty - (d or incr) end @@ -14,8 +18,10 @@ function moveRight(d) tx = tx + (d or (2 * incr)) end -function pause(msg) - pause(msg or "user paused the game") +function togglePause(msg) + -- Toggle pause state and remember the message. + is_paused = not is_paused + pause_message = msg or "user paused the game" end actions = { @@ -27,5 +33,5 @@ actions = { l = moveLeft, right = moveRight, r = moveRight, - pause = pause + pause = togglePause } diff --git a/src/examples/turtle/drawing.lua b/src/examples/turtle/drawing.lua index 2327153f..fad9b77c 100644 --- a/src/examples/turtle/drawing.lua +++ b/src/examples/turtle/drawing.lua @@ -1,5 +1,6 @@ local G = love.graphics +-- Colors and font are kept global for simplicity. font = G.newFont() bg_color = Color.black body_color = Color.green @@ -7,9 +8,9 @@ limb_color = body_color + Color.bright debug_color = Color.yellow function drawBackground(color) + -- Pick a safe background that is not the body / limb color. local c = bg_color - local not_green = color ~= body_color - and color ~= limb_color + local not_green = color ~= body_color and color ~= limb_color local color_valid = Color.valid(color) and not_green if color_valid then c = color @@ -20,11 +21,13 @@ end function drawFrontLegs(x_r, y_r, leg_xr, leg_yr) G.setColor(Color[limb_color]) + -- left front G.push("all") G.translate(-x_r, -y_r / 2 - leg_xr) G.rotate(-math.pi / 4) G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100) G.pop() + -- right front G.push("all") G.translate(x_r, -y_r / 2 - leg_xr) G.rotate(math.pi / 4) @@ -34,11 +37,13 @@ end function drawHindLegs(x_r, y_r, leg_r, leg_yr) G.setColor(Color[limb_color]) + -- left hind G.push("all") G.translate(-x_r, y_r / 2 + leg_r) G.rotate(math.pi / 4) G.ellipse("fill", 0, 0, leg_r, leg_yr, 100) G.pop() + -- right hind G.push("all") G.translate(x_r, y_r / 2 + leg_r) G.rotate(-math.pi / 4) @@ -47,21 +52,23 @@ function drawHindLegs(x_r, y_r, leg_r, leg_yr) end function drawBody(x_r, y_r, head_r) - --- body + -- body G.setColor(Color[body_color]) G.ellipse("fill", 0, 0, x_r, y_r, 100) - --- head + -- head (placed in turtle coordinates) local neck = 5 - G.circle("fill", 0, ((0 - y_r) - head_r) + neck, head_r, 100) - --- end + local hy = ((0 - y_r) - head_r) + neck + G.circle("fill", 0, hy, head_r, 100) end function drawTurtle(x, y) + -- Compact param block for readability. local head_r = 8 local leg_xr = 5 local leg_yr = 10 local x_r = 15 local y_r = 20 + G.push("all") G.translate(x, y) drawFrontLegs(x_r, y_r, leg_xr, leg_yr) @@ -73,13 +80,24 @@ end function drawHelp() G.setColor(Color[Color.white]) G.print("Press [I] to open console", 20, 20) - local help = "Enter 'forward', 'back', 'left', or 'right'" .. - "to move the turtle!" - G.print(help, 20, 50) + G.print("Type: forward/back/left/right (or fd/b/l/r)", 20, 50) end function drawDebuginfo() G.setColor(Color[debug_color]) - local dt = string.format("Turtle position: (%d, %d)", tx, ty) - G.print(dt, width - 200, 20) + local dt = string.format("Turtle: (%d, %d)", tx, ty) + G.print(dt, width - 160, 20) +end + +function drawPauseOverlay() + if not is_paused then return end + G.push("all") + G.setColor(0, 0, 0, 0.5) + G.rectangle("fill", 0, 0, width, height) + G.setColor(Color[Color.white]) + G.print("PAUSED", 20, 80) + if pause_message then + G.print(pause_message, 20, 110) + end + G.pop() end diff --git a/src/examples/turtle/main.lua b/src/examples/turtle/main.lua index 1833a4c9..dc13a334 100644 --- a/src/examples/turtle/main.lua +++ b/src/examples/turtle/main.lua @@ -1,21 +1,25 @@ +--- @diagnostic disable: duplicate-set-field,lowercase-global +-- Entry point: wires input, drawing, and update loop. + require("action") require("drawing") width, height = love.graphics.getDimensions() -midx = width / 2 -midy = height / 2 +midx, midy = width / 2, height / 2 incr = 10 tx, ty = midx, midy debug = false +is_paused = false +pause_message = nil +-- Console input handle local r = user_input() +-- Evaluate one console command against the actions map. function eval(input) local f = actions[input] - if f then - f() - end + if f then f() end end function love.draw() @@ -23,41 +27,42 @@ function love.draw() drawBackground() drawHelp() drawTurtle(tx, ty) - if debug then - drawDebuginfo() - end + if debug then drawDebuginfo() end + drawPauseOverlay() end function love.keypressed(key) + -- Shift+R to reset turtle to center. if love.keyboard.isDown("lshift", "rshift") then - if key == "r" then - tx, ty = midx, midy - end + if key == "r" then tx, ty = midx, midy end end if key == "space" then debug = not debug end if key == "pause" then - pause() + togglePause("toggled by keyboard") end end function love.keyreleased(key) + -- Open console with a title if key == "i" then r = input_text("TURTLE") end - + -- Ctrl+Esc to quit if love.keyboard.isDown("lctrl", "rctrl") then - if key == "escape" then - love.event.quit() - end + if key == "escape" then love.event.quit() end end end function love.update() - if ty > midy then - debug_color = Color.red - end + -- Early exit when paused: no state changes. + if is_paused then return end + + -- Tiny example of dynamic debug color. + if ty > midy then debug_color = Color.red end + + -- Pull and run queued console command. if not r:is_empty() then eval(r()) end diff --git a/src/examples/valid/README.md b/src/examples/valid/README.md index a9da9371..c6417030 100644 --- a/src/examples/valid/README.md +++ b/src/examples/valid/README.md @@ -1,23 +1,47 @@ -### Input validation +# Input validation (Compy-friendly Lua) -As an extension to the user input functionality, `validated_input()` allows arbitrary user-specified filters. -A "filter" is a function, which takes a string as input and returns a boolean value of whether it is valid and an optional `Error`. -The `Error` is structure which contains the error message (`msg`), and the location the error comes from, with line and character fields (`l` and `c`). +This project demonstrates how to **validate user input** in Lua, +following Compy formatting rules: + +* Maximum line length: 64 characters +* Functions and tables: ≤ 14 lines +* ≤ 4 arguments per function +* Nesting level: ≤ 4 +* No complex inline expressions +* Code must be clear and pedagogical + +--- + +## Usage -Example: ```lua -function non_empty(s) - if string.ulen(s) == 0 then - return false, Error('Input is empty!') +r = user_input() + +function love.update() + if r:is_empty() then + validated_input({ + min_length(2), + is_lower + }) + else + print(r()) end - return true end -``` -This is not a particularly useful validator, as the input will not be accepted and ran through the validations if it doesn't contain anything, but it demonstrates the idea quite well. +```` + +Here: + +* If no input is present, the program **prompts** with validation. +* Otherwise, it **prints** the entered value. + +--- + +## Validators + +### Minimum length -Filters will be run line-by-line, if the input has multiple lines, the line number is also indicated when it's invalid. -For increased visual usefulness, your validations can report on the first character which does not satisfy the criteria required: ```lua +-- Checks that the string has more than n characters function min_length(n) return function(s) local l = string.ulen(s) @@ -28,11 +52,41 @@ function min_length(n) end end ``` -This will result the entered text being red starting from the problem location. -Of course, this means in some cases that the line has to be validated char-by-char. To facilitate this, we provide the `string.forall()` helper, and a `Char` table containing some classifier functions. -`string.forall()` takes a validation function and runs it on each character, returning `true` if the string is valid, or `false` and the index of the offending character: +### Maximum length + +```lua +-- Checks that the string is at most n characters long +function max_length(n) + return function(s) + if string.ulen(s) <= n then + return true + end + return false, Error("too long!", n + 1) + end +end +``` + +### All uppercase + +```lua +-- Verifies every character is uppercase +function is_upper(s) + local function is_up(c) + return c == string.upper(c) + end + local ok, err_c = string.forall(s, is_up) + if ok then + return true + end + return false, Error("should be all uppercase", err_c) +end +``` + +### All lowercase + ```lua +-- Verifies every character is lowercase function is_lower(s) local ok, err_c = string.forall(s, Char.is_lower) if ok then @@ -42,29 +96,63 @@ function is_lower(s) end ``` -If you're curious about the details, check out `is_upper()`, which provides a manual implementation. +### Signed integer -#### Invoking +```lua +-- Checks for integer with optional minus sign +function is_number(s) + local sign = string.usub(s, 1, 1) + local offset = 0 + if sign == '-' then + offset = 1 + end + local digits = string.usub(s, 1 + offset) + local ok, err_c = string.forall(digits, Char.is_digit) + if ok then + return true + end + return false, Error("NaN", err_c + offset) +end +``` + +### Natural number ```lua -r = user_input() -validated_input({non_empty}) +-- Checks that the number is non-negative +function is_natural(s) + local is_num, err = is_number(s) + if not is_num then + return false, err + end + local n = tonumber(s) + if n < 0 then + return false, Error("It's negative!", 1) + end + return true +end ``` -Validations are applied to the input by passing an array of functions. Note the lack of parentheses after the function name, we don't want to call it yet, just refer to it by name. +--- + +## Helpers available + +* `string.ulen(s)` — unicode length +* `string.usub(s, from, to)` — unicode substring +* `string.forall(s, f)` — apply predicate `f` to each character +* `Char.is_alpha(c)` — is letter +* `Char.is_alnum(c)` — is alphanumeric +* `Char.is_lower(c)` — is lowercase +* `Char.is_upper(c)` — is uppercase +* `Char.is_digit(c)` — is digit +* `Char.is_space(c)` — is whitespace +* `Char.is_punct(c)` — punctuation -#### Helper functions +--- -* `string.ulen(s)` - as opposed to the builtin `len()`, this works for unicode strings -* `string.usub(s, from, to)` - unicode substrings -* `Char.is_alpha(c)` - is `c` a letter -* `Char.is_alnum(c)` - is `c` a letter or a number (alphanumeric) -* `Char.is_lower(c)` - is `c` lowercase -* `Char.is_upper(c)` - is `c` uppercase -* `Char.is_digit(c)` - is `c` a number -* `Char.is_space(c)` - is `c` whitespace -* `Char.is_punct(c)` - is `c` punctuation (!, ?, &, ;, parentheses, ...) +## Notes -Note that determining if something is a letter, or what case it is only reliable for the english alphabet. +* Validations are passed **as functions**, not called immediately. +* Errors highlight the location of the problem character. +* Designed for teaching: each function is short, commented, and + formatted for readability. -* `Error(msg, c, l)` - for creating errors, `l` and `c` are optional diff --git a/src/examples/valid/main.lua b/src/examples/valid/main.lua index ade1aa36..a6faf936 100644 --- a/src/examples/valid/main.lua +++ b/src/examples/valid/main.lua @@ -1,5 +1,6 @@ r = user_input() +-- Checks minimum length (unicode-aware) function min_length(n) return function(s) local l = string.ulen(s) @@ -10,35 +11,29 @@ function min_length(n) end end +-- Checks maximum length inclusive (unicode-aware) function max_length(n) return function(s) - if string.len(s) <= n then + if string.ulen(s) <= n then return true end return false, Error("too long!", n + 1) end end +-- Verifies all characters are uppercase (clear + short) function is_upper(s) - local ret = true - local l = string.ulen(s) - local err_c - local i = 1 - while ret and i <= l do - local v = string.usub(s, i, i) - if v ~= string.upper(v) then - ret = false - err_c = i - end - i = i + 1 + local function is_up(c) + return c == string.upper(c) end - - if ret then + local ok, err_c = string.forall(s, is_up) + if ok then return true end return false, Error("should be all uppercase", err_c) end +-- Verifies all characters are lowercase (helper-based) function is_lower(s) local ok, err_c = string.forall(s, Char.is_lower) if ok then @@ -47,6 +42,7 @@ function is_lower(s) return false, Error("should be lowercase", err_c) end +-- Checks signed integer form: optional '-' + digits function is_number(s) local sign = string.usub(s, 1, 1) local offset = 0 @@ -61,6 +57,7 @@ function is_number(s) return false, Error("NaN", err_c + offset) end +-- Natural integer (>= 0). Returns true on success. function is_natural(s) local is_num, err = is_number(s) if not is_num then @@ -70,8 +67,10 @@ function is_natural(s) if n < 0 then return false, Error("It's negative!", 1) end + return true end +-- Demo loop: ask for input with validations; else echo function love.update() if r:is_empty() then validated_input({