-- Roulette Machine -- Listens for a redstone signal on any side, spins a "ball" around the -- perimeter of every connected monitor, and lands on a random pocket. -- -- Setup: -- * 1 advanced computer -- * 1+ monitors (vanilla or Tom's wired together as a single logical monitor) -- * Redstone input on any side to start a spin local mon -- the active monitor peripheral local W, H -- monitor character size local perimeter -- ordered list of {x, y, color} cells around the edge local ballIndex -- current position in perimeter local lastBallIndex -- previous position (so we can repaint the pocket) local SPIN_MIN_TIME = 6 -- seconds local SPIN_MAX_TIME = 12 local START_DELAY = 0.03 -- seconds between ball moves at full speed local END_DELAY = 0.45 -- seconds between ball moves just before stopping local BALL_COLOR = colors.white local ZERO_COLOR = colors.green local TEXT_COLOR = colors.white ---------------------------------------------------------------------- -- Monitor helpers ---------------------------------------------------------------------- local function pickMonitor() -- peripheral.find returns the first match; with Tom's Peripherals -- a multi-monitor block exposes itself as a single "monitor". mon = peripheral.find("monitor") if not mon then error("No monitor attached / found on the network") end mon.setTextScale(0.5) W, H = mon.getSize() end local function setPixel(x, y, bg) mon.setBackgroundColor(bg) mon.setCursorPos(x, y) mon.write(" ") end local function setLabel(x, y, bg, fg, ch) mon.setBackgroundColor(bg) mon.setTextColor(fg) mon.setCursorPos(x, y) mon.write(ch) end ---------------------------------------------------------------------- -- Build the wheel perimeter ---------------------------------------------------------------------- -- Walks the edge clockwise starting at top-left, returning a list of -- {x = , y = , color = , label = } pockets. Colors alternate red/black -- with a single green "0" pocket at the start. local function buildPerimeter() perimeter = {} local function add(x, y) table.insert(perimeter, { x = x, y = y }) end -- top edge: left -> right for x = 1, W do add(x, 1) end -- right edge: top+1 -> bottom for y = 2, H do add(W, y) end -- bottom edge: right-1 -> left for x = W - 1, 1, -1 do add(x, H) end -- left edge: bottom-1 -> top+1 for y = H - 1, 2, -1 do add(1, y) end -- Assign colors: 0 = green, then alternating red/black around the wheel. for i, cell in ipairs(perimeter) do if i == 1 then cell.color = ZERO_COLOR cell.label = "0" else cell.color = (i % 2 == 0) and colors.red or colors.black cell.label = tostring(i - 1) end end end ---------------------------------------------------------------------- -- Drawing ---------------------------------------------------------------------- local function drawWheel() mon.setBackgroundColor(colors.black) mon.clear() for _, cell in ipairs(perimeter) do setPixel(cell.x, cell.y, cell.color) end end local function drawCenter(lines) -- Clear interior mon.setBackgroundColor(colors.black) for y = 2, H - 1 do for x = 2, W - 1 do setPixel(x, y, colors.black) end end mon.setTextColor(TEXT_COLOR) local startY = math.floor(H / 2) - math.floor(#lines / 2) for i, line in ipairs(lines) do local x = math.max(2, math.floor((W - #line) / 2) + 1) mon.setCursorPos(x, startY + i - 1) mon.setBackgroundColor(colors.black) mon.write(line) end end local function repaintPocket(idx) local c = perimeter[idx] setPixel(c.x, c.y, c.color) end local function drawBall(idx) local c = perimeter[idx] setPixel(c.x, c.y, BALL_COLOR) end ---------------------------------------------------------------------- -- Spin logic ---------------------------------------------------------------------- local function moveBall(newIdx) if lastBallIndex then repaintPocket(lastBallIndex) end drawBall(newIdx) lastBallIndex = newIdx ballIndex = newIdx end -- ease-out cubic from 0..1 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..." }) -- Make sure the centered text doesn't hide the ball trail; redraw shortly sleep(0.6) drawCenter({ "" }) -- During the fast portion the ball jumps several pockets per tick to look frantic. while elapsed < spinTime do local t = elapsed / spinTime -- 0 .. 1 local eased = easeOut(t) -- 0 .. 1, slows over time 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: a few slow ticks then stop on a uniformly random pocket. local finalIdx = math.random(1, n) -- Walk forward to the final pocket so the stop looks natural. 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 colorName = (pocket.color == colors.red) and "RED" or (pocket.color == colors.green) and "GREEN" or "BLACK" drawCenter({ "WINNER", colorName .. " " .. pocket.label, }) end ---------------------------------------------------------------------- -- Lifecycle ---------------------------------------------------------------------- local function start() math.randomseed(os.epoch("utc")) pickMonitor() buildPerimeter() ballIndex = 1 lastBallIndex = nil drawWheel() drawBall(ballIndex) drawCenter({ "ROULETTE", "Pull lever to spin" }) end local function stop() if mon then mon.setBackgroundColor(colors.black) mon.setTextColor(colors.white) mon.clear() mon.setCursorPos(1, 1) end end local function waitForRedstonePulse() -- Wait for a rising edge on any side. 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) -- Small cooldown so a held lever doesn't immediately re-trigger. sleep(3) -- Drain any redstone events during cooldown by redrawing idle screen. drawCenter({ "ROULETTE", "Pull lever to spin" }) end end return { start = start, stop = stop, main = main }