carpet.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 local mon = nil 21 local Logger = require("src.log") 22 local Tracer = require("src.trace") 23 24 ---@class Bet 25 ---@field amount number 26 ---@field color number 27 ---@field player string | nil 28 ---@field uuid string | nil 29 ---@field number number 30 31 ---@type Bet[] 32 local bets = {} 33 34 -- Global variables for display configuration 35 local NUMBER_SPACING = 6 -- Distance between numbers 36 local NUMBER_WIDTH = 4 -- Width of the number display area 37 local DOZEN_WIDTH = 6 -- Width for dozen columns (1st 12, etc.) 38 local SPECIAL_SPACING = (NUMBER_SPACING * 12) / 6 -- Distance between special displays 39 local SPECIAL_WIDTH = SPECIAL_SPACING * 0.85 -- Width of the special display area 40 local MAX_BETS = 4 41 42 -- Assign values above 50 for special options 43 local specialValues = { 44 ["1st 12"] = 51, 45 ["2nd 12"] = 52, 46 ["3rd 12"] = 53, 47 ["1 to 18"] = 54, 48 ["Even"] = 55, 49 ["Red"] = 56, 50 ["Black"] = 57, 51 ["Odd"] = 58, 52 ["19 to 36"] = 59, 53 } 54 55 -- Define the table layout 56 local layout = { 57 { 58 rowPos = 1, 59 items = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, 60 special = "1st 12", 61 spacing = NUMBER_SPACING, 62 itemWidth = NUMBER_WIDTH, 63 specialWidth = DOZEN_WIDTH, 64 }, 65 { 66 rowPos = 1 + (MAX_BETS + 2) * 1, 67 items = { 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 }, 68 special = "2nd 12", 69 spacing = NUMBER_SPACING, 70 itemWidth = NUMBER_WIDTH, 71 specialWidth = DOZEN_WIDTH, 72 }, 73 { 74 rowPos = 1 + (MAX_BETS + 2) * 2, 75 items = { 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36 }, 76 special = "3rd 12", 77 spacing = NUMBER_SPACING, 78 itemWidth = NUMBER_WIDTH, 79 specialWidth = DOZEN_WIDTH, 80 }, 81 { 82 rowPos = 1 + (MAX_BETS + 2) * 3, 83 items = { "1 to 18", "19 to 36", "Even", "Red", "Black", "Odd" }, 84 spacing = SPECIAL_SPACING, 85 itemWidth = SPECIAL_WIDTH, 86 }, 87 } 88 89 ---@param nbr number The bet number to print 90 ---@param idx number Where to start printing the bets 91 ---@param posx number The x position to print the bets 92 ---@param parentId string|nil The parent trace ID for logging 93 ---@return nil 94 local function printBet(nbr, idx, posx, parentId) 95 local tr = Tracer.new() 96 tr:setName("carpet.printBet") 97 tr:addTag("nbr", string.format("%d", nbr)) 98 tr:addTag("idx", string.format("%d", idx)) 99 tr:addTag("posx", string.format("%d", posx)) 100 if parentId then 101 tr:setParentId(parentId) 102 end 103 104 if mon == nil then 105 tr:addAnnotation("Monitor not initialized") 106 Tracer.addSpan(tr:endSpan()) 107 return 108 end 109 110 local usedBets = {} 111 112 for _, v in pairs(bets) do 113 if v.number == nbr then 114 Logger.debug("Match found: adding bet from " .. v.player .. " for " .. v.amount) 115 table.insert(usedBets, v) 116 end 117 end 118 119 tr:addAnnotation(string.format("Found %d bets for number %d", #usedBets, nbr)) 120 121 for i = 1, #usedBets do 122 local v = usedBets[i] 123 Logger.debug( 124 "Printing bet " 125 .. i 126 .. " at position " 127 .. posx 128 .. "," 129 .. (idx + i) 130 .. ": " 131 .. v.amount 132 .. " from " 133 .. v.player 134 ) 135 -- The numbers should appear under the number 136 mon.setCursorPos(posx, idx + i) 137 mon.setBackgroundColour(v.color) 138 mon.write(" ") 139 mon.setBackgroundColour(colours.green) 140 mon.write(" " .. v.amount) 141 end 142 Tracer.addSpan(tr:endSpan()) 143 end 144 145 ---@param item any The item (number or text) to display 146 ---@param width number The width of the display area 147 ---@return string The formatted text with proper padding 148 local function formatDisplayText(item, width) 149 local text = tostring(item) 150 local strLen = string.len(text) 151 local leftPad = math.floor((width - strLen) / 2) 152 local rightPad = width - strLen - leftPad 153 return string.rep(" ", leftPad) .. text .. string.rep(" ", rightPad) 154 end 155 156 ---@param rowDef table The row definition containing position and items 157 ---@param parentId string|nil The parent trace ID for logging 158 ---@return nil 159 local function printRow(rowDef, parentId) 160 local tr = Tracer.new() 161 tr:setName("carpet.printRow") 162 tr:addTag("rowPos", string.format("%d", rowDef.rowPos)) 163 if parentId then 164 tr:setParentId(parentId) 165 end 166 167 if mon == nil then 168 tr:addAnnotation("Monitor not initialized") 169 Tracer.addSpan(tr:endSpan()) 170 return 171 end 172 173 mon.setCursorPos(1, rowDef.rowPos) 174 175 -- Print the regular items in the row 176 for i, item in ipairs(rowDef.items) do 177 local posx = 2 + (i - 1) * rowDef.spacing 178 179 -- Set color based on whether it's a number or special item 180 if type(item) == "number" then 181 mon.setBackgroundColour(item % 2 == 0 and colours.red or colours.black) 182 else 183 mon.setBackgroundColour((i % 2 == 0) and colours.red or colours.black) 184 end 185 186 mon.setCursorPos(posx, rowDef.rowPos + 1) 187 mon.write(formatDisplayText(item, rowDef.itemWidth)) 188 189 -- Determine the bet number - either the number itself or its special value 190 local betNumber = type(item) == "number" and item or specialValues[item] 191 printBet(betNumber, rowDef.rowPos + 2, posx, tr.traceId) -- Pass traceId 192 end 193 194 -- Print the special column if specified 195 if rowDef.special then 196 local posx = 2 + (#rowDef.items * rowDef.spacing) 197 mon.setCursorPos(posx, rowDef.rowPos + 1) 198 mon.setBackgroundColour(colours.black) 199 -- Use specialWidth if defined, otherwise fall back to itemWidth 200 local displayWidth = rowDef.specialWidth or rowDef.itemWidth 201 mon.write(formatDisplayText(rowDef.special, displayWidth)) 202 printBet(specialValues[rowDef.special], rowDef.rowPos + 2, posx, tr.traceId) -- Pass traceId 203 end 204 Tracer.addSpan(tr:endSpan()) 205 end 206 207 ---@param parentId string|nil The parent trace ID for logging 208 local function update(parentId) 209 local tr = Tracer.new() 210 tr:setName("carpet.update") 211 if parentId then 212 tr:setParentId(parentId) 213 end 214 215 if mon == nil then 216 tr:addAnnotation("Monitor not initialized") 217 Tracer.addSpan(tr:endSpan()) 218 return 219 end 220 221 mon.clear() 222 mon.setBackgroundColour(colours.green) 223 mon.setTextColour(colours.white) 224 225 -- Fill the screen with green 226 local w, h = mon.getSize() 227 for i = 1, h do 228 mon.setCursorPos(1, i) 229 mon.write(string.rep(" ", w)) 230 end 231 232 -- Print all rows according to layout 233 for _, rowDef in ipairs(layout) do 234 printRow(rowDef, tr.traceId) -- Pass traceId 235 end 236 Tracer.addSpan(tr:endSpan()) 237 end 238 239 --- Finds the clicked number or special value based on the x and y coordinates in the layout. 240 --- 241 ---@param x number The x-coordinate of the click position. 242 ---@param y number The y-coordinate of the click position. 243 ---@param parentId string|nil The parent trace ID for logging 244 ---@return number|nil Returns the number corresponding to the clicked item, or a special value if the special column was clicked. 245 local function findClickedNumber(x, y, parentId) 246 local tr = Tracer.new() 247 tr:setName("carpet.findClickedNumber") 248 tr:addTag("x", string.format("%d", x)) 249 tr:addTag("y", string.format("%d", y)) 250 if parentId then 251 tr:setParentId(parentId) 252 end 253 254 -- Check each row in the layout to find what was clicked 255 for _, rowDef in ipairs(layout) do 256 -- Check if click is within the row's vertical bounds 257 -- Using MAX_BETS + 2 for consistency with row spacing in layout definition 258 if y >= rowDef.rowPos and y <= rowDef.rowPos + MAX_BETS + 2 then 259 -- Check for regular items 260 for i, item in ipairs(rowDef.items) do 261 local itemX = 2 + (i - 1) * rowDef.spacing 262 local itemWidth = rowDef.itemWidth 263 264 -- If click is within this item's bounds 265 if x >= itemX and x < itemX + itemWidth then 266 local clickedItem = type(item) == "number" and item or specialValues[item] 267 tr:addAnnotation(string.format("Clicked item %s (value %d)", tostring(item), clickedItem)) 268 Tracer.addSpan(tr:endSpan()) 269 return clickedItem 270 end 271 end 272 273 -- Check for special column if defined 274 if rowDef.special then 275 local specialX = 2 + (#rowDef.items * rowDef.spacing) 276 local specialWidth = rowDef.specialWidth or rowDef.itemWidth 277 278 -- If click is within the special column bounds 279 if x >= specialX and x < specialX + specialWidth then 280 local clickedItem = specialValues[rowDef.special] 281 tr:addAnnotation(string.format("Clicked special %s (value %d)", rowDef.special, clickedItem)) 282 Tracer.addSpan(tr:endSpan()) 283 return clickedItem 284 end 285 end 286 end 287 end 288 289 -- No valid area was clicked 290 tr:addAnnotation("No valid area clicked") 291 Tracer.addSpan(tr:endSpan()) 292 return nil 293 end 294 295 ---@param amount number The amount to bet 296 ---@param color number The color of the bet 297 ---@param player string The player who placed the bet 298 ---@param number number The number to bet on 299 ---@param parentId string|nil The parent trace ID for logging 300 ---@return nil 301 local function addBet(amount, color, player, number, parentId) 302 local tr = Tracer.new() 303 tr:setName("carpet.addBet") 304 tr:addTag("amount", string.format("%d", amount)) 305 tr:addTag("color", string.format("%d", color)) 306 tr:addTag("player", player) 307 tr:addTag("number", string.format("%d", number)) 308 if parentId then 309 tr:setParentId(parentId) 310 end 311 312 Logger.info("Adding bet: " .. amount .. " from player " .. player .. " on number " .. number) 313 314 -- Check if the player has already placed a bet on this number 315 ---@type Bet 316 local existingBet = nil 317 for i, v in pairs(bets) do 318 Logger.debug("Checking bet " .. i .. " from player " .. v.player) 319 if v.player == player and v.number == number then 320 existingBet = v 321 Logger.info("Found existing bet from " .. player .. " on number " .. number) 322 tr:addAnnotation("Found existing bet") 323 break 324 end 325 end 326 327 if existingBet then 328 Logger.info("Updating existing bet from " .. 329 player .. " from " .. existingBet.amount .. " to " .. (existingBet.amount + amount)) 330 existingBet.amount = existingBet.amount + amount 331 tr:addAnnotation("Updated existing bet") 332 else 333 Logger.info("Creating new bet for " .. player .. " of " .. amount .. " on number " .. number) 334 table.insert(bets, { amount = amount, color = color, player = player, number = number }) 335 tr:addAnnotation("Created new bet") 336 end 337 update(tr.traceId) -- Pass traceId 338 Logger.info("Bet added successfully") 339 Tracer.addSpan(tr:endSpan()) 340 end 341 342 local grid = {} 343 344 ---@param monitorName string 345 ---@param parentId string|nil The parent trace ID for logging 346 function grid.init(monitorName, parentId) 347 local tr = Tracer.new() 348 tr:setName("carpet.init") 349 tr:addTag("monitorName", monitorName) 350 if parentId then 351 tr:setParentId(parentId) 352 end 353 354 mon = peripheral.wrap(monitorName) 355 Logger.info("Looking for monitor peripheral...") 356 357 if mon == nil then 358 tr:addAnnotation("Monitor not found") 359 Tracer.addSpan(tr:endSpan()) 360 error("Monitor not found", 0) 361 return 362 end 363 Logger.info("Monitor found: " .. peripheral.getName(mon)) 364 tr:addAnnotation("Monitor found: " .. peripheral.getName(mon)) 365 366 if not mon.isColour() then 367 tr:addAnnotation("Monitor is not color") 368 Tracer.addSpan(tr:endSpan()) 369 error("Monitor is not color", 0) 370 return 371 end 372 Logger.info("Monitor supports color") 373 tr:addAnnotation("Monitor supports color") 374 375 Logger.info("Setting monitor text scale to 0.7") 376 mon.setTextScale(0.7) 377 378 local w, h = mon.getSize() 379 Logger.info("Monitor size: " .. w .. "x" .. h) 380 tr:addAnnotation(string.format("Monitor size: %dx%d", w, h)) 381 update(tr.traceId) -- Pass traceId 382 Tracer.addSpan(tr:endSpan()) 383 end 384 385 ---@param bet Bet The bet to remove 386 ---@param parentId string|nil The parent trace ID for logging 387 local function removeBet(bet, parentId) 388 local tr = Tracer.new() 389 tr:setName("carpet.removeBet") 390 tr:addTag("player", bet.player or "nil") 391 tr:addTag("amount", string.format("%d", bet.amount)) 392 tr:addTag("number", string.format("%d", bet.number)) 393 if parentId then 394 tr:setParentId(parentId) 395 end 396 397 local removed = false 398 for i, v in ipairs(bets) do 399 if v == bet then 400 table.remove(bets, i) 401 removed = true 402 tr:addAnnotation("Bet removed") 403 break 404 end 405 end 406 if not removed then 407 tr:addAnnotation("Bet not found") 408 end 409 update(tr.traceId) -- Pass traceId 410 Tracer.addSpan(tr:endSpan()) 411 end 412 413 grid.update = update 414 grid.getBets = function() 415 -- No tracing needed for simple getter unless complex logic is added 416 return bets 417 end 418 grid.findClickedNumber = findClickedNumber 419 grid.addBet = addBet 420 grid.removeBet = removeBet 421 422 ---@param parentId string|nil The parent trace ID for logging 423 function grid.resetBets(parentId) 424 local tr = Tracer.new() 425 tr:setName("carpet.resetBets") 426 if parentId then 427 tr:setParentId(parentId) 428 end 429 bets = {} 430 tr:addAnnotation("Bets reset") 431 Tracer.addSpan(tr:endSpan()) 432 -- Optionally call update if the visual state needs immediate clearing 433 -- update(tr.traceId) 434 end 435 436 return grid