diff --git a/disk-driver.lua b/disk-driver.lua index df763c2..9ed1407 100644 --- a/disk-driver.lua +++ b/disk-driver.lua @@ -1,5 +1,5 @@ local diskDrive = peripheral.find("drive") -local monitor = require("monitor-driver") + local function getFilePath() if diskDrive and diskDrive.isDiskPresent() then local path = diskDrive.getMountPath() @@ -75,9 +75,10 @@ local function writeValue(key, value) end local function initialize() - monitor.writeLine("Initializing Data Storage...") + local monitor = peripheral.find("monitor") + if monitor then monitor.write("Initializing Data Storage...") end if diskDrive and diskDrive.isDiskPresent() then - monitor.writeLine("Disk detected. Initializing data storage.") + if monitor then monitor.write("Disk detected. Initializing data storage.") end local path = getFilePath() if path and not fs.exists(path) then saveData({}) diff --git a/kernel.lua b/kernel.lua index acdc29f..05d4449 100644 --- a/kernel.lua +++ b/kernel.lua @@ -55,6 +55,7 @@ addDriver("task-manager") addDriver("monitor-driver") addDriver("disk-driver") addDriver("speaker-driver") +addSound("notification") return { addDriver = addDriver, addFolderDriver = addFolderDriver, addProgram = addProgram, addSound = addSound, addServerHandler = addServerHandler } \ No newline at end of file diff --git a/programs/roulette.lua b/programs/roulette.lua new file mode 100644 index 0000000..4ad79a8 --- /dev/null +++ b/programs/roulette.lua @@ -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 } diff --git a/speaker-driver.lua b/speaker-driver.lua index d0c1a71..df72366 100644 --- a/speaker-driver.lua +++ b/speaker-driver.lua @@ -1,15 +1,11 @@ local speaker = peripheral.find("speaker") local dfpwm = require("cc.audio.dfpwm") -local encoder = dfpwm.make_encoder() -local decoder = dfpwm.make_decoder() local sounds = { {name = "notification", file = "notification.dfpwm"} } local function initialize() - kernel = require("kernel") - kernel.addSound("notification") speaker = peripheral.find("speaker") if not speaker then print("Warning: No speaker attached.") @@ -39,7 +35,6 @@ local function play(name) return false end - -- Create a new decoder for this playback to reset state local decoder = dfpwm.make_decoder() for chunk in io.lines(filePath, 16 * 1024) do @@ -50,7 +45,6 @@ local function play(name) end return true end ----------------- local function getFileName(name) local extension = ".dfpwm" @@ -68,6 +62,7 @@ end local function playSound(fileName) local fileStream = getFileName(fileName) + local decoder = dfpwm.make_decoder() local values = io.lines(fileStream, 16 * 1024) for input in values do local decoded = decoder(input) @@ -77,20 +72,26 @@ local function playSound(fileName) end end - - local function playTTSFile(value) local message = textutils.urlEncode(value) local url = "http://api.astrocore.space/api/TextToSpeech?message=" .. message local response, err = http.get { url = url, binary = true } + + if not response then + print("TTS request failed: " .. (err or "unknown error")) + return false + end + local name = randomFileName() local fileName = name .. ".dfpwm" local fileData = response.readAll() - local file = fs.open(fileName,"w") + response.close() + + local file = fs.open(fileName, "wb") file.write(fileData) file.close() - response.close() + playSound(name) shell.execute("rm", fileName) end