-- Roulette Machine -- Designed for Tom's Peripherals GPU + screen blocks (multi-screen wall). -- -- Behaviour: -- * On boot, discovers the GPU peripheral by scanning all attached peripherals. -- * Calls refreshSize() + setSize(64) to bind the full monitor wall. -- * Draws a red/black/green pocket ring around the perimeter. -- * Waits for a redstone signal on any side, then spins the "ball" -- around the ring with random duration and ease-out deceleration, -- finally landing on a uniformly random pocket. ---------------------------------------------------------------------- -- 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 ---------------------------------------------------------------------- -- Backend ---------------------------------------------------------------------- local gfx local function initBackend() local gpu = findGPU() if not gpu then error("No GPU peripheral found. Attach a Tom's Peripherals GPU adjacent to the monitor wall.") end -- Let the server tick resolve the monitor wall geometry before setSize. gpu.refreshSize() sleep(0) gpu.setSize(64) -- getSize() -> pixelW, pixelH, blocksX, blocksY, sizePerBlock local pw, ph, bx, by, sb = gpu.getSize() print(("[roulette] GPU: %dx%d px | %dx%d blocks | %d px/block") :format(pw, ph, bx or 0, by or 0, sb or 0)) if not pw or pw < 8 or ph < 8 then error(("GPU pixel size %dx%d is too small. Is the monitor wall placed and facing correctly?") :format(pw or 0, ph or 0)) end -- Cell size: target at least 16 cells on the short axis, max 16 px each. local cell = math.max(2, math.min(16, math.floor(math.min(pw, ph) / 16))) local cw = math.floor(pw / cell) local ch = math.floor(ph / cell) print(("[roulette] Cell size: %d px -> grid: %dx%d cells"):format(cell, cw, ch)) return { kind = "gpu", cellSize = cell, pixelW = pw, pixelH = ph, size = function() return cw, ch end, fillRect = function(x, y, w, h, col) local px = (x - 1) * cell + 1 local py = (y - 1) * cell + 1 gpu.filledRectangle(px, py, w * cell, h * cell, col) end, text = function(x, y, str, fg, bg) local px = (x - 1) * cell + 1 local py = (y - 1) * cell + 1 pcall(gpu.drawText, px, py, str, fg or 0xFFFFFF, bg or 0x000000, 1, 0) end, clear = function(col) if col then gpu.fill(col) else gpu.fill() end end, sync = function() gpu.sync() end, colors = { red = 0xE53935, black = 0x101010, green = 0x2E7D32, white = 0xFFFFFF, bg = 0x000000, }, } end ---------------------------------------------------------------------- -- Wheel state ---------------------------------------------------------------------- local W, H local perimeter local ballIndex local lastBallIndex local SPIN_MIN_TIME = 6 local SPIN_MAX_TIME = 12 local START_DELAY = 0.03 local END_DELAY = 0.45 local function buildPerimeter() perimeter = {} local function add(x, y) table.insert(perimeter, { x = x, y = y }) end for x = 1, W do add(x, 1) end -- top L->R for y = 2, H do add(W, y) end -- right T->B for x = W - 1, 1, -1 do add(x, H) end -- bottom R->L for y = H - 1, 2, -1 do add(1, y) end -- left B->T for i, c in ipairs(perimeter) do if i == 1 then c.color = gfx.colors.green c.label = "0" else c.color = (i % 2 == 0) and gfx.colors.red or gfx.colors.black c.label = tostring(i - 1) end end end ---------------------------------------------------------------------- -- Drawing ---------------------------------------------------------------------- local function drawWheel() gfx.clear(gfx.colors.bg) for _, c in ipairs(perimeter) do gfx.fillRect(c.x, c.y, 1, 1, c.color) end end local function drawCenter(lines) if W > 2 and H > 2 then gfx.fillRect(2, 2, W - 2, H - 2, gfx.colors.bg) end local startY = math.floor(H / 2) - math.floor(#lines / 2) + 1 for i, line in ipairs(lines) do local x = math.max(2, math.floor((W - #line) / 2) + 1) gfx.text(x, startY + i - 1, line, gfx.colors.white, gfx.colors.bg) end end local function repaintPocket(idx) local c = perimeter[idx] gfx.fillRect(c.x, c.y, 1, 1, c.color) end local function drawBall(idx) local c = perimeter[idx] gfx.fillRect(c.x, c.y, 1, 1, gfx.colors.white) end local function moveBall(newIdx) if lastBallIndex then repaintPocket(lastBallIndex) end drawBall(newIdx) lastBallIndex = newIdx ballIndex = newIdx gfx.sync() end ---------------------------------------------------------------------- -- Spin ---------------------------------------------------------------------- local function easeOut(t) local inv = 1 - t return 1 - inv * inv * inv end local function spin() local n = #perimeter local spinTime = SPIN_MIN_TIME + math.random() * (SPIN_MAX_TIME - SPIN_MIN_TIME) local elapsed = 0 drawCenter({ "SPINNING" }) gfx.sync() sleep(0.5) drawCenter({ "" }) while elapsed < spinTime do local t = elapsed / spinTime local eased = easeOut(t) local delay = START_DELAY + (END_DELAY - START_DELAY) * eased local jump = math.max(1, math.floor((1 - eased) * 4 + 0.5)) moveBall(((ballIndex - 1 + jump) % n) + 1) sleep(delay) elapsed = elapsed + delay end -- Crawl to a uniformly random final pocket. local finalIdx = math.random(1, n) local steps = (finalIdx - ballIndex) % n if steps == 0 then steps = n end for i = 1, steps do moveBall(((ballIndex - 1 + 1) % n) + 1) sleep(END_DELAY + i * 0.04) end return perimeter[finalIdx] end local function announce(pocket) local name = "BLACK" if pocket.color == gfx.colors.red then name = "RED" end if pocket.color == gfx.colors.green then name = "GREEN" end drawCenter({ "WINNER", name .. " " .. pocket.label }) gfx.sync() end ---------------------------------------------------------------------- -- Lifecycle ---------------------------------------------------------------------- local function start() math.randomseed(os.epoch("utc")) gfx = initBackend() W, H = gfx.size() print(("[roulette] Wheel grid: %dx%d cells"):format(W, H)) if W < 3 or H < 3 then error(("Screen too small: %dx%d cells. Add more screen blocks."):format(W, H)) end buildPerimeter() ballIndex = 1 lastBallIndex = nil drawWheel() drawBall(ballIndex) drawCenter({ "ROULETTE", "Pull lever to spin" }) gfx.sync() end local function stop() if gfx then gfx.clear(gfx.colors.bg) gfx.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() local pocket = spin() announce(pocket) sleep(3) drawCenter({ "ROULETTE", "Pull lever to spin" }) gfx.sync() end end return { start = start, stop = stop, main = main }