Files
nova-corp/programs/roulette.lua

361 lines
11 KiB
Lua

-- Roulette Machine
-- Tom's Peripherals GPU + screen wall (832x448 or any size).
--
-- Layout (all pixel-space):
-- Pocket ring : 1 block (64px) wide border around the edge
-- Ball track : a lane just inside the pocket ring where the ball rolls
-- Center : status text
----------------------------------------------------------------------
-- 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
----------------------------------------------------------------------
-- Constants / tunables
----------------------------------------------------------------------
local POCKET_SIZE = 64 -- px per pocket cell (1 block)
local BALL_RADIUS = 18 -- px radius of the ball circle
local TRACK_INSET = 80 -- px from screen edge to ball centre track
local SPIN_TIME_MIN = 4 -- seconds
local SPIN_TIME_MAX = 7
local FRAME_DELAY = 0.02 -- seconds per frame (~50 fps target)
local COL_RED = 0xE53935
local COL_BLACK = 0x212121
local COL_GREEN = 0x2E7D32
local COL_WHITE = 0xFFFFFF
local COL_BALL = 0xF5F5F5
local COL_BG = 0x050505
local COL_TRACK = 0x1A1A1A -- subtle track lane colour
----------------------------------------------------------------------
-- GPU / pixel drawing layer
----------------------------------------------------------------------
local gpu
local PW, PH -- pixel width/height of wall
local function px_rect(x, y, w, h, col)
-- clamp to screen
if x < 1 then w = w + x - 1; x = 1 end
if y < 1 then h = h + y - 1; y = 1 end
if x + w - 1 > PW then w = PW - x + 1 end
if y + h - 1 > PH then h = PH - y + 1 end
if w < 1 or h < 1 then return end
gpu.filledRectangle(x, y, w, h, col)
end
-- Draw a filled circle at pixel centre (cx, cy) with given radius.
local function px_circle(cx, cy, r, col)
for dy = -r, r do
local half = math.floor(math.sqrt(r * r - dy * dy) + 0.5)
px_rect(cx - half, cy + dy, half * 2 + 1, 1, col)
end
end
-- Draw text centred horizontally at pixel y, using drawText.
local function px_text_centre(str, py, fg, bg, size)
size = size or 2
-- Each char is ~8px wide at size 1; rough estimate for centering.
local charW = 8 * size
local approxW = #str * charW
local px = math.max(1, math.floor((PW - approxW) / 2))
pcall(gpu.drawText, px, py, str, fg, bg, size, 1)
end
----------------------------------------------------------------------
-- Pocket layout
----------------------------------------------------------------------
-- Pockets are arranged clockwise around the perimeter, each POCKET_SIZE px.
-- We store the pixel centre of each pocket.
local pockets = {} -- { cx, cy, color, label, track_cx, track_cy }
local NUM_POCKETS
local function buildPockets()
pockets = {}
-- Number of pockets that fit each edge (integer, no partial pockets).
local cols = math.floor(PW / POCKET_SIZE) -- top & bottom edges
local rows = math.floor(PH / POCKET_SIZE) -- left & right edges
-- Clockwise: top L->R, right T->B, bottom R->L, left B->T
-- Avoid double-counting corners by using the top/bottom for full width
-- and left/right for inner height only.
local function add(cx, cy)
table.insert(pockets, { cx = cx, cy = cy })
end
local half = math.floor(POCKET_SIZE / 2)
-- Top edge
for i = 0, cols - 1 do
add(i * POCKET_SIZE + half, half)
end
-- Right edge (skip top-right corner already added)
for i = 1, rows - 1 do
add(PW - half, i * POCKET_SIZE + half)
end
-- Bottom edge R->L (skip bottom-right corner)
for i = cols - 1, 1, -1 do
add(i * POCKET_SIZE + half, PH - half)
end
-- Left edge B->T (skip bottom-left and top-left corners)
for i = rows - 1, 1, -1 do
add(half, i * POCKET_SIZE + half)
end
NUM_POCKETS = #pockets
-- Assign colours: pocket 1 = green (0), rest alternate red/black.
for i, p in ipairs(pockets) do
if i == 1 then
p.color = COL_GREEN; p.label = "0"
else
p.color = (i % 2 == 0) and COL_RED or COL_BLACK
p.label = tostring(i - 1)
end
end
-- Compute ball track centre for each pocket (inset from screen edge).
for _, p in ipairs(pockets) do
local tx = math.max(TRACK_INSET, math.min(PW - TRACK_INSET, p.cx))
local ty = math.max(TRACK_INSET, math.min(PH - TRACK_INSET, p.cy))
p.track_cx = tx
p.track_cy = ty
end
end
----------------------------------------------------------------------
-- Drawing
----------------------------------------------------------------------
local function drawPocket(p, highlight)
local x = p.cx - math.floor(POCKET_SIZE / 2)
local y = p.cy - math.floor(POCKET_SIZE / 2)
local col = highlight and COL_WHITE or p.color
px_rect(x + 1, y + 1, POCKET_SIZE - 2, POCKET_SIZE - 2, col)
-- thin dark border
px_rect(x, y, POCKET_SIZE, 1, COL_BG)
px_rect(x, y + POCKET_SIZE - 1, POCKET_SIZE, 1, COL_BG)
px_rect(x, y, 1, POCKET_SIZE, COL_BG)
px_rect(x + POCKET_SIZE - 1, y, 1, POCKET_SIZE, COL_BG)
end
local function drawAllPockets()
for _, p in ipairs(pockets) do
drawPocket(p, false)
end
end
local function drawTrack()
-- Fill the interior (inside pocket ring) with track colour.
px_rect(POCKET_SIZE + 1, POCKET_SIZE + 1,
PW - POCKET_SIZE * 2 - 1, PH - POCKET_SIZE * 2 - 1, COL_TRACK)
end
local function drawCenter(lines, textSize)
textSize = textSize or 2
-- Clear centre area.
local margin = POCKET_SIZE + BALL_RADIUS * 3
px_rect(margin, margin, PW - margin * 2, PH - margin * 2, COL_BG)
local lineH = 10 * textSize
local totalH = #lines * lineH
local startY = math.floor((PH - totalH) / 2)
for i, line in ipairs(lines) do
px_text_centre(line, startY + (i - 1) * lineH, COL_WHITE, COL_BG, textSize)
end
end
----------------------------------------------------------------------
-- Ball (pixel-space, smooth)
----------------------------------------------------------------------
local ballX, ballY = 0, 0
local lastBallPocket = nil
local function eraseBall()
-- Redraw the track patch under where the ball was.
px_circle(ballX, ballY, BALL_RADIUS + 1, COL_TRACK)
end
local function drawBallAt(x, y)
ballX, ballY = x, y
px_circle(x, y, BALL_RADIUS, COL_BALL)
end
----------------------------------------------------------------------
-- Spin logic
----------------------------------------------------------------------
-- Convert pocket index to a continuous angle (radians) position
-- so we can interpolate smoothly.
-- We map pocket index to a fractional position [0, NUM_POCKETS).
local function pocketPos(idx)
-- Returns the track cx, cy for a given pocket index.
local p = pockets[idx]
return p.track_cx, p.track_cy
end
-- Lerp between two pocket track positions.
local function lerpPos(i1, i2, t)
local x1, y1 = pocketPos(i1)
local x2, y2 = pocketPos(i2)
return x1 + (x2 - x1) * t, y1 + (y2 - y1) * t
end
local function easeOut(t)
-- Cubic ease-out
local inv = 1 - t
return 1 - inv * inv * inv
end
local currentPocketIdx = 1
local function spin()
local n = NUM_POCKETS
local spinTime = SPIN_TIME_MIN + math.random() * (SPIN_TIME_MAX - SPIN_TIME_MIN)
-- Total distance to travel in pocket-units: several full laps plus random offset.
local laps = 4 + math.random(0, 3)
local finalIdx = math.random(1, n)
local startIdx = currentPocketIdx
local totalSteps = laps * n + ((finalIdx - startIdx) % n)
if totalSteps == 0 then totalSteps = n end
local elapsed = 0
-- fractional pocket position
local posF = startIdx - 1 -- 0-based float
while elapsed < spinTime do
local t = math.min(elapsed / spinTime, 1)
local eased = easeOut(t)
-- Current fractional position along the total travel
local travelled = eased * totalSteps
posF = (startIdx - 1 + travelled) % n
local idxLow = math.floor(posF) % n + 1
local idxHigh = idxLow % n + 1
local frac = posF - math.floor(posF)
eraseBall()
local bx, by = lerpPos(idxLow, idxHigh, frac)
drawBallAt(bx, by)
gpu.sync()
sleep(FRAME_DELAY)
elapsed = elapsed + FRAME_DELAY
end
-- Snap to final pocket.
eraseBall()
local fx, fy = pocketPos(finalIdx)
drawBallAt(fx, fy)
gpu.sync()
currentPocketIdx = finalIdx
return pockets[finalIdx]
end
local function announce(pocket)
local name = "BLACK"
if pocket.color == COL_RED then name = "RED" end
if pocket.color == COL_GREEN then name = "GREEN" end
drawCenter({ "WINNER!", name .. " " .. pocket.label }, 3)
gpu.sync()
end
----------------------------------------------------------------------
-- Lifecycle
----------------------------------------------------------------------
local function start()
math.randomseed(os.epoch("utc"))
local g = findGPU()
if not g then
error("No GPU peripheral found.")
end
gpu = g
gpu.refreshSize()
sleep(0)
gpu.setSize(64)
PW, PH = gpu.getSize()
print(("[roulette] GPU: %dx%d px"):format(PW, PH))
if not PW or PW < 64 or PH < 64 then
error(("GPU pixel size %dx%d too small."):format(PW or 0, PH or 0))
end
buildPockets()
print(("[roulette] %d pockets around perimeter"):format(NUM_POCKETS))
-- Initial draw.
gpu.fill(COL_BG)
drawTrack()
drawAllPockets()
-- Place ball at pocket 1 track position.
local sx, sy = pocketPos(1)
drawBallAt(sx, sy)
currentPocketIdx = 1
drawCenter({ "ROULETTE", "Pull lever to spin" })
gpu.sync()
end
local function stop()
if gpu then
gpu.fill(COL_BG)
gpu.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()
drawCenter({ "SPINNING..." })
gpu.sync()
sleep(0.3)
local pocket = spin()
announce(pocket)
sleep(4)
-- Redraw wheel and idle message, keep ball on winning pocket.
drawTrack()
drawAllPockets()
local fx, fy = pocketPos(currentPocketIdx)
drawBallAt(fx, fy)
drawCenter({ "ROULETTE", "Pull lever to spin" })
gpu.sync()
end
end
return { start = start, stop = stop, main = main }