-- Roulette Machine -- Designed for Tom's Peripherals GPU + screen blocks (multi-screen wall). -- Falls back to vanilla CC:Tweaked monitor if no GPU is attached. -- -- Behaviour: -- * On boot, sizes itself to the full screen surface. -- * 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. ---------------------------------------------------------------------- -- Backend abstraction (GPU vs vanilla monitor) ---------------------------------------------------------------------- -- All drawing goes through a small `gfx` table with these methods: -- gfx.size() -> width, height in cells/pixels -- gfx.fillRect(x, y, w, h, col) -- col is a backend-specific color -- gfx.text(x, y, str, fg, bg) -- gfx.clear(col) -- gfx.sync() -- present frame (no-op for monitor) -- gfx.colors -- table with .red .black .green .white .bg -- gfx.cellSize -- pixel size of one "pocket cell" local gfx local function findGPU() -- Tom's Peripherals registers GPUs as tm_gpu_0, tm_gpu_1, ... so -- scan all peripherals for any type that starts with "tm_gpu" or -- exposes the GPU API surface. for _, side in ipairs(peripheral.getNames()) do local t = peripheral.getType(side) if t and t:sub(1, 6) == "tm_gpu" then return peripheral.wrap(side), t end end for _, side in ipairs(peripheral.getNames()) do local p = peripheral.wrap(side) if p and p.refreshSize and p.filledRectangle and p.sync then return p, peripheral.getType(side) end end return nil end local function makeGpuBackend(gpu) gpu.refreshSize() -- Try a high per-block resolution; older/cheaper GPU tiers may cap -- this so fall back if it errors. local trySizes = { 64, 32, 16, 8 } for _, s in ipairs(trySizes) do if pcall(gpu.setSize, s) then break end end local pw, ph = gpu.getSize() print(("[roulette] GPU pixel size: %sx%s"):format(tostring(pw), tostring(ph))) if not pw or not ph or pw < 8 or ph < 8 then error(("GPU reports unusable pixel size %sx%s. Place at least one screen block adjacent to the GPU.") :format(tostring(pw), tostring(ph))) end -- Pick a cell size that gives at least 16x16 cells, capped at 16 px. local cell = math.max(2, math.min(16, math.floor(math.min(pw, ph) / 16))) print(("[roulette] Using cell size: %d px -> %dx%d cells"):format( cell, math.floor(pw / cell), math.floor(ph / cell))) return { kind = "gpu", cellSize = cell, pixelW = pw, pixelH = ph, size = function() return math.floor(pw / cell), math.floor(ph / cell) end, fillRect = function(x, y, w, h, col) -- 1-indexed pixel coords, per gpuDraw.lua example. 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 -- drawText(x, y, text, textColor, bgColor, size, padding) pcall(gpu.drawText, px, py, str, fg, bg, 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 local function makeMonitorBackend() local mon = peripheral.find("monitor") if not mon then return nil end mon.setTextScale(0.5) return { kind = "monitor", cellSize = 1, size = function() return mon.getSize() end, fillRect = function(x, y, w, h, col) mon.setBackgroundColor(col) for dy = 0, h - 1 do mon.setCursorPos(x, y + dy) mon.write(string.rep(" ", w)) end end, text = function(x, y, str, fg, bg) mon.setBackgroundColor(bg or colors.black) mon.setTextColor(fg or colors.white) mon.setCursorPos(x, y) mon.write(str) end, clear = function(col) mon.setBackgroundColor(col) mon.clear() end, sync = function() end, colors = { red = colors.red, black = colors.black, green = colors.green, white = colors.white, bg = colors.black, }, } end local function initBackend() local gpu = findGPU() if gpu then return makeGpuBackend(gpu) end local mon = makeMonitorBackend() if mon then return mon end error("No GPU or monitor attached. Connect a Tom's Peripherals GPU + screens, or a CC monitor.") end ---------------------------------------------------------------------- -- Wheel state ---------------------------------------------------------------------- local W, H -- size in cells local perimeter -- ordered list of {x, y, color, label} 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 for y = 2, H do add(W, y) end -- right for x = W - 1, 1, -1 do add(x, H) end -- bottom for y = H - 1, 2, -1 do add(1, y) end -- left 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) -- Clear interior 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)) local nextIdx = ((ballIndex - 1 + jump) % n) + 1 moveBall(nextIdx) sleep(delay) elapsed = elapsed + delay end -- Final settle: pick a uniformly random pocket and crawl to it. local finalIdx = math.random(1, n) local steps = ((finalIdx - ballIndex) % n) if steps == 0 then steps = n end for i = 1, steps do local nextIdx = ((ballIndex - 1 + 1) % n) + 1 moveBall(nextIdx) 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" elseif 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 }