This commit is contained in:
2026-05-05 17:41:22 -04:00
parent 57f83bd56b
commit 875edb891a
4 changed files with 254 additions and 13 deletions

238
programs/roulette.lua Normal file
View File

@@ -0,0 +1,238 @@
-- 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 }