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

393 lines
12 KiB
Lua

-- 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 = 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
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
----------------------------------------------------------------------
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
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)
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()
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
-- 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)
drawAllWedges(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
local elapsed = 0
drawWheelFull(angle, nil)
drawHubText({ "SPINNING..." })
while elapsed < 30.0 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)
drawChrome()
drawPointer()
gpu.sync()
sleep(FRAME_DELAY)
elapsed = elapsed + FRAME_DELAY
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)
drawChrome()
drawPointer()
gpu.sync()
sleep(0.14)
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 }