265 lines
7.8 KiB
Lua
265 lines
7.8 KiB
Lua
-- 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 }
|