-- Roulette Machine -- Tom's Peripherals GPU + screen wall (832x448 or any size). -- -- Layout (all pixel-space): -- Pocket ring : 1 block (64px) wide border around the edge, numbered -- Ball track : lane just inside the pocket ring -- Center : status text -- Drop-in : ball falls from top, bounces a few times, rolls into track ---------------------------------------------------------------------- -- GPU discovery ---------------------------------------------------------------------- local function findGPU() print("[roulette] 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("[roulette] Using GPU: " .. name) return peripheral.wrap(name) end end return nil end ---------------------------------------------------------------------- -- Constants / tunables ---------------------------------------------------------------------- local POCKET_SIZE = 64 -- px per pocket cell (1 block) local BALL_RADIUS = 16 -- px radius of the ball circle local TRACK_INSET = 88 -- px from screen edge to ball centre track local FRAME_DELAY = 0.02 -- seconds per frame (~50 fps) -- Drop-in physics local GRAVITY = 900 -- px/s^2 local BOUNCE_DAMPING = 0.52 -- speed kept after each wall bounce local SPRING_K = 6.0 -- spring constant pulling ball to target local SPRING_DAMP = 2.8 -- spring damping coefficient -- Spin physics local SPIN_SPEED_MIN = 18.0 -- pockets/sec initial angular speed local SPIN_SPEED_MAX = 26.0 local SPIN_FRICTION = 1.6 -- pockets/sec^2 deceleration local COL_RED = 0xE53935 local COL_BLACK = 0x212121 local COL_GREEN = 0x2E7D32 local COL_WHITE = 0xFFFFFF local COL_BALL = 0xF5F5F5 local COL_BG = 0x050505 local COL_TRACK = 0x1A1A1A local COL_GLOW_RED = 0xFF8A80 local COL_GLOW_BLACK = 0x9E9E9E local COL_GLOW_GREEN = 0xA5D6A7 local COL_NUM_LIGHT = 0xFFFFFF -- number colour on dark pockets local COL_NUM_DARK = 0x111111 -- number colour on light/glow pockets ---------------------------------------------------------------------- -- GPU / pixel drawing layer ---------------------------------------------------------------------- local gpu local PW, PH -- pixel dimensions of wall 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) 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 -- Draw a ring (outline circle) for glow effect. local function px_ring(cx, cy, r, thickness, col) for t = 0, thickness - 1 do local ro = r + t local ri = r + t - 1 for dy = -ro, ro do local ho = math.floor(math.sqrt(math.max(0, ro*ro - dy*dy)) + 0.5) local hi = math.floor(math.sqrt(math.max(0, ri*ri - 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, col) end end end end local function px_text(str, px, py, fg, bg, size) pcall(gpu.drawText, math.floor(px), math.floor(py), str, fg, bg, size or 1, 0) end local function px_text_centre(str, py, fg, bg, size) size = size or 2 -- gpu font: each char ~6px wide at size 1 + 1px padding = 7px local charW = 7 * size local approxW = #str * charW local x = math.max(1, math.floor((PW - approxW) / 2)) px_text(str, x, py, fg, bg, size) end ---------------------------------------------------------------------- -- Pocket layout ---------------------------------------------------------------------- local pockets = {} local NUM_POCKETS local function buildPockets() pockets = {} local cols = math.floor(PW / POCKET_SIZE) local rows = math.floor(PH / POCKET_SIZE) local half = math.floor(POCKET_SIZE / 2) local function add(cx, cy) table.insert(pockets, { cx = cx, cy = cy }) end -- Clockwise: top, right, bottom R->L, left B->T (no double corners) for i = 0, cols - 1 do add(i * POCKET_SIZE + half, half) end for i = 1, rows - 1 do add(PW - half, i * POCKET_SIZE + half) end for i = cols - 1, 1, -1 do add(i * POCKET_SIZE + half, PH - half) end for i = rows - 1, 1, -1 do add(half, i * POCKET_SIZE + half) end NUM_POCKETS = #pockets for i, p in ipairs(pockets) do if i == 1 then p.color = COL_GREEN p.glowColor = COL_GLOW_GREEN p.label = "0" else if i % 2 == 0 then p.color = COL_RED p.glowColor = COL_GLOW_RED else p.color = COL_BLACK p.glowColor = COL_GLOW_BLACK end p.label = tostring(i - 1) end -- Track position: clamp inward from each edge. p.track_cx = math.max(TRACK_INSET, math.min(PW - TRACK_INSET, p.cx)) p.track_cy = math.max(TRACK_INSET, math.min(PH - TRACK_INSET, p.cy)) end end ---------------------------------------------------------------------- -- Drawing helpers ---------------------------------------------------------------------- local function drawPocket(p, glowing) local x = p.cx - math.floor(POCKET_SIZE / 2) local y = p.cy - math.floor(POCKET_SIZE / 2) local bg = glowing and p.glowColor or p.color -- Fill body px_rect(x + 1, y + 1, POCKET_SIZE - 2, POCKET_SIZE - 2, bg) -- Border px_rect(x, y, POCKET_SIZE, 1, COL_BG) px_rect(x, y+POCKET_SIZE-1, POCKET_SIZE, 1, COL_BG) px_rect(x, y, 1, POCKET_SIZE, COL_BG) px_rect(x+POCKET_SIZE-1, y, 1, POCKET_SIZE, COL_BG) -- Number: centred in cell, size 2 local numFg = (glowing or p.color == COL_BLACK) and COL_WHITE or COL_NUM_DARK -- Each char ~6px wide at size 2 = ~12px; 1-digit = 12px, 2-digit = 24px local numW = #p.label * 12 local nx = x + math.floor((POCKET_SIZE - numW) / 2) local ny = y + math.floor((POCKET_SIZE - 16) / 2) -- 16px tall at size 2 px_text(p.label, nx, ny, numFg, bg, 2) end local function drawAllPockets(glowIdx) for i, p in ipairs(pockets) do drawPocket(p, i == glowIdx) end end local function drawTrack() px_rect(POCKET_SIZE + 1, POCKET_SIZE + 1, PW - POCKET_SIZE * 2 - 2, PH - POCKET_SIZE * 2 - 2, COL_TRACK) end local function drawCenter(lines, textSize) textSize = textSize or 2 local margin = POCKET_SIZE + BALL_RADIUS * 3 px_rect(margin, margin, PW - margin * 2, PH - margin * 2, COL_BG) local lineH = 10 * textSize local totalH = #lines * lineH local startY = math.floor((PH - totalH) / 2) for i, line in ipairs(lines) do px_text_centre(line, startY + (i - 1) * lineH, COL_WHITE, COL_BG, textSize) end end ---------------------------------------------------------------------- -- Ball ---------------------------------------------------------------------- local ballX, ballY = 0, 0 local function eraseBall(bgCol) px_circle(ballX, ballY, BALL_RADIUS + 2, bgCol or COL_TRACK) end local function drawBallAt(x, y) ballX = math.floor(x) ballY = math.floor(y) -- subtle shadow px_circle(ballX + 2, ballY + 2, BALL_RADIUS, 0x333333) -- white ball px_circle(ballX, ballY, BALL_RADIUS, COL_BALL) -- highlight glint px_circle(ballX - 5, ballY - 5, 4, COL_WHITE) end ---------------------------------------------------------------------- -- Drop-in animation -- Ball spawns above screen center, falls under gravity, bounces off -- track walls. After 2 bounces a spring takes over and homes it to -- the target pocket position. ---------------------------------------------------------------------- local function dropInAnimation(targetX, targetY) local bx = PW / 2 local by = -BALL_RADIUS - 10 local vx = (targetX - bx) * 0.25 local vy = 80 local minX = TRACK_INSET - BALL_RADIUS local maxX = PW - TRACK_INSET + BALL_RADIUS local minY = TRACK_INSET - BALL_RADIUS local maxY = PH - TRACK_INSET + BALL_RADIUS local dt = FRAME_DELAY local elapsed = 0 local MAX_TIME = 4.0 local bounces = 0 local springing = false while elapsed < MAX_TIME do if not springing then vy = vy + GRAVITY * dt bx = bx + vx * dt by = by + vy * dt local hit = false if bx < minX then bx = minX; vx = math.abs(vx) * BOUNCE_DAMPING; hit = true end if bx > maxX then bx = maxX; vx = -math.abs(vx) * BOUNCE_DAMPING; hit = true end if by < minY then by = minY; vy = math.abs(vy) * BOUNCE_DAMPING; hit = true end if by > maxY then by = maxY; vy = -math.abs(vy) * BOUNCE_DAMPING; hit = true end if hit then bounces = bounces + 1 if bounces >= 2 then springing = true end end else local dx = targetX - bx local dy = targetY - by vx = vx + dx * SPRING_K * dt vy = vy + dy * SPRING_K * dt vx = vx - vx * SPRING_DAMP * dt vy = vy - vy * SPRING_DAMP * dt bx = bx + vx * dt by = by + vy * dt local speed = math.sqrt(vx*vx + vy*vy) local dist = math.sqrt(dx*dx + dy*dy) if dist < 2 and speed < 4 then break end end eraseBall(COL_TRACK) drawBallAt(bx, by) gpu.sync() sleep(dt) elapsed = elapsed + dt end eraseBall(COL_TRACK) drawBallAt(targetX, targetY) gpu.sync() end ---------------------------------------------------------------------- -- Win glow animation ---------------------------------------------------------------------- local function glowAnimation(pocketIdx) local p = pockets[pocketIdx] for flash = 1, 6 do drawPocket(p, flash % 2 == 1) -- alternate glow/normal gpu.sync() sleep(0.18) end -- Leave glowing drawPocket(p, true) gpu.sync() end ---------------------------------------------------------------------- -- Spin logic — friction-based physics -- Ball starts with a random high angular speed (pockets/sec) and -- decelerates under constant friction until it naturally stops. ---------------------------------------------------------------------- local function pocketPos(idx) local p = pockets[idx] return p.track_cx, p.track_cy end local function lerpPos(i1, i2, t) local x1, y1 = pocketPos(i1) local x2, y2 = pocketPos(i2) return x1 + (x2 - x1) * t, y1 + (y2 - y1) * t end local currentPocketIdx = 1 local function spin() local n = NUM_POCKETS local speed = SPIN_SPEED_MIN + math.random() * (SPIN_SPEED_MAX - SPIN_SPEED_MIN) local posF = currentPocketIdx - 1 -- fractional pocket index (0-based) local dt = FRAME_DELAY while speed > 0 do speed = speed - SPIN_FRICTION * dt if speed < 0 then speed = 0 end posF = (posF + speed * dt) % n local idxLow = math.floor(posF) % n + 1 local idxHi = idxLow % n + 1 local frac = posF - math.floor(posF) eraseBall(COL_TRACK) local bx, by = lerpPos(idxLow, idxHi, frac) drawBallAt(bx, by) gpu.sync() sleep(dt) end -- Snap to nearest pocket local finalIdx = math.floor(posF + 0.5) % n + 1 eraseBall(COL_TRACK) local fx, fy = pocketPos(finalIdx) drawBallAt(fx, fy) gpu.sync() currentPocketIdx = finalIdx return pockets[finalIdx], finalIdx 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(("[roulette] GPU: %dx%d px"):format(PW, PH)) if not PW or PW < 64 or PH < 64 then error(("GPU pixel size %dx%d too small."):format(PW or 0, PH or 0)) end buildPockets() print(("[roulette] %d pockets"):format(NUM_POCKETS)) gpu.fill(COL_BG) drawTrack() drawAllPockets() -- Drop the ball in from the top to its starting position. local sx, sy = pocketPos(1) dropInAnimation(sx, sy) currentPocketIdx = 1 drawCenter({ "ROULETTE", "Pull lever to spin" }) gpu.sync() end local function stop() if gpu then gpu.fill(COL_BG); gpu.sync() end end local function waitForRedstonePulse() while true do os.pullEvent("redstone") for _, side in ipairs(redstone.getSides()) do if redstone.getInput(side) then return side end end end end local function main() while true do waitForRedstonePulse() drawCenter({ "SPINNING..." }) gpu.sync() sleep(0.2) local pocket, pocketIdx = spin() -- Glow the winning pocket glowAnimation(pocketIdx) -- Announce winner local name = "BLACK" if pocket.color == COL_RED then name = "RED" end if pocket.color == COL_GREEN then name = "GREEN" end drawCenter({ "WINNER!", name .. " " .. pocket.label }, 3) gpu.sync() sleep(5) -- Reset: redraw board, drop ball back in to winning pocket position. drawTrack() drawAllPockets() local fx, fy = pocketPos(currentPocketIdx) dropInAnimation(fx, fy) drawCenter({ "ROULETTE", "Pull lever to spin" }) gpu.sync() end end return { start = start, stop = stop, main = main }