From 5c6a02b850d1336d55d93b78e8e9efbf20c82df4 Mon Sep 17 00:00:00 2001 From: itzmarkoni Date: Tue, 5 May 2026 20:09:20 -0400 Subject: [PATCH] updated --- programs/plinko.lua | 4 + programs/prizewheel.lua | 409 ++++++++++++++++------------------------ programs/roulette.lua | 3 + 3 files changed, 171 insertions(+), 245 deletions(-) diff --git a/programs/plinko.lua b/programs/plinko.lua index 0246e1c..782743b 100644 --- a/programs/plinko.lua +++ b/programs/plinko.lua @@ -523,6 +523,10 @@ local function start() buildBoard() print(("[plichinko] %d pegs, %d buckets"):format(#pegs, NUM_BUCKETS)) + -- Clear full screen before drawing anything + gpu.fill(COL_BG) + gpu.sync() + drawBoard() gpu.sync() end diff --git a/programs/prizewheel.lua b/programs/prizewheel.lua index 11cd57f..5e60fd2 100644 --- a/programs/prizewheel.lua +++ b/programs/prizewheel.lua @@ -1,15 +1,12 @@ --- Prize Wheel — spinning prize wheel, perspective view +-- Prize Wheel — spinning prize wheel, top-down view -- Tom's Peripherals GPU + screen wall. -- --- The wheel is drawn as a perspective ellipse (top tilted away from viewer). +-- The wheel fills the screen as a circle (top-down, like roulette). -- It physically rotates: angular velocity starts high and decays under --- friction until it stops. A fixed pointer at the top selects the prize. +-- friction. A fixed pointer arrow at the top selects the prize. -- --- Rendering approach: --- A point at wheel angle `a`, radius fraction `f` maps to screen as: --- sx = CX + f * RX * cos(a + wheelAngle) --- sy = WY + f * RY * sin(a + wheelAngle) (RY = RX * TILT) --- Wedges are rasterised row-by-row using ellipse span math. +-- The wheel IS the thing that spins — wedges rotate each frame. +-- Centre hub shows the current prize name when stopped. ---------------------------------------------------------------------- -- GPU discovery @@ -32,69 +29,59 @@ end -- Constants ---------------------------------------------------------------------- -local FRAME_DELAY = 0.033 -- ~30 fps +local FRAME_DELAY = 0.033 local TWO_PI = math.pi * 2 --- Perspective tilt: RY / RX. 0 = edge-on, 1 = top-down. --- 0.28 ≈ wheel tilted ~74° toward viewer (like a real prize wheel on a stand). -local TILT = 0.28 - --- Prize segments — name, colour, relative weight (wider = more likely) +-- Prize segments — name, colour, relative weight local PRIZES = { - { name = "$100", col = 0xF44336, weight = 2 }, -- red - { name = "SPIN AGAIN", col = 0xFFEB3B, weight = 3 }, -- yellow - { name = "$500", col = 0x4CAF50, weight = 1 }, -- green - { name = "BANKRUPT", col = 0x212121, weight = 2 }, -- near-black - { name = "$250", col = 0x2196F3, weight = 2 }, -- blue - { name = "$50", col = 0xFF9800, weight = 3 }, -- orange - { name = "JACKPOT", col = 0xE91E63, weight = 1 }, -- pink - { name = "$150", col = 0x9C27B0, weight = 2 }, -- purple - { name = "LOSE TURN", col = 0x607D8B, weight = 2 }, -- grey-blue - { name = "$75", col = 0x00BCD4, weight = 3 }, -- cyan - { name = "$1000", col = 0x8BC34A, weight = 1 }, -- lime - { name = "$25", col = 0xFF5722, weight = 3 }, -- deep orange + { name = "$100", col = 0xF44336, weight = 2 }, + { name = "SPIN AGAIN", col = 0xFFEB3B, weight = 3 }, + { name = "$500", col = 0x4CAF50, weight = 1 }, + { name = "BANKRUPT", col = 0x212121, weight = 2 }, + { name = "$250", col = 0x2196F3, weight = 2 }, + { name = "$50", col = 0xFF9800, weight = 3 }, + { name = "JACKPOT", col = 0xE91E63, weight = 1 }, + { name = "$150", col = 0x9C27B0, weight = 2 }, + { name = "LOSE TURN", col = 0x607D8B, weight = 2 }, + { name = "$75", col = 0x00BCD4, weight = 3 }, + { name = "$1000", col = 0x8BC34A, weight = 1 }, + { name = "$25", col = 0xFF5722, weight = 3 }, } --- Build cumulative angle table from weights local function buildSegments() local total = 0 for _, p in ipairs(PRIZES) do total = total + p.weight end local segs = {} - local cumAngle = 0 + local cum = 0 for i, p in ipairs(PRIZES) do local arc = (p.weight / total) * TWO_PI segs[i] = { - name = p.name, - col = p.col, - startA = cumAngle, - endA = cumAngle + arc, - midA = cumAngle + arc / 2, + name = p.name, + col = p.col, + startA = cum, + endA = cum + arc, + midA = cum + arc / 2, } - cumAngle = cumAngle + arc + cum = cum + arc end return segs end --- Physics spin constants -local OMEGA_MIN = 4.0 -- rad/s minimum starting spin -local OMEGA_MAX = 10.0 -- rad/s maximum starting spin -local FRICTION = 0.987 -- angular velocity multiplier per frame -local STOP_OMEGA = 0.04 -- rad/s below this we consider it stopped +-- Spin physics +local OMEGA_MIN = 4.0 -- rad/s +local OMEGA_MAX = 10.0 +local FRICTION = 0.987 -- multiplier per frame +local STOP_OMEGA = 0.04 -- rad/s -- Colours -local COL_BG = 0x050505 -local COL_RIM = 0x8B6914 -- gold rim -local COL_RIM_DARK = 0x5C4400 -local COL_SPOKE = 0xB8860B -local COL_HUB = 0x333333 -local COL_HUB_RING = 0x8B6914 -local COL_SEP = 0xFFFFFF -- wedge separator lines -local COL_STAND = 0x5D4037 -- wood brown -local COL_STAND_DRK = 0x3E2723 -local COL_POINTER = 0xF5F5F5 -local COL_POINTER_S = 0x444444 -local COL_WHITE = 0xFFFFFF -local COL_GLOW = 0xFFD600 -- winner highlight +local COL_BG = 0x050505 +local COL_RIM = 0x8B6914 +local COL_HUB = 0x1A1A1A +local COL_HUB_RING = 0x8B6914 +local COL_SEP = 0xE0E0E0 +local COL_WHITE = 0xFFFFFF +local COL_POINTER = 0xF5F5F5 +local COL_PTR_SHD = 0x444444 ---------------------------------------------------------------------- -- GPU / pixel primitives @@ -102,10 +89,7 @@ local COL_GLOW = 0xFFD600 -- winner highlight local gpu local PW, PH - --- Screen geometry (set in start()) -local CX, WY -- wheel centre x, wheel centre y -local RX, RY -- wheel half-width, half-height (perspective) +local CX, CY, R_OUTER, R_POCKET_IN local function px_rect(x, y, w, h, col) x = math.floor(x); y = math.floor(y) @@ -118,6 +102,29 @@ local function px_rect(x, y, w, h, col) gpu.filledRectangle(x, y, w, h, col) end +local function px_circle(cx, cy, r, col) + cx = math.floor(cx); cy = math.floor(cy); r = math.floor(r) + for dy = -r, r do + local half = math.floor(math.sqrt(r*r - dy*dy) + 0.5) + px_rect(cx - half, cy + dy, half*2 + 1, 1, col) + end +end + +local function px_annulus(cx, cy, r1, r2, col) + cx = math.floor(cx); cy = math.floor(cy) + r1 = math.floor(r1); r2 = math.floor(r2) + for dy = -r2, r2 do + local ho = math.floor(math.sqrt(math.max(0, r2*r2 - dy*dy)) + 0.5) + local hi = math.floor(math.sqrt(math.max(0, r1*r1 - dy*dy)) + 0.5) + if ho > hi then + px_rect(cx - ho, cy + dy, ho - hi, 1, col) + px_rect(cx + hi, cy + dy, ho - hi + 1, 1, col) + elseif hi == 0 then + px_rect(cx - ho, cy + dy, ho*2 + 1, 1, col) + end + end +end + local function px_text(str, x, y, fg, bg, size) pcall(gpu.drawText, math.floor(x), math.floor(y), str, fg, bg, size or 1, 0) end @@ -128,57 +135,12 @@ local function px_text_centre(str, y, fg, bg, size) px_text(str, CX - math.floor(w / 2), y, fg, bg, size) end --- Filled ellipse -local function px_ellipse(cx, cy, rx, ry, col) - cx = math.floor(cx); cy = math.floor(cy) - rx = math.floor(rx); ry = math.floor(ry) - if rx < 1 or ry < 1 then return end - for dy = -ry, ry do - local t = dy / ry - local half = math.floor(rx * math.sqrt(math.max(0, 1 - t*t)) + 0.5) - if half >= 1 then - px_rect(cx - half, cy + dy, half * 2 + 1, 1, col) - end - end -end - --- Ellipse annulus -local function px_ellipse_annulus(cx, cy, rx1, ry1, rx2, ry2, col) - cx = math.floor(cx); cy = math.floor(cy) - for dy = -ry2, ry2 do - local t2 = dy / ry2 - local ho = math.floor(rx2 * math.sqrt(math.max(0, 1 - t2*t2)) + 0.5) - local hi = 0 - if math.abs(dy) <= ry1 then - local t1 = dy / ry1 - hi = math.floor(rx1 * math.sqrt(math.max(0, 1 - t1*t1)) + 0.5) - end - if ho > hi then - px_rect(cx - ho, cy + dy, ho - hi, 1, col) - px_rect(cx + hi, cy + dy, ho - hi + 1, 1, col) - elseif hi == 0 then - px_rect(cx - ho, cy + dy, ho*2 + 1, 1, col) - end - end -end - ---------------------------------------------------------------------- -- Wheel drawing ---------------------------------------------------------------------- local segments = {} --- Convert wheel-local polar (angle, radius fraction) → screen (sx, sy) --- wheelAngle is the current rotation offset -local function wheelToScreen(a, f, wheelAngle) - local wa = a + wheelAngle - return CX + f * RX * math.cos(wa), - WY + f * RY * math.sin(wa) -end - --- Draw a single wedge of the perspective ellipse. --- seg.startA / seg.endA are in wheel-local angles. --- wheelAngle rotates the whole wheel. local function drawWedge(seg, wheelAngle, glowing) local col = seg.col if glowing then @@ -188,27 +150,24 @@ local function drawWedge(seg, wheelAngle, glowing) col = r * 0x10000 + g * 0x100 + b end - -- Rasterise the wedge by scanning screen rows within the ellipse bounding box. - local by0 = math.floor(WY - RY) - 1 - local by1 = math.ceil (WY + RY) + 1 - local bx0 = math.floor(CX - RX) - 1 - local bx1 = math.ceil (CX + RX) + 1 + local ri = R_POCKET_IN + local ro = R_OUTER + local a0 = seg.startA + wheelAngle + local arc = seg.endA - seg.startA - local a0 = seg.startA + wheelAngle - local a1 = seg.endA + wheelAngle - local arc = seg.endA - seg.startA -- always positive + local bx0 = math.floor(CX - ro) - 1 + local bx1 = math.ceil (CX + ro) + 1 + local by0 = math.floor(CY - ro) - 1 + local by1 = math.ceil (CY + ro) + 1 for sy = by0, by1 do - local dy = sy - WY - -- ellipse x half-span at this row - if math.abs(dy) <= RY then - local t = dy / RY - local xhalf = RX * math.sqrt(math.max(0, 1 - t*t)) - local runStart = nil - for sx = math.floor(CX - xhalf), math.ceil(CX + xhalf) do - local dx = sx - CX - local angle = math.atan2(dy / RY, dx / RX) -- ellipse-normalised angle - local rel = (angle - a0) % TWO_PI + local dy = sy - CY + local runStart = nil + for sx = bx0, bx1 do + local dx = sx - CX + local dist = math.sqrt(dx*dx + dy*dy) + if dist >= ri and dist <= ro then + local rel = (math.atan2(dy, dx) - a0) % TWO_PI if rel <= arc then if not runStart then runStart = sx end else @@ -217,29 +176,33 @@ local function drawWedge(seg, wheelAngle, glowing) runStart = nil end end + else + if runStart then + px_rect(runStart, sy, sx - runStart, 1, col) + runStart = nil + end end - if runStart then - px_rect(runStart, sy, math.ceil(CX + xhalf) - runStart + 1, 1, col) - end + end + if runStart then + px_rect(runStart, sy, bx1 - runStart + 1, 1, col) end end - -- Separator line at startA edge - local steps = math.floor(RX) - for i = 0, steps do - local f = i / steps - local sx = CX + f * RX * math.cos(a0) - local sy = WY + f * RY * math.sin(a0) + -- Separator spoke at startA + local a0w = seg.startA + wheelAngle + for i = 0, math.floor(ro - ri) do + local r = ri + i + local sx = CX + math.cos(a0w) * r + local sy = CY + math.sin(a0w) * r px_rect(math.floor(sx), math.floor(sy), 2, 1, COL_SEP) end - -- Label at wedge midpoint, ~70% radius - local midA = seg.midA + wheelAngle - local lx = CX + 0.70 * RX * math.cos(midA) - local ly = WY + 0.70 * RY * math.sin(midA) - local label = seg.name - local lsize = (RX > 80) and 1 or 1 - px_text(label, lx - math.floor(#label * 3), ly - 4, COL_WHITE, col, lsize) + -- Label at 72% radius, wedge midpoint + local midA = seg.midA + wheelAngle + local lr = (ri + ro) * 0.72 + local lx = CX + math.cos(midA) * lr + local ly = CY + math.sin(midA) * lr + px_text(seg.name, lx - math.floor(#seg.name * 3), ly - 4, COL_WHITE, col, 1) end local function drawAllWedges(wheelAngle, glowIdx) @@ -249,129 +212,88 @@ local function drawAllWedges(wheelAngle, glowIdx) end end -local function drawChrome(wheelAngle) - -- Outer rim (ellipse annulus, slightly larger than wheel) - local rimRX = RX + 6; local rimRY = RY + math.floor(6 * TILT) - px_ellipse_annulus(CX, WY, RX, RY, rimRX, rimRY, COL_RIM) - -- Inner hub - local hubRX = math.floor(RX * 0.10) - local hubRY = math.floor(RY * 0.10) - px_ellipse(CX, WY, hubRX + 3, hubRY + 3, COL_HUB_RING) - px_ellipse(CX, WY, hubRX, hubRY, COL_HUB) +local function drawChrome() + px_annulus(CX, CY, R_OUTER, R_OUTER + 7, COL_RIM) + -- Hub + px_circle(CX, CY, R_POCKET_IN, COL_HUB) + px_annulus(CX, CY, R_POCKET_IN - 4, R_POCKET_IN, COL_HUB_RING) + px_circle(CX, CY, 6, COL_HUB_RING) + px_circle(CX, CY, 3, COL_HUB) end -local function drawStand() - -- Two angled legs below the wheel - local baseY = WY + RY + 6 - local legBot = PH - 4 - local legW = 8 - -- Left leg - local lx1 = CX - math.floor(RX * 0.3) - local lx2 = CX - math.floor(RX * 0.7) - -- Draw as a trapezoid approximation with filled rects - local steps = legBot - baseY - for i = 0, steps do - local frac = i / math.max(1, steps) - local cx_l = lx1 + math.floor((lx2 - lx1) * frac) - px_rect(cx_l - math.floor(legW/2), baseY + i, legW, 1, COL_STAND) - end - -- Right leg - local rx1 = CX + math.floor(RX * 0.3) - local rx2 = CX + math.floor(RX * 0.7) - for i = 0, steps do - local frac = i / math.max(1, steps) - local cx_r = rx1 + math.floor((rx2 - rx1) * frac) - px_rect(cx_r - math.floor(legW/2), baseY + i, legW, 1, COL_STAND) - end - -- Horizontal crossbar - local barY = baseY + math.floor(steps * 0.55) - px_rect(lx2 - math.floor(legW/2), barY, rx2 - lx2 + legW, 6, COL_STAND_DRK) -end - --- Fixed pointer triangle at the top of the wheel (screen top, pointing down) +-- Fixed downward-pointing triangle at top of screen, above wheel local function drawPointer() local tipX = CX - local tipY = WY - RY - 5 -- just above the rim - local baseY = tipY - 18 - local halfW = 10 - -- Shadow - for i = 0, 18 do - local frac = i / 18 + local tipY = CY - R_OUTER - 8 + local baseY = tipY - 20 + local halfW = 11 + for i = 0, 20 do + local frac = i / 20 local hw = math.floor(halfW * (1 - frac)) + 1 - px_rect(tipX - hw + 2, baseY + i + 2, hw*2, 1, COL_POINTER_S) + px_rect(tipX - hw + 2, baseY + i + 2, hw*2, 1, COL_PTR_SHD) end - -- Arrow - for i = 0, 18 do - local frac = i / 18 + for i = 0, 20 do + local frac = i / 20 local hw = math.floor(halfW * (1 - frac)) + 1 px_rect(tipX - hw, baseY + i, hw*2, 1, COL_POINTER) end end +local function drawHubText(lines) + local r = R_POCKET_IN - 5 + px_circle(CX, CY, r, COL_HUB) + local lineH = 12 + local totalH = #lines * lineH + local startY = CY - math.floor(totalH / 2) + for i, line in ipairs(lines) do + local lx = CX - math.floor(#line * 6 / 2) + px_text(line, lx, startY + (i-1) * lineH, COL_WHITE, COL_HUB, 1) + end + px_annulus(CX, CY, R_POCKET_IN - 4, R_POCKET_IN, COL_HUB_RING) + gpu.sync() +end + local function drawWheelFull(wheelAngle, glowIdx) - -- Clear wheel area - local rimRX = RX + 8; local rimRY = RY + math.floor(8 * TILT) - px_ellipse(CX, WY, rimRX, rimRY, COL_BG) + px_circle(CX, CY, R_OUTER + 9, COL_BG) drawAllWedges(wheelAngle, glowIdx) - drawChrome(wheelAngle) + drawChrome() drawPointer() end ---------------------------------------------------------------------- --- Center / result text (drawn below wheel, above stand) +-- Segment under pointer +-- Pointer is at screen top = angle -pi/2. +-- Wheel-local angle = (-pi/2 - wheelAngle) mod TWO_PI ---------------------------------------------------------------------- -local function drawResultText(lines) - local y0 = WY + RY + math.floor(RY * 0.15) - -- Clear area - px_rect(1, y0, PW, 30, COL_BG) - for i, line in ipairs(lines) do - px_text_centre(line, y0 + (i-1) * 14, COL_WHITE, COL_BG, 1) +local function segmentAtPointer(wheelAngle) + local a = (-math.pi / 2 - wheelAngle) % TWO_PI + for i, seg in ipairs(segments) do + if a >= seg.startA and a < seg.endA then return i end end - gpu.sync() + return 1 end ---------------------------------------------------------------------- -- Spin physics ---------------------------------------------------------------------- --- Find which segment index is currently under the pointer. --- The pointer is at the top of the wheel = screen angle -pi/2. --- In wheel-local space that is angle (-pi/2 - wheelAngle) mod TWO_PI. -local function segmentAtPointer(wheelAngle) - local pointerLocalAngle = (-math.pi / 2 - wheelAngle) % TWO_PI - for i, seg in ipairs(segments) do - if pointerLocalAngle >= seg.startA and pointerLocalAngle < seg.endA then - return i - end - end - return 1 -end - local function spin() - local omega = OMEGA_MIN + math.random() * (OMEGA_MAX - OMEGA_MIN) - local angle = math.random() * TWO_PI -- random start rotation - local elapsed = 0 - local MAX_TIME = 30.0 + local omega = OMEGA_MIN + math.random() * (OMEGA_MAX - OMEGA_MIN) + local angle = math.random() * TWO_PI + local elapsed = 0 - -- Draw wheel at initial angle drawWheelFull(angle, nil) - gpu.sync() + drawHubText({ "SPINNING..." }) - while elapsed < MAX_TIME do - -- Decay angular velocity + while elapsed < 30.0 do omega = omega * FRICTION - - -- Integrate angle angle = angle + omega * FRAME_DELAY - -- Keep angle in [0, TWO_PI) to avoid float drift over long spins if angle > TWO_PI * 100 then angle = angle % TWO_PI end - -- Erase wheel, redraw at new angle - local rimRX = RX + 8; local rimRY = RY + math.floor(8 * TILT) - px_ellipse(CX, WY, rimRX, rimRY, COL_BG) + px_circle(CX, CY, R_OUTER + 9, COL_BG) drawAllWedges(angle, nil) - drawChrome(angle) + drawChrome() drawPointer() gpu.sync() @@ -381,21 +303,19 @@ local function spin() if omega < STOP_OMEGA then break end end - -- Final angle local winIdx = segmentAtPointer(angle) - -- Glow animation + -- Flash winning wedge for flash = 1, 7 do - local rimRX = RX + 8; local rimRY = RY + math.floor(8 * TILT) - px_ellipse(CX, WY, rimRX, rimRY, COL_BG) + px_circle(CX, CY, R_OUTER + 9, COL_BG) drawAllWedges(angle, flash % 2 == 1 and winIdx or nil) - drawChrome(angle) + drawChrome() drawPointer() gpu.sync() sleep(0.14) end - return winIdx + return winIdx, angle end ---------------------------------------------------------------------- @@ -431,18 +351,20 @@ local function start() error(("GPU pixel size %dx%d too small."):format(PW or 0, PH or 0)) end - -- Geometry: wheel sits in the upper ~60% of the screen - CX = math.floor(PW / 2) - RX = math.floor(math.min(PW, PH) / 2) - 12 - RY = math.floor(RX * TILT) - WY = math.floor(PH * 0.38) -- wheel centre sits above mid + CX = math.floor(PW / 2) + CY = math.floor(PH / 2) + local R_MAX = math.floor(math.min(PW, PH) / 2) - 16 + R_OUTER = R_MAX + R_POCKET_IN = math.floor(R_MAX * 0.28) segments = buildSegments() + -- Clear full screen before drawing anything gpu.fill(COL_BG) - drawStand() + gpu.sync() + drawWheelFull(0, nil) - drawResultText({ "Pull lever to spin!" }) + drawHubText({ "PRIZE WHEEL", "Pull lever" }) end local function stop() @@ -453,20 +375,17 @@ local function main() while true do waitForRedstonePulse() - drawResultText({ "SPINNING..." }) + local winIdx, finalAngle = spin() + local prize = segments[winIdx] - local winIdx = spin() - local prize = segments[winIdx] - - drawResultText({ "WINNER!", prize.name }) + drawHubText({ "WINNER!", prize.name }) sleep(5) - -- Redraw clean with a fresh random idle angle gpu.fill(COL_BG) - drawStand() - drawWheelFull(math.random() * TWO_PI, nil) - drawResultText({ "Pull lever to spin!" }) + gpu.sync() + drawWheelFull(finalAngle, nil) + drawHubText({ "PRIZE WHEEL", "Pull lever" }) end end diff --git a/programs/roulette.lua b/programs/roulette.lua index c8788a4..9e0e0fe 100644 --- a/programs/roulette.lua +++ b/programs/roulette.lua @@ -581,7 +581,10 @@ local function start() R_POCKET_IN = math.floor(R_MAX * 0.58) R_HUB = math.floor(R_MAX * 0.38) + -- Clear full screen before drawing anything gpu.fill(COL_BG) + gpu.sync() + drawWheelFull(nil) drawCenterText({ "ROULETTE", "Pull lever" }) end