updated
This commit is contained in:
@@ -1,84 +1,169 @@
|
|||||||
-- Roulette Machine
|
-- Roulette Machine
|
||||||
-- Listens for a redstone signal on any side, spins a "ball" around the
|
-- Designed for Tom's Peripherals GPU + screen blocks (multi-screen wall).
|
||||||
-- perimeter of every connected monitor, and lands on a random pocket.
|
-- Falls back to vanilla CC:Tweaked monitor if no GPU is attached.
|
||||||
--
|
--
|
||||||
-- Setup:
|
-- Behaviour:
|
||||||
-- * 1 advanced computer
|
-- * On boot, sizes itself to the full screen surface.
|
||||||
-- * 1+ monitors (vanilla or Tom's wired together as a single logical monitor)
|
-- * Draws a red/black/green pocket ring around the perimeter.
|
||||||
-- * Redstone input on any side to start a spin
|
-- * Waits for a redstone signal on any side, then spins the "ball"
|
||||||
|
-- around the ring with random duration and ease-out deceleration,
|
||||||
local mon -- the active monitor peripheral
|
-- finally landing on a uniformly random pocket.
|
||||||
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
|
-- 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()
|
local gfx
|
||||||
-- peripheral.find returns the first match; with Tom's Peripherals
|
|
||||||
-- a multi-monitor block exposes itself as a single "monitor".
|
local function findGPU()
|
||||||
mon = peripheral.find("monitor")
|
-- Tom's Peripherals registers GPUs as tm_gpu_0, tm_gpu_1, ... so
|
||||||
if not mon then
|
-- scan all peripherals for any type that starts with "tm_gpu" or
|
||||||
error("No monitor attached / found on the network")
|
-- 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
|
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)
|
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
|
end
|
||||||
|
|
||||||
local function setPixel(x, y, bg)
|
local function initBackend()
|
||||||
mon.setBackgroundColor(bg)
|
local gpu = findGPU()
|
||||||
mon.setCursorPos(x, y)
|
if gpu then
|
||||||
mon.write(" ")
|
return makeGpuBackend(gpu)
|
||||||
end
|
end
|
||||||
|
local mon = makeMonitorBackend()
|
||||||
local function setLabel(x, y, bg, fg, ch)
|
if mon then return mon end
|
||||||
mon.setBackgroundColor(bg)
|
error("No GPU or monitor attached. Connect a Tom's Peripherals GPU + screens, or a CC monitor.")
|
||||||
mon.setTextColor(fg)
|
|
||||||
mon.setCursorPos(x, y)
|
|
||||||
mon.write(ch)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Build the wheel perimeter
|
-- Wheel state
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
-- Walks the edge clockwise starting at top-left, returning a list of
|
local W, H -- size in cells
|
||||||
-- {x = , y = , color = , label = } pockets. Colors alternate red/black
|
local perimeter -- ordered list of {x, y, color, label}
|
||||||
-- with a single green "0" pocket at the start.
|
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()
|
local function buildPerimeter()
|
||||||
perimeter = {}
|
perimeter = {}
|
||||||
local function add(x, y)
|
local function add(x, y) table.insert(perimeter, { x = x, y = y }) end
|
||||||
table.insert(perimeter, { x = x, y = y })
|
|
||||||
end
|
|
||||||
|
|
||||||
-- top edge: left -> right
|
for x = 1, W do add(x, 1) end -- top
|
||||||
for x = 1, W do add(x, 1) end
|
for y = 2, H do add(W, y) end -- right
|
||||||
-- right edge: top+1 -> bottom
|
for x = W - 1, 1, -1 do add(x, H) end -- bottom
|
||||||
for y = 2, H do add(W, y) end
|
for y = H - 1, 2, -1 do add(1, y) end -- left
|
||||||
-- 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, c in ipairs(perimeter) do
|
||||||
for i, cell in ipairs(perimeter) do
|
|
||||||
if i == 1 then
|
if i == 1 then
|
||||||
cell.color = ZERO_COLOR
|
c.color = gfx.colors.green
|
||||||
cell.label = "0"
|
c.label = "0"
|
||||||
else
|
else
|
||||||
cell.color = (i % 2 == 0) and colors.red or colors.black
|
c.color = (i % 2 == 0) and gfx.colors.red or gfx.colors.black
|
||||||
cell.label = tostring(i - 1)
|
c.label = tostring(i - 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -88,53 +173,46 @@ end
|
|||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
local function drawWheel()
|
local function drawWheel()
|
||||||
mon.setBackgroundColor(colors.black)
|
gfx.clear(gfx.colors.bg)
|
||||||
mon.clear()
|
for _, c in ipairs(perimeter) do
|
||||||
for _, cell in ipairs(perimeter) do
|
gfx.fillRect(c.x, c.y, 1, 1, c.color)
|
||||||
setPixel(cell.x, cell.y, cell.color)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function drawCenter(lines)
|
local function drawCenter(lines)
|
||||||
-- Clear interior
|
-- Clear interior
|
||||||
mon.setBackgroundColor(colors.black)
|
if W > 2 and H > 2 then
|
||||||
for y = 2, H - 1 do
|
gfx.fillRect(2, 2, W - 2, H - 2, gfx.colors.bg)
|
||||||
for x = 2, W - 1 do
|
|
||||||
setPixel(x, y, colors.black)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
mon.setTextColor(TEXT_COLOR)
|
local startY = math.floor(H / 2) - math.floor(#lines / 2) + 1
|
||||||
local startY = math.floor(H / 2) - math.floor(#lines / 2)
|
|
||||||
for i, line in ipairs(lines) do
|
for i, line in ipairs(lines) do
|
||||||
local x = math.max(2, math.floor((W - #line) / 2) + 1)
|
local x = math.max(2, math.floor((W - #line) / 2) + 1)
|
||||||
mon.setCursorPos(x, startY + i - 1)
|
gfx.text(x, startY + i - 1, line, gfx.colors.white, gfx.colors.bg)
|
||||||
mon.setBackgroundColor(colors.black)
|
|
||||||
mon.write(line)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function repaintPocket(idx)
|
local function repaintPocket(idx)
|
||||||
local c = perimeter[idx]
|
local c = perimeter[idx]
|
||||||
setPixel(c.x, c.y, c.color)
|
gfx.fillRect(c.x, c.y, 1, 1, c.color)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function drawBall(idx)
|
local function drawBall(idx)
|
||||||
local c = perimeter[idx]
|
local c = perimeter[idx]
|
||||||
setPixel(c.x, c.y, BALL_COLOR)
|
gfx.fillRect(c.x, c.y, 1, 1, gfx.colors.white)
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
-- Spin logic
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
local function moveBall(newIdx)
|
local function moveBall(newIdx)
|
||||||
if lastBallIndex then repaintPocket(lastBallIndex) end
|
if lastBallIndex then repaintPocket(lastBallIndex) end
|
||||||
drawBall(newIdx)
|
drawBall(newIdx)
|
||||||
lastBallIndex = newIdx
|
lastBallIndex = newIdx
|
||||||
ballIndex = newIdx
|
ballIndex = newIdx
|
||||||
|
gfx.sync()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ease-out cubic from 0..1
|
----------------------------------------------------------------------
|
||||||
|
-- Spin
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
local function easeOut(t)
|
local function easeOut(t)
|
||||||
local inv = 1 - t
|
local inv = 1 - t
|
||||||
return 1 - inv * inv * inv
|
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 spinTime = SPIN_MIN_TIME + math.random() * (SPIN_MAX_TIME - SPIN_MIN_TIME)
|
||||||
local elapsed = 0
|
local elapsed = 0
|
||||||
|
|
||||||
drawCenter({ "SPINNING..." })
|
drawCenter({ "SPINNING" })
|
||||||
-- Make sure the centered text doesn't hide the ball trail; redraw shortly
|
gfx.sync()
|
||||||
sleep(0.6)
|
sleep(0.5)
|
||||||
drawCenter({ "" })
|
drawCenter({ "" })
|
||||||
|
|
||||||
-- During the fast portion the ball jumps several pockets per tick to look frantic.
|
|
||||||
while elapsed < spinTime do
|
while elapsed < spinTime do
|
||||||
local t = elapsed / spinTime -- 0 .. 1
|
local t = elapsed / spinTime
|
||||||
local eased = easeOut(t) -- 0 .. 1, slows over time
|
local eased = easeOut(t)
|
||||||
local delay = START_DELAY + (END_DELAY - START_DELAY) * eased
|
local delay = START_DELAY + (END_DELAY - START_DELAY) * eased
|
||||||
local jump = math.max(1, math.floor((1 - eased) * 4 + 0.5))
|
local jump = math.max(1, math.floor((1 - eased) * 4 + 0.5))
|
||||||
|
|
||||||
@@ -163,9 +240,8 @@ local function spin()
|
|||||||
elapsed = elapsed + delay
|
elapsed = elapsed + delay
|
||||||
end
|
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)
|
local finalIdx = math.random(1, n)
|
||||||
-- Walk forward to the final pocket so the stop looks natural.
|
|
||||||
local steps = ((finalIdx - ballIndex) % n)
|
local steps = ((finalIdx - ballIndex) % n)
|
||||||
if steps == 0 then steps = n end
|
if steps == 0 then steps = n end
|
||||||
for i = 1, steps do
|
for i = 1, steps do
|
||||||
@@ -178,13 +254,11 @@ local function spin()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function announce(pocket)
|
local function announce(pocket)
|
||||||
local colorName =
|
local name = "BLACK"
|
||||||
(pocket.color == colors.red) and "RED" or
|
if pocket.color == gfx.colors.red then name = "RED"
|
||||||
(pocket.color == colors.green) and "GREEN" or "BLACK"
|
elseif pocket.color == gfx.colors.green then name = "GREEN" end
|
||||||
drawCenter({
|
drawCenter({ "WINNER", name .. " " .. pocket.label })
|
||||||
"WINNER",
|
gfx.sync()
|
||||||
colorName .. " " .. pocket.label,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
@@ -193,32 +267,32 @@ end
|
|||||||
|
|
||||||
local function start()
|
local function start()
|
||||||
math.randomseed(os.epoch("utc"))
|
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()
|
buildPerimeter()
|
||||||
ballIndex = 1
|
ballIndex = 1
|
||||||
lastBallIndex = nil
|
lastBallIndex = nil
|
||||||
drawWheel()
|
drawWheel()
|
||||||
drawBall(ballIndex)
|
drawBall(ballIndex)
|
||||||
drawCenter({ "ROULETTE", "Pull lever to spin" })
|
drawCenter({ "ROULETTE", "Pull lever to spin" })
|
||||||
|
gfx.sync()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function stop()
|
local function stop()
|
||||||
if mon then
|
if gfx then
|
||||||
mon.setBackgroundColor(colors.black)
|
gfx.clear(gfx.colors.bg)
|
||||||
mon.setTextColor(colors.white)
|
gfx.sync()
|
||||||
mon.clear()
|
|
||||||
mon.setCursorPos(1, 1)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function waitForRedstonePulse()
|
local function waitForRedstonePulse()
|
||||||
-- Wait for a rising edge on any side.
|
|
||||||
while true do
|
while true do
|
||||||
os.pullEvent("redstone")
|
os.pullEvent("redstone")
|
||||||
for _, side in ipairs(redstone.getSides()) do
|
for _, side in ipairs(redstone.getSides()) do
|
||||||
if redstone.getInput(side) then
|
if redstone.getInput(side) then return side end
|
||||||
return side
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -228,10 +302,9 @@ local function main()
|
|||||||
waitForRedstonePulse()
|
waitForRedstonePulse()
|
||||||
local pocket = spin()
|
local pocket = spin()
|
||||||
announce(pocket)
|
announce(pocket)
|
||||||
-- Small cooldown so a held lever doesn't immediately re-trigger.
|
|
||||||
sleep(3)
|
sleep(3)
|
||||||
-- Drain any redstone events during cooldown by redrawing idle screen.
|
|
||||||
drawCenter({ "ROULETTE", "Pull lever to spin" })
|
drawCenter({ "ROULETTE", "Pull lever to spin" })
|
||||||
|
gfx.sync()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user