/ src / ring.lua
ring.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  -- ==============================
 21  -- Configuration and Initialization
 22  -- ==============================
 23  local mon = nil
 24  local ring = {}
 25  local ballPos = 1
 26  local lastBallPos = { x = nil, y = nil }
 27  local Tracer = require("src.trace") -- Add Tracer require
 28  
 29  -- ==============================
 30  -- Constants
 31  -- ==============================
 32  local RING_SIZE = 36
 33  local ELEMENT_WIDTH = 6
 34  local ELEMENT_HEIGHT = 3
 35  local SPACING_X = ELEMENT_WIDTH
 36  local SPACING_Y = ELEMENT_HEIGHT
 37  local START_X = 4
 38  local START_Y = 4
 39  
 40  -- Colors
 41  local COLOR = {
 42  	BG = colors.green,
 43  	RED = colors.red,
 44  	BLACK = colors.black,
 45  	WHITE = colors.white,
 46  	BLUE = colors.blue,
 47  	GRAY = colors.gray,
 48  	LIGHT = colors.lightGray,
 49  }
 50  
 51  -- ==============================
 52  -- Helper Functions
 53  -- ==============================
 54  
 55  ---Checks if a coordinate is within monitor bounds
 56  ---@param x number X coordinate
 57  ---@param y number Y coordinate
 58  ---@param parentId string|nil Optional parent trace ID
 59  ---@return boolean inBounds True if coordinates are within monitor bounds
 60  local function isInBounds(x, y, parentId)
 61  	if mon == nil then return false end -- Add nil check
 62  	local tr = Tracer.new()
 63  	tr:setName("ring.isInBounds")
 64  	tr:addTag("x", string.format("%d", x))
 65  	tr:addTag("y", string.format("%d", y))
 66  	if parentId then
 67  		tr:setParentId(parentId)
 68  	end
 69  
 70  	local monW, monH = mon.getSize()
 71  	local result = x >= 1 and x <= monW and y >= 1 and y <= monH
 72  
 73  	tr:addAnnotation(string.format("Result: %s", tostring(result)))
 74  	Tracer.addSpan(tr:endSpan())
 75  	return result
 76  end
 77  
 78  ---Draws a single element on the monitor
 79  ---@param x number Left position
 80  ---@param y number Top position
 81  ---@param color number Color from colors table
 82  ---@param number number|nil Optional number to display in the element
 83  ---@param parentId string|nil Optional parent trace ID
 84  local function drawElement(x, y, color, number, parentId)
 85  	if mon == nil then return end -- Add nil check
 86  	local tr = Tracer.new()
 87  	tr:setName("ring.drawElement")
 88  	tr:addTag("x", string.format("%d", x))
 89  	tr:addTag("y", string.format("%d", y))
 90  	tr:addTag("color", string.format("%d", color))
 91  	tr:addTag("number", number and string.format("%d", number) or "nil")
 92  	if parentId then
 93  		tr:setParentId(parentId)
 94  	end
 95  
 96  	-- Draw a rectangle
 97  	mon.setBackgroundColor(color)
 98  	mon.setTextColor(COLOR.WHITE)
 99  
