Files
nova-corp/programs/prizewheel.lua
2026-05-05 20:38:40 -04:00

419 lines
13 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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 }