-- Prize Wheel — spinning prize wheel, perspective view -- Tom's Peripherals GPU + screen wall. -- -- The wheel is drawn as a perspective ellipse (top tilted away from viewer). -- It physically rotates: angular velocity starts high and decays under -- friction until it stops. A fixed pointer 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. ---------------------------------------------------------------------- -- 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 -- ~30 fps 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) 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 } -- 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 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, } cumAngle = cumAngle + 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 -- 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 ---------------------------------------------------------------------- -- GPU / pixel primitives ---------------------------------------------------------------------- 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 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_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 -- 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 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 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 a0 = seg.startA + wheelAngle local a1 = seg.endA + wheelAngle local arc = seg.endA - seg.startA -- always positive 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 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 end if runStart then px_rect(runStart, sy, math.ceil(CX + xhalf) - runStart + 1, 1, col) end 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) 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) end local function drawAllWedges(wheelAngle, glowIdx) for i, seg in ipairs(segments) do drawWedge(seg, wheelAngle, i == glowIdx) sleep(0) 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) 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) 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 hw = math.floor(halfW * (1 - frac)) + 1 px_rect(tipX - hw + 2, baseY + i + 2, hw*2, 1, COL_POINTER_S) end -- Arrow for i = 0, 18 do local frac = i / 18 local hw = math.floor(halfW * (1 - frac)) + 1 px_rect(tipX - hw, baseY + i, hw*2, 1, COL_POINTER) end 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) drawAllWedges(wheelAngle, glowIdx) drawChrome(wheelAngle) drawPointer() end ---------------------------------------------------------------------- -- Center / result text (drawn below wheel, above stand) ---------------------------------------------------------------------- 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) end gpu.sync() 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 -- Draw wheel at initial angle drawWheelFull(angle, nil) gpu.sync() while elapsed < MAX_TIME do -- Decay angular velocity 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) drawAllWedges(angle, nil) drawChrome(angle) drawPointer() gpu.sync() sleep(FRAME_DELAY) elapsed = elapsed + FRAME_DELAY if omega < STOP_OMEGA then break end end -- Final angle local winIdx = segmentAtPointer(angle) -- Glow animation for flash = 1, 7 do local rimRX = RX + 8; local rimRY = RY + math.floor(8 * TILT) px_ellipse(CX, WY, rimRX, rimRY, COL_BG) drawAllWedges(angle, flash % 2 == 1 and winIdx or nil) drawChrome(angle) drawPointer() gpu.sync() sleep(0.14) end return winIdx 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 -- 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 segments = buildSegments() gpu.fill(COL_BG) drawStand() drawWheelFull(0, nil) drawResultText({ "Pull lever to spin!" }) end local function stop() if gpu then gpu.fill(COL_BG); gpu.sync() end end local function main() while true do waitForRedstonePulse() drawResultText({ "SPINNING..." }) local winIdx = spin() local prize = segments[winIdx] drawResultText({ "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!" }) end end return { start = start, stop = stop, main = main }