100  	for i = 0, ELEMENT_WIDTH - 1 do
101  		for j = 0, ELEMENT_HEIGHT - 1 do
102  			mon.setCursorPos(x + i, y + j)
103  			mon.write(" ")
104  		end
105  	end
106  
107  	-- Center the number in the element if provided
108  	if number == nil then
109  		tr:addAnnotation("No number provided")
110  		Tracer.addSpan(tr:endSpan())
111  		return
112  	end
113  
114  	local numberX = x + math.floor(ELEMENT_WIDTH / 2)
115  	if number >= 10 then
116  		numberX = numberX - 1
117  	end
118  
119  	mon.setCursorPos(numberX, y + math.floor(ELEMENT_HEIGHT / 2))
120  	mon.write(tostring(number))
121  	tr:addAnnotation(string.format("Drew number %d", number))
122  	Tracer.addSpan(tr:endSpan())
123  end
124  
125  ---Draws a ball element on the monitor
126  ---@param x number Left position
127  ---@param y number Top position
128  ---@param parentId string|nil Optional parent trace ID
129  local function drawBallElement(x, y, parentId)
130  	if mon == nil then return end -- Add nil check
131  	local tr = Tracer.new()
132  	tr:setName("ring.drawBallElement")
133  	tr:addTag("x", string.format("%d", x))
134  	tr:addTag("y", string.format("%d", y))
135  	if parentId then
136  		tr:setParentId(parentId)
137  	end
138  
139  	local ball = {
140  		"  00  ",
141  		"000000",
142  		"  00  ",
143  	}
144  
145  	for i = 1, #ball do
146  		for j = 1, #ball[i] do
147  			mon.setCursorPos(x + j - 1, y + i - 1)
148  			mon.setBackgroundColor(ball[i]:sub(j, j) == "0" and COLOR.BLUE or COLOR.BG)
149  			mon.write(" ")
150  		end
151  	end
152  	tr:addAnnotation("Ball element drawn")
153  	Tracer.addSpan(tr:endSpan())
154  end
155  
156  ---Converts a roulette number to x,y coordinates
157  ---@param number number The roulette number (1-36)
158  ---@param parentId string|nil Optional parent trace ID
159  ---@return number|nil x The x coordinate
160  ---@return number|nil y The y coordinate
161  local function numberToPos(number, parentId)
162  	if mon == nil then return nil, nil end -- Add nil check
163  	local tr = Tracer.new()
164  	tr:setName("ring.numberToPos")
165  	tr:addTag("number", string.format("%d", number))
166  	if parentId then
167  		tr:setParentId(parentId)
168  	end
169  
170  	local posx = START_X + ELEMENT_WIDTH
171  	local posy = START_Y + ELEMENT_HEIGHT
172  	local newx, newy
173  
174  	if number <= 10 then
175  		newx = posx + (number - 1) * SPACING_X
176  		newy = posy
177  	elseif number <= 19 then
178  		newx = posx + SPACING_X * 9
179  		newy = posy + (number - 10) * SPACING_Y
180  	elseif number <= 28 then
181  		newx = posx + SPACING_X * 9 - (number - 19) * SPACING_X
182  		newy = posy + SPACING_Y * 9
183  	elseif number <= 36 then
184  		newx = posx
185  		newy = posy + SPACING_Y * 9 - (number - 28) * SPACING_Y
186  	else
187  		tr:addAnnotation("Invalid number")
188  		Tracer.addSpan(tr:endSpan())
189  		return nil, nil
190  	end
191  
192  	tr:addAnnotation(string.format("Calculated pos: (%d, %d)", newx, newy))
193  	Tracer.addSpan(tr:endSpan())
194  	return newx, newy
195  end
196  
197  ---Draws a line on the monitor
198  ---@param startX number Start X coordinate
199  ---@param startY number Start Y coordinate
200  ---@param endX number End X coordinate
201  ---@param endY number End Y coordinate
202  ---@param color number|nil Optional color (defaults to white)
203  ---@param thickness number|nil Optional line thickness (defaults to 1)
204  ---@param parentId string|nil Optional parent trace ID
205  local function drawLine(startX, startY, endX, endY, color, thickness, parentId)
206  	if mon == nil then return end -- Add nil check
207  	local tr = Tracer.new()
208  	tr:setName("ring.drawLine")
209  	tr:addTag("startX", string.format("%d", startX))
210  	tr:addTag("startY", string.format("%d", startY))
211  	tr:addTag("endX", string.format("%d", endX))
212  	tr:addTag("endY", string.format("%d", endY))
213  	tr:addTag("color", color and string.format("%d", color) or "nil")
214  	tr:addTag("thickness", thickness and string.format("%d", thickness) or "nil")
215  	if parentId then
216  		tr:setParentId(parentId)
217  	end
218  
219  	mon.setBackgroundColor(color or COLOR.WHITE)
220  	thickness = thickness or 1
221  
222  	local dx = math.abs(endX - startX)
223  	local dy = math.abs(endY - startY)
224  	local sx = startX < endX and 1 or -1
225  	local sy = startY < endY and 1 or -1
226  	local err = dx - dy
227  	local x, y = startX, startY
228  	local halfThick = math.floor(thickness / 2)
229  	local pixelsDrawn = 0
230  
231  	while true do
232  		if thickness == 1 then
233  			if isInBounds(x, y, tr.traceId) then
234  				mon.setCursorPos(x, y)
235  				mon.write(" ")
236  				pixelsDrawn = pixelsDrawn + 1
237  			end
238  		else
239  			-- Draw perpendicular to the major axis for thickness
240  			if dx >= dy then -- More horizontal
241  				for i = -halfThick, halfThick do
242  					local drawY = y + i
243  					if isInBounds(x, drawY, tr.traceId) then
244  						mon.setCursorPos(x, drawY)
245  						mon.write(" ")
246  						pixelsDrawn = pixelsDrawn + 1
247  					end
248  				end
249  			else -- More vertical
250  				for i = -halfThick, halfThick do
251  					local drawX = x + i
252  					if isInBounds(drawX, y, tr.traceId) then
253  						mon.setCursorPos(drawX, y)
254  						mon.write(" ")
255  						pixelsDrawn = pixelsDrawn + 1
256  					end
257  				end
258  			end
259  		end
260  
261  		-- Check if we've reached the end point
262  		if x == endX and y == endY then
263  			break
264  		end
265  
266  		-- Calculate next position
267  		local e2 = 2 * err
268  		if e2 > -dy then
269  			err = err - dy
270  			x = x + sx
271  		end
272  		if e2 < dx then
273  			err = err + dx
274  			y = y + sy
275  		end
276  	end
277  	tr:addAnnotation(string.format("Drew %d pixels", pixelsDrawn))
278  	Tracer.addSpan(tr:endSpan())
279  end
280  
281  ---Draws a sequence of elements in a row or column
282  ---@param startNum number Starting number
283  ---@param count number Number of elements to draw
284  ---@param startX number Starting X position
285  ---@param startY number Starting Y position
286  ---@param incrementX number X increment between elements
287  ---@param incrementY number Y increment between elements
288  ---@param parentId string|nil Optional parent trace ID
289  local function drawSequence(startNum, count, startX, startY, incrementX, incrementY, parentId)
290  	if mon == nil then return end -- Add nil check (indirect usage via drawElement)
291  	local tr = Tracer.new()
292  	tr:setName("ring.drawSequence")
293  	tr:addTag("startNum", string.format("%d", startNum))
294  	tr:addTag("count", string.format("%d", count))
295  	tr:addTag("startX", string.format("%d", startX))
296  	tr:addTag("startY", string.format("%d", startY))
297  	tr:addTag("incrementX", string.format("%d", incrementX))
298  	tr:addTag("incrementY", string.format("%d", incrementY))
299  	if parentId then
300  		tr:setParentId(parentId)
301  	end
302  
303  	local x, y = startX, startY
304  	for i = 0, count - 1 do
305  		local num = startNum + i
306  		drawElement(x, y, num % 2 == 0 and COLOR.RED or COLOR.BLACK, num, tr.traceId)
307  		x = x + incrementX
308  		y = y + incrementY
309  	end
310  	tr:addAnnotation(string.format("Drew %d elements", count))
311  	Tracer.addSpan(tr:endSpan())
312  end
313  
314  ---Draws the ball at a specific roulette number position
315  ---@param number number Roulette number position
316  ---@param parentId string|nil Optional parent trace ID
317  local function drawBall(number, parentId)
318  	if mon == nil then return end -- Add nil check (indirect usage via drawElement/drawBallElement)
319  	local tr = Tracer.new()
320  	tr:setName("ring.drawBall")
321  	tr:addTag("number", string.format("%d", number))
322  	if parentId then
323  		tr:setParentId(parentId)
324  	end
325  
326  	local x, y = numberToPos(number, tr.traceId)
327  	if x == nil or y == nil then
328  		tr:addAnnotation("Invalid number, cannot draw ball")
329  		Tracer.addSpan(tr:endSpan())
330  		return
331  	end
332  
333  	-- Clear the last ball position
334  	if lastBallPos.x ~= nil then
335  		tr:addAnnotation(string.format("Clearing last ball at (%d, %d)", lastBallPos.x, lastBallPos.y))
336  		drawElement(lastBallPos.x, lastBallPos.y, COLOR.BG, nil, tr.traceId)
337  	end
338  
339  	-- Draw the new ball
340  	tr:addAnnotation(string.format("Drawing new ball at (%d, %d)", x, y))
341  	drawBallElement(x, y, tr.traceId)
342  
343  	-- Remember this position
344  	lastBallPos.x = x
345  	lastBallPos.y = y
346  	Tracer.addSpan(tr:endSpan())
347  end
348  
349  ---Draws the decorative middle area of the roulette board
350  ---@param startX number Starting X position
351  ---@param startY number Starting Y position
352  ---@param parentId string|nil Optional parent trace ID
353  local function drawMiddleDecoration(startX, startY, parentId)
354  	if mon == nil then return end -- Add nil check (indirect usage via drawLine/isInBounds)
355  	local tr = Tracer.new()
356  	tr:setName("ring.drawMiddleDecoration")
357  	tr:addTag("startX", string.format("%d", startX))
358  	tr:addTag("startY", string.format("%d", startY))
359  	if parentId then
360  		tr:setParentId(parentId)
361  	end
362  
363  	-- Calculate the center of the ring
364  	local endX = SPACING_X * 12 + START_X - 1
365  	local endY = SPACING_Y * 12 + START_Y - 1
366  	local centerX = (startX + endX) / 2
367  	local centerY = (startY + endY) / 2
368  
369  	local writableStartX = startX + ELEMENT_WIDTH * 2
370  	local writableEndX = endX - ELEMENT_WIDTH * 2
371  	local writableStartY = startY + ELEMENT_HEIGHT * 2
372  	local writableEndY = endY - ELEMENT_HEIGHT * 2
373  
374  	-- Ball size
375  	local ballRadius = 4
376  
377  	-- Draw a line at the horizontal center going from left to right
378  	drawLine(writableStartX, centerY, writableEndX, centerY, COLOR.WHITE, 1, tr.traceId)
379  
380  	-- Draw a line at the vertical center going from top to bottom
381  	drawLine(centerX, writableStartY, centerX, writableEndY, COLOR.WHITE, 1, tr.traceId)
382  
383  	-- Write a diagonal line from top-left to bottom-right
384  	drawLine(writableStartX, writableStartY, writableEndX, writableEndY, COLOR.WHITE, 1, tr.traceId)
385  
386  	-- Write a diagonal line from top-right to bottom-left
387  	drawLine(writableEndX, writableStartY, writableStartX, writableEndY, COLOR.WHITE, 1, tr.traceId)
388  
389  	-- Draw a square around the center, with a line encasing the entire writable area
390  	drawLine(writableStartX, writableStartY, writableEndX, writableStartY, COLOR.LIGHT, 1, tr.traceId)
391  	drawLine(writableStartX, writableStartY, writableStartX, writableEndY, COLOR.LIGHT, 1, tr.traceId)
392  	drawLine(writableEndX, writableStartY, writableEndX, writableEndY, COLOR.LIGHT, 1, tr.traceId)
393  	drawLine(writableStartX, writableEndY, writableEndX, writableEndY, COLOR.LIGHT, 1, tr.traceId)
394  
395  	-- Draw the central ball
396  	mon.setBackgroundColor(COLOR.GRAY)
397  	local circlePixels = 0
398  	for y = -ballRadius, ballRadius do
399  		for x = -ballRadius, ballRadius do
400  			-- Check if point is within circle
401  			if x * x + y * y <= ballRadius * ballRadius then
402  				local drawX = centerX + x
403  				local drawY = centerY + y
404  				if isInBounds(drawX, drawY, tr.traceId) then
405  					mon.setCursorPos(drawX, drawY)
406  					mon.write(" ")
407  					circlePixels = circlePixels + 1
408  				end
409  			end
410  		end
411  	end
412  	tr:addAnnotation(string.format("Drew center decoration, circle pixels: %d", circlePixels))
413  	Tracer.addSpan(tr:endSpan())
414  end
415  
416  -- ==============================
417  -- Main Drawing Functions
418  -- ==============================
419  
420  ---Draws the complete roulette ring
421  ---@param parentId string|nil Optional parent trace ID
422  local function drawRing(parentId)
423  	if mon == nil then return end -- Add nil check
424  	local tr = Tracer.new()
425  	tr:setName("ring.drawRing")
426  	if parentId then
427  		tr:setParentId(parentId)
428  	end
429  
430  	mon.setBackgroundColor(COLOR.BG)
431  	mon.clear()
432  
433  	-- Calculate positions
434  	local endX = SPACING_X * 11 + START_X
435  	local endY = SPACING_Y * 11 + START_Y
436  	local midX = endX - ELEMENT_WIDTH
437  	local midY = endY - ELEMENT_HEIGHT
438  
439  	-- Draw corners
440  	drawElement(START_X, START_Y, COLOR.BLACK, nil, tr.traceId)
441  	drawElement(START_X + ELEMENT_WIDTH, START_Y, COLOR.BLACK, 1, tr.traceId)
442  	drawElement(START_X, START_Y + ELEMENT_HEIGHT, COLOR.BLACK, 1, tr.traceId)
443  
444  	-- Draw top row (2-9)
445  	drawSequence(2, 8, START_X + 2 * ELEMENT_WIDTH, START_Y, SPACING_X, 0, tr.traceId)
446  
447  	-- Draw top-right corner
448  	drawElement(midX, START_Y, COLOR.RED, 10, tr.traceId)
449  	drawElement(endX, START_Y, COLOR.RED, nil, tr.traceId)
450  	drawElement(endX, START_Y + ELEMENT_HEIGHT, COLOR.RED, 10, tr.traceId)
451  
452  	-- Draw right column (11-18)
453  	drawSequence(11, 8, endX, START_Y + 2 * ELEMENT_HEIGHT, 0, SPACING_Y, tr.traceId)
454  
455  	-- Draw bottom-right corner
456  	drawElement(endX, midY, COLOR.BLACK, 19, tr.traceId)
457  	drawElement(endX, endY, COLOR.BLACK, nil, tr.traceId)
458  	drawElement(midX, endY, COLOR.BLACK, 19, tr.traceId)
459  
460  	-- Draw bottom row (20-27)
461  	drawSequence(20, 8, midX - ELEMENT_WIDTH, endY, -SPACING_X, 0, tr.traceId)
462  
463  	-- Draw bottom-left corner
464  	drawElement(START_X + ELEMENT_WIDTH, endY, COLOR.RED, 28, tr.traceId)
465  	drawElement(START_X, endY, COLOR.RED, nil, tr.traceId)
466  	drawElement(START_X, midY, COLOR.RED, 28, tr.traceId)
467  
468  	-- Draw left column (29-36)
469  	drawSequence(29, 8, START_X, midY - ELEMENT_HEIGHT, 0, -SPACING_Y, tr.traceId)
470  
471  	-- Draw the decorative middle
472  	drawMiddleDecoration(START_X, START_Y, tr.traceId)
473  
474  	tr:addAnnotation("Ring drawn")
475  	Tracer.addSpan(tr:endSpan())
476  end
477  
478  ---Animates the ball movement with easing
479  ---@param force number How many positions to move
480  ---@param parentId string|nil Optional parent trace ID
481  ---@return number The final ball position
482  local function launchBall(force, parentId)
483  	if mon == nil then return ballPos end -- Add nil check, return current pos
484  	local tr = Tracer.new()
485  	tr:setName("ring.launchBall")
486  	tr:addTag("force", string.format("%d", force))
487  	tr:addTag("startPos", string.format("%d", ballPos))
488  	if parentId then
489  		tr:setParentId(parentId)
490  	end
491  
492  	-- Pre-calculate final position
493  	local newBallPos = (ballPos + force) % RING_SIZE
494  	if newBallPos == 0 then
495  		newBallPos = RING_SIZE
496  	end
497  	tr:addTag("finalPos", string.format("%d", newBallPos))
498  
499  	-- Draw the ring once before animation
500  	drawRing(tr.traceId)
501  
502  	-- Define animation parameters
503  	local minSleep = 0.01
504  	local maxSleep = 0.45
505  	local sleepRange = maxSleep - minSleep
506  
507  	-- Easing function for smooth animation
508  	local function ease(step)
509  		if step < 0.8 then
510  			return 0.5 * step / 0.8
511  		else
512  			return 0.5 + 0.5 * (1 - math.pow(1 - (step - 0.8) / 0.2, 3))
513  		end
514  	end
515  
516  	-- Animate through every position
517  	for step = 1, force do
518  		-- Calculate current position
519  		local currentPos = (ballPos + step) % RING_SIZE
520  		if currentPos == 0 then
521  			currentPos = RING_SIZE
522  		end
523  
524  		-- Draw the ball at each position
525  		drawBall(currentPos, tr.traceId)
526  
527  		-- Add a tiny bit of randomness to the sleep time
528  		local randomFactor = math.random() * 0.02 - 0.01
529  		local sleepTime = minSleep + ease(step / force) * sleepRange + randomFactor
530  		sleep(sleepTime)
531  	end
532  
533  	-- Update ball position to final location
534  	ballPos = newBallPos
535  	tr:addAnnotation(string.format("Animation complete, final position: %d", ballPos))
536  
537  	-- Make the winning number blink
538  	local x, y = numberToPos(ballPos, tr.traceId)
539  	if x and y then
540  		local originalColor = ballPos % 2 == 0 and COLOR.RED or COLOR.BLACK
541  		local blinkCount = 10
542  		tr:addAnnotation(string.format("Blinking winning number %d times", blinkCount))
543  
544  		for i = 1, blinkCount do
545  			-- Invert colors
546  			drawElement(x, y, COLOR.WHITE, ballPos, tr.traceId)
547  			mon.setTextColor(originalColor)
548  			local numberX = x + math.floor(ELEMENT_WIDTH / 2)
549  			if ballPos >= 10 then
550  				numberX = numberX - 1
551  			end
552  			mon.setCursorPos(numberX, y + math.floor(ELEMENT_HEIGHT / 2))
553  			mon.write(tostring(ballPos))
554  			sleep(0.3)
555  
556  			-- Return to original
557  			drawElement(x, y, originalColor, ballPos, tr.traceId)
558  			sleep(0.3)
559  		end
560  
561  		-- Draw ball at final position
562  		drawBall(ballPos, tr.traceId)
563  		tr:addAnnotation("Blinking finished")
564  	else
565  		tr:addAnnotation("Could not get position for blinking")
566  	end
567  	Tracer.addSpan(tr:endSpan())
568  	return ballPos
569  end
570  
571  -- ==============================
572  -- Main Program Loop
573  -- ==============================
574  
575  local ring = {}
576  
577  -- Public API
578  ring.drawRing = function()
579  	drawRing()
580  end
581  ring.launchBall = function(force)
582  	return launchBall(force)
583  end
584  ring.drawBall = function(number)
585  	drawBall(number)
586  end
587  ring.numberToPos = function(number)
588  	return numberToPos(number)
589  end
590  
591  -- Getters
592  function ring.getBallPosition()
593  	local tr = Tracer.new()
594  	tr:setName("ring.getBallPosition")
595  	tr:addAnnotation(string.format("Current ball position: %d", ballPos))
596  	Tracer.addSpan(tr:endSpan())
597  	return ballPos
598  end
599  
600  ---Initializes the ring with a monitor peripheral
601  ---@param monitor string The name of the monitor peripheral
602  function ring.init(monitor)
603  	local tr = Tracer.new()
604  	tr:setName("ring.init")
605  	tr:addTag("monitor", monitor)
606  
607  	mon = peripheral.wrap(monitor)
608  
609  	-- Monitor validation
610  	if mon == nil then
611  		tr:addAnnotation("Monitor not found")
612  		Tracer.addSpan(tr:endSpan())
613  		error("Monitor not found", 0)
614  		return
615  	end
616  	if not mon.isColour() then
617  		tr:addAnnotation("Monitor is not color")
618  		Tracer.addSpan(tr:endSpan())
619  		error("Monitor is not color", 0)
620  		return
621  	end
622  
623  	local w, h = mon.getSize()
624  	tr:addTag("monitorWidth", string.format("%d", w))
625  	tr:addTag("monitorHeight", string.format("%d", h))
626  	-- Center the ring on the monitor
627  	local totalWidth = SPACING_X * 12
628  	local totalHeight = SPACING_Y * 12
629  
630  	START_X = math.floor((w - totalWidth) / 2)
631  	START_Y = math.floor((h - totalHeight) / 2)
632  
633  	-- Ensure minimum margins
634  	START_X = math.max(START_X, 2)
635  	START_Y = math.max(START_Y, 2)
636  	tr:addTag("startX", string.format("%d", START_X))
637  	tr:addTag("startY", string.format("%d", START_Y))
638  
639  	mon.setTextScale(0.5)
640  
641  	drawRing(tr.traceId)
642  	drawBall(ballPos, tr.traceId)
643  	tr:addAnnotation("Initialization complete")
644  	Tracer.addSpan(tr:endSpan())
645  	return ring
646  end
647  
648  -- Return the module
649  return ring