From f4ac7faf0b38e55eae7447503d3447b2ea8ac2db Mon Sep 17 00:00:00 2001 From: itzmarkoni Date: Tue, 5 May 2026 18:04:45 -0400 Subject: [PATCH] updated --- programs/roulette.lua | 289 ++++++++++++++++++++++++++---------------- 1 file changed, 181 insertions(+), 108 deletions(-) diff --git a/programs/roulette.lua b/programs/roulette.lua index 4ad79a8..47de789 100644 --- a/programs/roulette.lua +++ b/programs/roulette.lua @@ -1,84 +1,169 @@ -- 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. +-- Designed for Tom's Peripherals GPU + screen blocks (multi-screen wall). +-- Falls back to vanilla CC:Tweaked monitor if no GPU is attached. -- --- 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 +-- 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. ---------------------------------------------------------------------- --- Monitor helpers +-- 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 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") +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() + -- Per-block pixel resolution. 32 gives a nice balance between + -- pocket density and per-pixel performance on multi-block screens. + gpu.setSize(32) + local pw, ph = gpu.getSize() + -- Pocket cell size in pixels. Bigger = chunkier pockets. + local cell = 16 + + 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) + gpu.drawText(px, py, str, fg, bg, 1, 0) + end, + clear = function(col) + -- gpu.fill() with no args clears to black; with a color clears to that color. + 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) - W, H = mon.getSize() + 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 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) +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 ---------------------------------------------------------------------- --- Build the wheel perimeter +-- Wheel state ---------------------------------------------------------------------- --- 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 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 + 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 + 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 - -- Assign colors: 0 = green, then alternating red/black around the wheel. - for i, cell in ipairs(perimeter) do + for i, c in ipairs(perimeter) do if i == 1 then - cell.color = ZERO_COLOR - cell.label = "0" + c.color = gfx.colors.green + c.label = "0" else - cell.color = (i % 2 == 0) and colors.red or colors.black - cell.label = tostring(i - 1) + c.color = (i % 2 == 0) and gfx.colors.red or gfx.colors.black + c.label = tostring(i - 1) end end end @@ -88,53 +173,46 @@ end ---------------------------------------------------------------------- local function drawWheel() - mon.setBackgroundColor(colors.black) - mon.clear() - for _, cell in ipairs(perimeter) do - setPixel(cell.x, cell.y, cell.color) + 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 - mon.setBackgroundColor(colors.black) - for y = 2, H - 1 do - for x = 2, W - 1 do - setPixel(x, y, colors.black) - end + if W > 2 and H > 2 then + gfx.fillRect(2, 2, W - 2, H - 2, gfx.colors.bg) end - mon.setTextColor(TEXT_COLOR) - local startY = math.floor(H / 2) - math.floor(#lines / 2) + 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) - mon.setCursorPos(x, startY + i - 1) - mon.setBackgroundColor(colors.black) - mon.write(line) + gfx.text(x, startY + i - 1, line, gfx.colors.white, gfx.colors.bg) end end local function repaintPocket(idx) local c = perimeter[idx] - setPixel(c.x, c.y, c.color) + gfx.fillRect(c.x, c.y, 1, 1, c.color) end local function drawBall(idx) local c = perimeter[idx] - setPixel(c.x, c.y, BALL_COLOR) + gfx.fillRect(c.x, c.y, 1, 1, gfx.colors.white) end ----------------------------------------------------------------------- --- Spin logic ----------------------------------------------------------------------- - local function moveBall(newIdx) if lastBallIndex then repaintPocket(lastBallIndex) end drawBall(newIdx) lastBallIndex = newIdx ballIndex = newIdx + gfx.sync() end --- ease-out cubic from 0..1 +---------------------------------------------------------------------- +-- Spin +---------------------------------------------------------------------- + local function easeOut(t) local inv = 1 - t return 1 - inv * inv * inv @@ -145,15 +223,14 @@ local function spin() 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({ "SPINNING" }) + gfx.sync() + sleep(0.5) 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 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)) @@ -163,9 +240,8 @@ local function spin() elapsed = elapsed + delay end - -- Final settle: a few slow ticks then stop on a uniformly random pocket. + -- Final settle: pick a uniformly random pocket and crawl to it. 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 @@ -178,13 +254,11 @@ local function spin() 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, - }) + 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 ---------------------------------------------------------------------- @@ -193,32 +267,32 @@ end local function start() math.randomseed(os.epoch("utc")) - pickMonitor() + gfx = initBackend() + W, H = gfx.size() + if W < 4 or H < 4 then + error(("Screen too small: %dx%d cells"):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 mon then - mon.setBackgroundColor(colors.black) - mon.setTextColor(colors.white) - mon.clear() - mon.setCursorPos(1, 1) + if gfx then + gfx.clear(gfx.colors.bg) + gfx.sync() 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 + if redstone.getInput(side) then return side end end end end @@ -228,10 +302,9 @@ local function main() 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" }) + gfx.sync() end end