/ src / carpet.lua
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