/ spin.lua
spin.lua
1 --[[ 2 ToasterGen Spin 3 4 Copyright (C) 2025 Clifton Toaster Reid <cliftontreid@duck.com> 5 6 This library is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Lesser General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This library is distributed in the hope that it will be useful, 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Lesser General Public License for more details. 15 16 You should have received a copy of the GNU Lesser General Public License 17 along with this library. If not, see <https://www.gnu.org/licenses/>. 18 ]] 19 20 ---@class Reward 21 ---@field numeric number The number with which to multiply the bet to get the payout 22 ---@field dozen number The number with which to multiply the bet for winning the 1st 12, 2nd 12, and 3rd 12 options 23 ---@field binary number The number with which to multiply the bet for winning the 1-18, 19-36, EVEN, and ODD options 24 ---@field colour number The number with which to multiply the bet for winning the RED and BLACK options 25 26 ---@class DeviceConfig 27 ---@field carpet string The device that is used as carpet 28 ---@field ring string The device that is used as ring 29 ---@field modem string The device that is used as modem 30 ---@field redstone string The device that is used as redstone 31 ---@field ivmanagers table<number, string> The devices that are used as inventory managers 32 ---@field ivmanBigger number The streigth of the signal for the lowest inventory manager 33 34 ---@class DebugConfig 35 ---@field loki string | nil The URL of the Loki server to send logs to 36 ---@field tempo string | nil The URL of the Tempo server to send traces to 37 38 ---@type number | nil 39 local LokiTimer = nil 40 local LokiSleepyTime = 5 41 ---@type number | nil 42 local TempoTimer = nil 43 local TempoSleepyTime = 2 44 45 ---@class ClientConfig 46 ---@field version string The version of the config file schema using Semantic Versioning 47 ---@field rewards Reward The rewards that can be given to the player 48 ---@field devices DeviceConfig The devices that are used in the game 49 ---@field debug DebugConfig The debug configuration 50 51 local toml = require("src.toml") 52 53 ---@type ClientConfig 54 local config 55 local currentBetter = -1 56 57 local ucolors = { 58 -- Cannot be red, black or green 59 colors.orange, 60 colors.yellow, 61 colors.pink, 62 colors.cyan, 63 colors.blue, 64 colors.purple, 65 colors.white, 66 colors.lime, 67 } 68 69 ---@return ClientConfig 70 local function loadConfig() 71 local file = fs.open("/config.toml", "r") 72 local cfg = toml.parse(file.readAll()) 73 74 file.close() 75 return cfg 76 end 77 78 local function defaultConfig() 79 ---@type ClientConfig 80 local c = { 81 version = "0.1.0", 82 rewards = { 83 numeric = 2, 84 dozen = 1.75, 85 binary = 1.35, 86 colour = 1.35, 87 }, 88 devices = { 89 carpet = "monitor_0", 90 ring = "monitor_1", 91 redstone = "back", 92 ivmanagers = { "playerDetector_1" }, 93 modem = "top", 94 ivmanBigger = 1, 95 }, 96 debug = { 97 loki = nil, 98 tempo = nil, 99 }, 100 } 101 102 return c 103 end 104 105 local function saveConfig(config) 106 local file = fs.open("/config.toml", "w") 107 file.write(toml.encode(config)) 108 file.close() 109 end 110 111 local function isConfig() 112 return fs.exists("config.toml") 113 end 114 115 --- Write the bets and the winning number to a file 116 ---@param nbr number The winning number 117 ---@param bets Bet[] The bets that have been placed 118 local function emergencyWrite(nbr, bets) 119 -- write all bets to a json file '/bets.json' and the winning number to '/win' 120 121 local file = fs.open("/bets.json", "w") 122 file.write(textutils.serialize(bets)) 123 file.close() 124 file = fs.open("/win", "w") 125 file.write(textutils.serialize(nbr)) 126 file.close() 127 end 128 129 ---@param bet Bet The bet to check 130 ---@param nbr number The winning number 131 local function getPayout(bet, nbr) 132 -- handle numeric bets 133 if bet.number <= 36 and bet.number >= 0 then 134 if bet.number == nbr then 135 return bet.amount * config.rewards.numeric 136 end 137 end 138 139 -- handle dozen bets 140 if bet.number == 51 then 141 if nbr >= 1 and nbr <= 12 then 142 return bet.amount * config.rewards.dozen 143 end 144 end 145 if bet.number == 52 then 146 if nbr >= 13 and nbr <= 24 then 147 return bet.amount * config.rewards.dozen 148 end 149 end 150 if bet.number == 53 then 151 if nbr >= 25 and nbr <= 36 then 152 return bet.amount * config.rewards.dozen 153 end 154 end 155 156 -- handle binary bets, 54 55 58 59 157 if bet.number == 54 then -- 1 to 18 158 if nbr >= 1 and nbr <= 18 then 159 return bet.amount * config.rewards.binary 160 end 161 end 162 if bet.number == 55 or bet.number == 56 then -- even / red 163 if nbr % 2 == 0 then 164 return bet.amount * config.rewards.binary 165 end 166 end 167 if bet.number == 58 or bet.number == 57 then -- odd / black 168 if nbr % 2 == 1 then 169 return bet.amount * config.rewards.binary 170 end 171 end 172 if bet.number == 59 then -- 19 to 36 173 if nbr >= 19 and nbr <= 36 then 174 return bet.amount * config.rewards.binary 175 end 176 end 177 178 return nil 179 end 180 181 local function mainLoop() 182 -- Load modules once outside the loop 183 local carpet = require("src.carpet") 184 local ring = require("src.ring") 185 local Logger = require("src.log") 186 local Tracer = require("src.trace") 187 local iv = require("src.inventory") 188 189 if config.debug then 190 if config.debug.loki then 191 Logger.setLoki(config.debug.loki) 192 end 193 if config.debug.tempo then 194 Tracer.setTempo(config.debug.tempo) 195 end 196 end 197 198 -- Initialize devices 199 iv.init(config.devices.ivmanagers) 200 ring.init(config.devices.ring) 201 carpet.init(config.devices.carpet) 202 203 -- Handle redstone signal events 204 local function handleRedstoneEvent() 205 local tr = Tracer.new() 206 tr:setName("event.redstone") 207 tr:addTag("device", config.devices.redstone) 208 209 local redStreingth = redstone.getAnalogInput(config.devices.redstone) 210 local id = redStreingth - config.devices.ivmanBigger 211 if id < 0 then 212 Logger.error("Redstone signal too low") 213 tr:addAnnotation("low signal") 214 Tracer.addSpan(tr:endSpan()) 215 return 216 end 217 -- Store the player id in currentBetter 218 currentBetter = id 219 Logger.info("Player " .. id .. " clicked on the button") 220 221 Tracer.addSpan(tr:endSpan()) 222 end 223 224 -- Handle carpet monitor touch events 225 local function handleCarpetTouch(x, y) 226 local tr = Tracer.new() 227 tr:setName("event.monitor_touch") 228 tr:addTag("device", config.devices.carpet) 229 tr:addTag("action", "bet") 230 tr:addTag("x", x) 231 tr:addTag("y", y) 232 tr:addTag("player", tostring(currentBetter)) 233 234 if currentBetter == -1 then 235 Logger.error("No player detected") 236 return 237 end 238 239 local ballance = iv.getMoneyInPlayer(currentBetter) 240 if ballance == nil or ballance <= 0 then 241 Logger.error("Player " .. tostring(currentBetter) .. " has no money") 242 Logger.debug("Player is " .. (iv.getPlayer(currentBetter) or "")) 243 tr:addAnnotation("no money") 244 Tracer.addSpan(tr:endSpan()) 245 return 246 end 247 248 local nbr = carpet.findClickedNumber(x, y) 249 if nbr == nil then 250 Logger.error("No number clicked") 251 tr:addAnnotation("no number clicked") 252 Tracer.addSpan(tr:endSpan()) 253 return 254 end 255 256 local res = iv.takeMoneyFromPlayer(currentBetter, 1) 257 if res == nil then 258 Logger.error("Error taking money from player " .. tostring(currentBetter)) 259 tr:addAnnotation("error taking money") 260 Tracer.addSpan(tr:endSpan()) 261 return 262 end 263 264 local player = iv.getPlayer(currentBetter) 265 carpet.addBet(1, ucolors[currentBetter + 1], player or "", nbr) 266 Logger.info("Bet added successfully") 267 268 tr:addAnnotation("bet added") 269 os.sleep(0.2) 270 Tracer.addSpan(tr:endSpan()) 271 end 272 273 -- Handle ring monitor touch events 274 local function handleRingTouch() 275 local tr = Tracer.new() 276 tr:setName("event.monitor_touch") 277 tr:addTag("device", config.devices.ring) 278 tr:addTag("player", tostring(currentBetter)) 279 tr:addTag("action", "spin") 280 281 local min, max = 150, 200 282 local nbr = ring.launchBall(math.random(min, max)) 283 local bets = carpet.getBets() 284 285 Logger.info("Running through " .. #bets .. " bets") 286 for _, b in ipairs(bets) do 287 local ptr = Tracer.new() 288 ptr:setName("bet_check") 289 ptr:addTag("bet", tostring(b.number)) 290 ptr:addTag("player", b.player) 291 ptr:addTag("color", tostring(b.color)) 292 ptr:addTag("amount", tostring(b.amount)) 293 ptr:addTag("winning_number", tostring(nbr)) 294 ptr:setParentId(tr.traceId) 295 296 Logger.debug("Checking bet for player " .. 297 b.player .. " on number " .. b.number .. " with color " .. b.color .. " and amount " .. b.amount) 298 local payout = getPayout(b, nbr) 299 300 if payout then 301 Logger.info("Payout for bet: " .. payout) 302 tr:addAnnotation("payout" .. tostring(payout)) 303 local idx = iv.findPlayer(b.player) 304 if idx == nil then 305 Logger.error("Player " .. b.player .. " not found for payout") 306 emergencyWrite(nbr, bets) 307 ptr:addAnnotation("player not found") 308 Tracer.addSpan(ptr:endSpan()) 309 else 310 local res = iv.addMoneyToPlayer(idx - 1, payout) 311 if res == nil then 312 Logger.error("Failed to give money to player " .. b.player) 313 emergencyWrite(nbr, bets) 314 ptr:addAnnotation("error giving money") 315 Tracer.addSpan(ptr:endSpan()) 316 else 317 Logger.info("Money added to player " .. b.player) 318 ptr:addAnnotation("money added") 319 Tracer.addSpan(ptr:endSpan()) 320 end 321 end 322 Logger.info("Payout processed for player " .. b.player) 323 else 324 Logger.info("No payout for bet") 325 ptr:addAnnotation("no payout") 326 Tracer.addSpan(ptr:endSpan()) 327 end 328 end 329 carpet.resetBets() 330 carpet.update() 331 Logger.info("Bets reset successfully") 332 333 tr:addAnnotation("spin finished") 334 Tracer.addSpan(tr:endSpan()) 335 end 336 337 -- Handle monitor touch events 338 local function handleMonitorTouch(monitorName, x, y) 339 if monitorName == config.devices.carpet then 340 handleCarpetTouch(x, y) 341 elseif monitorName == config.devices.ring then 342 handleRingTouch() 343 end 344 end 345 346 LokiTimer = os.startTimer(LokiSleepyTime) 347 TempoTimer = os.startTimer(TempoSleepyTime) 348 349 -- Main event loop 350 while true do 351 local rEvent = { os.pullEventRaw() } 352 353 if rEvent[1] == "redstone" then 354 handleRedstoneEvent() 355 elseif rEvent[1] == "monitor_touch" then 356 handleMonitorTouch(rEvent[2], rEvent[3], rEvent[4]) 357 elseif rEvent[1] == "terminate" then 358 Logger.info("Terminating program") 359 error("Program terminated by user", 0) 360 elseif rEvent[1] == "timer" then 361 if rEvent[2] == LokiTimer then 362 Logger.sendLoki() 363 LokiTimer = os.startTimer(LokiSleepyTime) 364 elseif rEvent[2] == TempoTimer then 365 Tracer.sendTempo() 366 TempoTimer = os.startTimer(TempoSleepyTime) 367 end 368 end 369 end 370 end 371 372 local function main() 373 if not isConfig() then 374 saveConfig(defaultConfig()) 375 error("No config file found, a default one has been created", 0) 376 error("Please edit the config file to your liking", 0) 377 return 378 end 379 380 config = loadConfig() 381 mainLoop() 382 end 383 384 main()