diff --git a/programs/prizewheel.lua b/programs/prizewheel.lua index 5e60fd2..afea41d 100644 --- a/programs/prizewheel.lua +++ b/programs/prizewheel.lua @@ -68,10 +68,10 @@ local function buildSegments() end -- 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 +local OMEGA_MIN = 8.0 -- rad/s fast launch +local OMEGA_MAX = 16.0 +local FRICTION = 0.980 -- per frame — stops cleanly in ~4s +local STOP_OMEGA = 0.10 -- rad/s clean stop threshold, no wiggle -- Colours local COL_BG = 0x050505 @@ -136,85 +136,110 @@ local function px_text_centre(str, y, fg, bg, size) end ---------------------------------------------------------------------- --- Wheel drawing +-- Wheel drawing — single-pass rasteriser +-- +-- Instead of drawing each wedge separately (N passes over the full +-- bounding box), we do ONE pass: for every pixel inside the wheel +-- annulus, compute its angle and look up the wedge colour. +-- This is N× faster and eliminates the per-wedge sleep(0) during spin. ---------------------------------------------------------------------- local segments = {} -local function drawWedge(seg, wheelAngle, glowing) - local col = seg.col - if glowing then - local r = math.min(255, math.floor(col / 0x10000) + 80) - local g = math.min(255, math.floor((col % 0x10000) / 0x100) + 80) - local b = math.min(255, col % 0x100 + 80) - col = r * 0x10000 + g * 0x100 + b +-- Build a flat lookup: given a wheel-local angle in [0, TWO_PI), +-- return the segment index. Linear scan over 12 segments is fine. +local function segmentForAngle(a) + for i, seg in ipairs(segments) do + if a >= seg.startA and a < seg.endA then return i end end - - local ri = R_POCKET_IN - local ro = R_OUTER - local a0 = seg.startA + wheelAngle - local arc = seg.endA - seg.startA - - 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 - 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 - if runStart then - px_rect(runStart, sy, sx - runStart, 1, col) - runStart = nil - end - end - else - if runStart then - px_rect(runStart, sy, sx - runStart, 1, col) - runStart = nil - end - end - end - if runStart then - px_rect(runStart, sy, bx1 - runStart + 1, 1, col) - end - end - - -- 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 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) + return #segments -- wrap-around safety end -local function drawAllWedges(wheelAngle, glowIdx) +-- Draw the full wheel in one raster pass. +-- wheelAngle: current rotation offset (added to every wedge startA/endA). +-- glowIdx: if non-nil, that segment is brightened. +local function drawWheel(wheelAngle, glowIdx) + -- Precompute brightened colours so we don't recompute inside the loop + local cols = {} for i, seg in ipairs(segments) do - drawWedge(seg, wheelAngle, i == glowIdx) - sleep(0) + if i == glowIdx then + local c = seg.col + local r = math.min(255, math.floor(c / 0x10000) + 80) + local g = math.min(255, math.floor((c % 0x10000) / 0x100) + 80) + local b = math.min(255, c % 0x100 + 80) + cols[i] = r * 0x10000 + g * 0x100 + b + else + cols[i] = seg.col + end + end + + local ri = R_POCKET_IN + local ro = R_OUTER + local ri2 = ri * ri + local ro2 = ro * ro + + -- Normalise wheelAngle so angles stay in a sane range + local wa = wheelAngle % TWO_PI + + for sy = math.floor(CY - ro) - 1, math.ceil(CY + ro) + 1 do + local dy = sy - CY + local dy2 = dy * dy + if dy2 <= ro2 then + local xhalf = math.floor(math.sqrt(ro2 - dy2) + 0.5) + local runStart = nil + local runCol = nil + for sx = CX - xhalf, CX + xhalf do + local dx = sx - CX + local dx2 = dx * dx + local d2 = dx2 + dy2 + local col = nil + if d2 >= ri2 and d2 <= ro2 then + -- Inside annulus — find wedge + -- atan2 returns angle in world space; subtract wheelAngle + -- to get wheel-local angle, then mod into [0, TWO_PI) + local worldA = math.atan2(dy, dx) + local localA = (worldA - wa) % TWO_PI + local idx = segmentForAngle(localA) + col = cols[idx] + end + if col == runCol then + -- extend run (including col==nil for outside-annulus gaps) + else + if runCol and runStart then + px_rect(runStart, sy, sx - runStart, 1, runCol) + end + runStart = sx + runCol = col + end + end + -- flush last run + if runCol and runStart then + local xend = CX + xhalf + 1 + px_rect(runStart, sy, xend - runStart, 1, runCol) + end + end + end + + -- Separator spokes — draw after fill so they appear on top + for i, seg in ipairs(segments) do + local a = seg.startA + wa + for step = 0, math.floor(R_OUTER - R_POCKET_IN) do + local rr = R_POCKET_IN + step + px_rect(math.floor(CX + math.cos(a) * rr), + math.floor(CY + math.sin(a) * rr), 2, 1, COL_SEP) + end + -- Label + local midA = seg.midA + wa + local lr = (R_POCKET_IN + R_OUTER) * 0.68 + 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, cols[i], 1) end end 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) @@ -255,7 +280,7 @@ end local function drawWheelFull(wheelAngle, glowIdx) px_circle(CX, CY, R_OUTER + 9, COL_BG) - drawAllWedges(wheelAngle, glowIdx) + drawWheel(wheelAngle, glowIdx) drawChrome() drawPointer() end @@ -281,38 +306,39 @@ end local function spin() local omega = OMEGA_MIN + math.random() * (OMEGA_MAX - OMEGA_MIN) local angle = math.random() * TWO_PI - local elapsed = 0 + -- Initial draw drawWheelFull(angle, nil) drawHubText({ "SPINNING..." }) - while elapsed < 30.0 do + local frameCount = 0 + while true do omega = omega * FRICTION angle = angle + omega * FRAME_DELAY if angle > TWO_PI * 100 then angle = angle % TWO_PI end - px_circle(CX, CY, R_OUTER + 9, COL_BG) - drawAllWedges(angle, nil) + -- drawWheel overwrites every annulus pixel — no need to clear first + drawWheel(angle, nil) drawChrome() drawPointer() gpu.sync() - sleep(FRAME_DELAY) - elapsed = elapsed + FRAME_DELAY + -- Yield to OS every 4 frames to avoid CC timeout without a sleep(0) overhead + frameCount = frameCount + 1 + if frameCount % 4 == 0 then sleep(0) end if omega < STOP_OMEGA then break end end local winIdx = segmentAtPointer(angle) - -- Flash winning wedge - for flash = 1, 7 do - px_circle(CX, CY, R_OUTER + 9, COL_BG) - drawAllWedges(angle, flash % 2 == 1 and winIdx or nil) + -- Flash winning wedge — 6 flashes at 120ms each + for flash = 1, 6 do + drawWheel(angle, flash % 2 == 1 and winIdx or nil) drawChrome() drawPointer() gpu.sync() - sleep(0.14) + sleep(0.12) end return winIdx, angle diff --git a/programs/roulette.lua b/programs/roulette.lua index 310a41c..89816bb 100644 --- a/programs/roulette.lua +++ b/programs/roulette.lua @@ -76,24 +76,23 @@ local COL_BALL_SHD = 0x444444 local BALL_RADIUS = 8 -- px (screen drawing radius) local BALL_WORLD_R = 5 -- physics sphere radius in world units --- Initial tangential speed (world units / s) -local BALL_SPEED_MIN = 700 -local BALL_SPEED_MAX = 1000 +-- Initial tangential speed (world units / s) — very fast launch +local BALL_SPEED_MIN = 1800 +local BALL_SPEED_MAX = 2800 -- Gravity (world units / s²) local GRAVITY = 1800 --- Bowl cone half-angle from horizontal (radians) — steeper = faster slide +-- Bowl cone half-angle from horizontal (radians) local BOWL_SLOPE = math.pi / 9 -- 20 degrees --- Pocket well is deeper — steeper slope local POCKET_SLOPE = math.pi / 5 -- 36 degrees --- Restitution on bowl surface normal bounce +-- Restitution on wall bounce local RESTITUTION_WALL = 0.82 local RESTITUTION_POCKET = 0.68 --- Rolling friction: velocity multiplier per second on the bowl surface -local FRICTION_ROLL = 0.988 -- per frame on track -local FRICTION_POCKET = 0.970 -- per frame in pocket --- Ball drops from track into pocket ring when its radial position --- crosses the inner deflector radius -local BOUNCE_KICK_MAX = 0.08 -- rad random angular kick on rim bounce +-- Rolling friction: multiplier per frame — strong enough to stop cleanly +local FRICTION_ROLL = 0.975 -- per frame (~0.43/s at 33fps — decays in ~5s) +local FRICTION_POCKET = 0.955 -- pocket damps faster +-- Stop when horizontal speed drops below this — no wiggle +local STOP_SPEED = 18 -- px/s +local BOUNCE_KICK_MAX = 0.08 -- rad ---------------------------------------------------------------------- -- GPU / pixel primitives @@ -242,74 +241,62 @@ end local ballX, ballY = 0, 0 -- Repair the wheel pixels inside a circle of radius r centred on (cx,cy). --- Re-rasterises all wedges clipped to that bounding box, then chrome on top. --- This replaces the old flat-colour eraseBall which left coloured smears. +-- Single pass: for every pixel in the bounding box, compute the correct +-- wheel colour and paint it. No flat-colour approximation. local function repairWheelPatch(cx, cy, r) cx = math.floor(cx); cy = math.floor(cy) - -- Bounding box for the patch local px0 = cx - r; local px1 = cx + r local py0 = cy - r; local py1 = cy + r - -- For each wedge, re-rasterise only within this bounding box + -- Pre-compute per-wedge geometry (angle extents) once + local halfArc = math.pi / NUM_POCKETS + local wedges = {} for slotIdx = 1, NUM_POCKETS do - local halfArc = math.pi / NUM_POCKETS - local midAngle = FIXED_ROTOR + (slotIdx - 1) * TWO_PI / NUM_POCKETS - local a0 = midAngle - halfArc - local a1 = midAngle + halfArc - local arc = (a1 - a0) % TWO_PI - - local num = WHEEL_ORDER[slotIdx] - local col = pocketColor(num) - - local ri, ro = R_POCKET_IN, R_POCKET_OUT - - for sy = py0, py1 do - local runStart = nil - for sx = px0, px1 do - local dx = sx - CX; local dy = sy - CY - local dist = math.sqrt(dx*dx + dy*dy) - local inRing = dist >= ri and dist <= ro - local rel = (math.atan2(dy, dx) - a0) % TWO_PI - local inWedge = rel <= arc - if inRing and inWedge then - if not runStart then runStart = sx end - else - if runStart then - px_rect(runStart, sy, sx - runStart, 1, col) - runStart = nil - end - end - end - if runStart then - px_rect(runStart, sy, px1 - runStart + 1, 1, col) - end - end + local mid = FIXED_ROTOR + (slotIdx - 1) * TWO_PI / NUM_POCKETS + wedges[slotIdx] = { + a0 = mid - halfArc, + arc = TWO_PI / NUM_POCKETS, + col = pocketColor(WHEEL_ORDER[slotIdx]), + } end - -- Repaint chrome rings and background that overlap the patch, - -- in inside-out priority order using elseif so each pixel is set once. for sy = py0, py1 do for sx = px0, px1 do - local dx = sx - CX; local dy = sy - CY - local d = math.sqrt(dx*dx + dy*dy) + local dx = sx - CX + local dy = sy - CY + local d = math.sqrt(dx*dx + dy*dy) + local col + + -- Determine correct colour for this pixel, inside-out priority if d <= R_HUB - 4 then - px_rect(sx, sy, 1, 1, COL_HUB) + col = COL_HUB elseif d <= R_HUB then - px_rect(sx, sy, 1, 1, COL_HUB_RING) + col = COL_HUB_RING + elseif d <= R_POCKET_IN - 2 then + col = COL_HUB -- gap between hub ring and inner rim elseif d <= R_POCKET_IN then - -- gap between hub ring and inner rim — plain hub bg - px_rect(sx, sy, 1, 1, COL_HUB) - elseif d <= R_POCKET_IN + 2 then - px_rect(sx, sy, 1, 1, COL_RIM) + col = COL_RIM elseif d <= R_POCKET_OUT then - -- inside pocket ring — wedge already painted above + -- Pocket ring: find which wedge this pixel belongs to + local angle = math.atan2(dy, dx) + for _, w in ipairs(wedges) do + local rel = (angle - w.a0) % TWO_PI + if rel <= w.arc then + col = w.col + break + end + end + if not col then col = COL_HUB end -- fallback elseif d <= R_POCKET_OUT + 2 then - px_rect(sx, sy, 1, 1, COL_RIM) + col = COL_RIM elseif d <= R_OUTER - 6 then - px_rect(sx, sy, 1, 1, COL_TRACK) + col = COL_TRACK elseif d <= R_OUTER then - px_rect(sx, sy, 1, 1, COL_RIM) + col = COL_RIM end + -- d > R_OUTER: outside wheel, leave as-is (ball never goes there) + + if col then px_rect(sx, sy, 1, 1, col) end end end end @@ -545,7 +532,7 @@ local function spin() -- Settled when horizontal speed is very low local hspd = math.sqrt(vx*vx + vy*vy) - if hspd < 5 then break end + if hspd < STOP_SPEED then break end end eraseBall3()