-- Prize Wheel — spinning prize wheel, top-down view -- Tom's Peripherals GPU + screen wall. -- -- The wheel fills the screen as a circle (top-down, like roulette). -- It physically rotates: angular velocity starts high and decays under -- friction. A fixed pointer arrow at the top selects the prize. -- -- The wheel IS the thing that spins — wedges rotate each frame. -- Centre hub shows the current prize name when stopped. ---------------------------------------------------------------------- -- GPU discovery ---------------------------------------------------------------------- local function findGPU() print("[prizewheel] Scanning peripherals...") for _, name in ipairs(peripheral.getNames()) do local t = peripheral.getType(name) print(" " .. name .. " = " .. tostring(t)) if t and t:find("gpu") then print("[prizewheel] Using GPU: " .. name) return peripheral.wrap(name) end end return nil end ---------------------------------------------------------------------- -- Constants ---------------------------------------------------------------------- local FRAME_DELAY = 0.033 local TWO_PI = math.pi * 2 -- Prize segments — name, colour, relative weight local PRIZES = { { 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 }, } local function buildSegments() local total = 0 for _, p in ipairs(PRIZES) do total = total + p.weight end local segs = {} 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 = cum, endA = cum + arc, midA = cum + arc / 2, } cum = cum + arc end return segs end -- Spin physics 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 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 ---------------------------------------------------------------------- local gpu local PW, PH 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) w = math.floor(w); h = math.floor(h) if x < 1 then w = w + x - 1; x = 1 end if y < 1 then h = h + y - 1; y = 1 end if x + w - 1 > PW then w = PW - x + 1 end if y + h - 1 > PH then h = PH - y + 1 end if w < 1 or h < 1 then return end 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 local function px_text_centre(str, y, fg, bg, size) size = size or 1 local w = #str * 6 * size px_text(str, CX - math.floor(w / 2), y, fg, bg, size) end ---------------------------------------------------------------------- -- 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 = {} -- 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 return #segments -- wrap-around safety end -- 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 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) 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 -- Fixed downward-pointing triangle at top of screen, above wheel local function drawPointer() local tipX = CX 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_PTR_SHD) end 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) px_circle(CX, CY, R_OUTER + 9, COL_BG) drawWheel(wheelAngle, glowIdx) drawChrome() drawPointer() end ---------------------------------------------------------------------- -- Segment under pointer -- Pointer is at screen top = angle -pi/2. -- Wheel-local angle = (-pi/2 - wheelAngle) mod TWO_PI ---------------------------------------------------------------------- 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 return 1 end ---------------------------------------------------------------------- -- Spin physics ---------------------------------------------------------------------- local function spin() local omega = OMEGA_MIN + math.random() * (OMEGA_MAX - OMEGA_MIN) local angle = math.random() * TWO_PI -- Initial draw drawWheelFull(angle, nil) drawHubText({ "SPINNING..." }) 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 -- drawWheel overwrites every annulus pixel — no need to clear first drawWheel(angle, nil) drawChrome() drawPointer() gpu.sync() -- 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 — 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.12) end return winIdx, angle end ---------------------------------------------------------------------- -- Redstone helper ---------------------------------------------------------------------- local function waitForRedstonePulse() while true do os.pullEvent("redstone") for _, side in ipairs(redstone.getSides()) do if redstone.getInput(side) then return end end end end ---------------------------------------------------------------------- -- Lifecycle ---------------------------------------------------------------------- local function start() math.randomseed(os.epoch("utc")) gpu = findGPU() if not gpu then error("No GPU peripheral found.") end gpu.refreshSize() sleep(0) gpu.setSize(64) PW, PH = gpu.getSize() print(("[prizewheel] GPU: %dx%d px"):format(PW, PH)) if not PW or PW < 128 or PH < 128 then error(("GPU pixel size %dx%d too small."):format(PW or 0, PH or 0)) end 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) gpu.sync() drawWheelFull(0, nil) drawHubText({ "PRIZE WHEEL", "Pull lever" }) end local function stop() if gpu then gpu.fill(COL_BG); gpu.sync() end end local function main() while true do waitForRedstonePulse() local winIdx, finalAngle = spin() local prize = segments[winIdx] drawHubText({ "WINNER!", prize.name }) sleep(5) gpu.fill(COL_BG) gpu.sync() drawWheelFull(finalAngle, nil) drawHubText({ "PRIZE WHEEL", "Pull lever" }) end end return { start = start, stop = stop, main = main }