-- 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 -- Ball track : a lane just inside the pocket ring where the ball rolls -- Center : status text ---------------------------------------------------------------------- -- 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 = 18 -- px radius of the ball circle local TRACK_INSET = 80 -- px from screen edge to ball centre track local SPIN_TIME_MIN = 4 -- seconds local SPIN_TIME_MAX = 7 local FRAME_DELAY = 0.02 -- seconds per frame (~50 fps target) 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 -- subtle track lane colour ---------------------------------------------------------------------- -- GPU / pixel drawing layer ---------------------------------------------------------------------- local gpu local PW, PH -- pixel width/height of wall local function px_rect(x, y, w, h, col) -- clamp to screen 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 -- Draw a filled circle at pixel centre (cx, cy) with given radius. local function px_circle(cx, cy, r, col) 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 text centred horizontally at pixel y, using drawText. local function px_text_centre(str, py, fg, bg, size) size = size or 2 -- Each char is ~8px wide at size 1; rough estimate for centering. local charW = 8 * size local approxW = #str * charW local px = math.max(1, math.floor((PW - approxW) / 2)) pcall(gpu.drawText, px, py, str, fg, bg, size, 1) end ---------------------------------------------------------------------- -- Pocket layout ---------------------------------------------------------------------- -- Pockets are arranged clockwise around the perimeter, each POCKET_SIZE px. -- We store the pixel centre of each pocket. local pockets = {} -- { cx, cy, color, label, track_cx, track_cy } local NUM_POCKETS local function buildPockets() pockets = {} -- Number of pockets that fit each edge (integer, no partial pockets). local cols = math.floor(PW / POCKET_SIZE) -- top & bottom edges local rows = math.floor(PH / POCKET_SIZE) -- left & right edges -- Clockwise: top L->R, right T->B, bottom R->L, left B->T -- Avoid double-counting corners by using the top/bottom for full width -- and left/right for inner height only. local function add(cx, cy) table.insert(pockets, { cx = cx, cy = cy }) end local half = math.floor(POCKET_SIZE / 2) -- Top edge for i = 0, cols - 1 do add(i * POCKET_SIZE + half, half) end -- Right edge (skip top-right corner already added) for i = 1, rows - 1 do add(PW - half, i * POCKET_SIZE + half) end -- Bottom edge R->L (skip bottom-right corner) for i = cols - 1, 1, -1 do add(i * POCKET_SIZE + half, PH - half) end -- Left edge B->T (skip bottom-left and top-left corners) for i = rows - 1, 1, -1 do add(half, i * POCKET_SIZE + half) end NUM_POCKETS = #pockets -- Assign colours: pocket 1 = green (0), rest alternate red/black. for i, p in ipairs(pockets) do if i == 1 then p.color = COL_GREEN; p.label = "0" else p.color = (i % 2 == 0) and COL_RED or COL_BLACK p.label = tostring(i - 1) end end -- Compute ball track centre for each pocket (inset from screen edge). for _, p in ipairs(pockets) do local tx = math.max(TRACK_INSET, math.min(PW - TRACK_INSET, p.cx)) local ty = math.max(TRACK_INSET, math.min(PH - TRACK_INSET, p.cy)) p.track_cx = tx p.track_cy = ty end end ---------------------------------------------------------------------- -- Drawing ---------------------------------------------------------------------- local function drawPocket(p, highlight) local x = p.cx - math.floor(POCKET_SIZE / 2) local y = p.cy - math.floor(POCKET_SIZE / 2) local col = highlight and COL_WHITE or p.color px_rect(x + 1, y + 1, POCKET_SIZE - 2, POCKET_SIZE - 2, col) -- thin dark 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) end local function drawAllPockets() for _, p in ipairs(pockets) do drawPocket(p, false) end end local function drawTrack() -- Fill the interior (inside pocket ring) with track colour. px_rect(POCKET_SIZE + 1, POCKET_SIZE + 1, PW - POCKET_SIZE * 2 - 1, PH - POCKET_SIZE * 2 - 1, COL_TRACK) end local function drawCenter(lines, textSize) textSize = textSize or 2 -- Clear centre area. 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 (pixel-space, smooth) ---------------------------------------------------------------------- local ballX, ballY = 0, 0 local lastBallPocket = nil local function eraseBall() -- Redraw the track patch under where the ball was. px_circle(ballX, ballY, BALL_RADIUS + 1, COL_TRACK) end local function drawBallAt(x, y) ballX, ballY = x, y px_circle(x, y, BALL_RADIUS, COL_BALL) end ---------------------------------------------------------------------- -- Spin logic ---------------------------------------------------------------------- -- Convert pocket index to a continuous angle (radians) position -- so we can interpolate smoothly. -- We map pocket index to a fractional position [0, NUM_POCKETS). local function pocketPos(idx) -- Returns the track cx, cy for a given pocket index. local p = pockets[idx] return p.track_cx, p.track_cy end -- Lerp between two pocket track positions. 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 function easeOut(t) -- Cubic ease-out local inv = 1 - t return 1 - inv * inv * inv end local currentPocketIdx = 1 local function spin() local n = NUM_POCKETS local spinTime = SPIN_TIME_MIN + math.random() * (SPIN_TIME_MAX - SPIN_TIME_MIN) -- Total distance to travel in pocket-units: several full laps plus random offset. local laps = 4 + math.random(0, 3) local finalIdx = math.random(1, n) local startIdx = currentPocketIdx local totalSteps = laps * n + ((finalIdx - startIdx) % n) if totalSteps == 0 then totalSteps = n end local elapsed = 0 -- fractional pocket position local posF = startIdx - 1 -- 0-based float while elapsed < spinTime do local t = math.min(elapsed / spinTime, 1) local eased = easeOut(t) -- Current fractional position along the total travel local travelled = eased * totalSteps posF = (startIdx - 1 + travelled) % n local idxLow = math.floor(posF) % n + 1 local idxHigh = idxLow % n + 1 local frac = posF - math.floor(posF) eraseBall() local bx, by = lerpPos(idxLow, idxHigh, frac) drawBallAt(bx, by) gpu.sync() sleep(FRAME_DELAY) elapsed = elapsed + FRAME_DELAY end -- Snap to final pocket. eraseBall() local fx, fy = pocketPos(finalIdx) drawBallAt(fx, fy) gpu.sync() currentPocketIdx = finalIdx return pockets[finalIdx] end local function announce(pocket) 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() end ---------------------------------------------------------------------- -- Lifecycle ---------------------------------------------------------------------- local function start() math.randomseed(os.epoch("utc")) local g = findGPU() if not g then error("No GPU peripheral found.") end gpu = g 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 around perimeter"):format(NUM_POCKETS)) -- Initial draw. gpu.fill(COL_BG) drawTrack() drawAllPockets() -- Place ball at pocket 1 track position. local sx, sy = pocketPos(1) drawBallAt(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.3) local pocket = spin() announce(pocket) sleep(4) -- Redraw wheel and idle message, keep ball on winning pocket. drawTrack() drawAllPockets() local fx, fy = pocketPos(currentPocketIdx) drawBallAt(fx, fy) drawCenter({ "ROULETTE", "Pull lever to spin" }) gpu.sync() end end return { start = start, stop = stop, main = main }