diff --git a/programs/plinko.lua b/programs/plinko.lua index 3076f0b..bbe11e6 100644 --- a/programs/plinko.lua +++ b/programs/plinko.lua @@ -146,9 +146,12 @@ local function buildBoard() local startX = math.floor(PW / 2 - rowW / 2) local py = boardTop + (row - 1) * rowSpacing for col = 1, pegsInRow do + -- Jitter each peg slightly so the layout is never identical + local jx = math.floor((math.random() - 0.5) * colSpacing * 0.35) + local jy = math.floor((math.random() - 0.5) * rowSpacing * 0.35) table.insert(pegs, { - x = startX + (col - 1) * colSpacing, - y = py, + x = startX + (col - 1) * colSpacing + jx, + y = py + jy, }) end end @@ -272,14 +275,23 @@ local function drawBall(bx, by) end local function physicsLoop() - -- Random drop X between the leftmost and rightmost peg in the first row. - local firstPeg = pegs[1] - local lastFirst = pegs[PEG_COLS_TOP] - local dropX = firstPeg.x + math.random() * (lastFirst.x - firstPeg.x) + -- Re-randomise peg positions each drop + buildBoard() + drawBoard() + gpu.sync() + + -- Random drop X anywhere across the board width + local dropX = boardLeft + math.random() * (boardRight - boardLeft) local dropY = boardTop - rowSpacing * 0.6 - local bx, by = dropX, dropY - local vx, vy = (math.random() - 0.5) * 40, 60 + -- Random launch angle: mostly downward but tilted ±35° + local launchAngle = (math.pi / 2) + (math.random() - 0.5) * (math.pi / 180 * 70) + local launchSpeed = 180 + math.random() * 220 -- px/s + + local bx = dropX + local by = dropY + local vx = math.cos(launchAngle) * launchSpeed + local vy = math.sin(launchAngle) * launchSpeed -- positive = downward (screen coords) local lastBx, lastBy = bx, by local elapsed = 0 @@ -514,23 +526,14 @@ local function main() while true do waitForRedstonePulse() - -- Erase subtitle, show "dropping" - px_text_centre("Pull lever to drop", 46, COL_BG, COL_BG, 1) - px_text_centre(" DROPPING... ", 46, 0xFFD600, COL_BG, 1) - gpu.sync() - sleep(0.25) - px_text_centre(" DROPPING... ", 46, COL_BG, COL_BG, 1) - px_text_centre("Pull lever to drop", 46, 0x607080, COL_BG, 1) - gpu.sync() - - -- Run physics — outcome determined by simulation + -- Run physics — redraws board with fresh peg positions, then drops local winIdx = physicsLoop() -- Celebrate flashBucket(winIdx) showResult(buckets[winIdx].mult) - -- Redraw clean board + -- Redraw clean board (fresh pegs already in place from physicsLoop) drawBoard() gpu.sync() end diff --git a/programs/roulette.lua b/programs/roulette.lua index 748813c..8d08230 100644 --- a/programs/roulette.lua +++ b/programs/roulette.lua @@ -1,22 +1,16 @@ --- Roulette Machine — circular wheel, top-down view +-- Roulette Machine — static wheel, physics ball -- Tom's Peripherals GPU + screen wall (any size). -- --- Authentic European pocket order (37 pockets, 0–36). +-- The wheel is completely static — it never rotates. +-- The ball is simulated in 2-D Cartesian coordinates: +-- * Orbits inside a circular track (bounces off outer rim and inner wall) +-- * Has tangential + radial velocity components +-- * Loses energy each bounce (restitution < 1) +-- * When slow enough, crosses the inner wall and bounces around the +-- pocket ring until it comes to rest in a pocket -- --- Physics: --- Rotor : spins CW, decelerates under friction (heavy wheel, slow). --- Ball : orbits outer track CCW at higher speed, decelerates faster. --- When angular speed drops below DROP_SPEED the ball loses --- centripetal support, gains inward radial velocity, and a --- small random deflector-pin kick is applied. --- Ball decelerates in the pocket ring until stopped. --- Result : nearest pocket by angle (ball vs rotor) at rest. --- --- Rendering strategy: --- The full wheel (37 wedges) is expensive to rasterise, so it is only --- redrawn when the rotor has rotated more than ROTOR_REDRAW_THRESH rad --- since the last draw. Between redraws only the ball is erased/repainted --- over the static background — keeping the frame rate smooth. +-- Result: whichever pocket the ball is closest to when it stops. +-- The wheel is drawn once at startup; only the ball moves each frame. ---------------------------------------------------------------------- -- GPU discovery @@ -39,14 +33,9 @@ end -- Constants ---------------------------------------------------------------------- -local FRAME_DELAY = 0.05 -- ~20 fps (keeps CC happy) +local FRAME_DELAY = 0.03 -- ~33 fps local TWO_PI = math.pi * 2 --- Rotor is only redrawn when it has moved this many radians since last draw. --- At R_OUTER ~200px, 0.02 rad ≈ 4px of arc — imperceptible until it accumulates. -local ROTOR_REDRAW_THRESH = 0.025 -- rad - --- European wheel order local WHEEL_ORDER = { 0, 32, 15, 19, 4, 21, 2, 25, 17, 34, 6, 27, 13, 36, 11, 30, 8, 23, 10, 5, 24, 16, 33, 1, 20, 14, 31, 9, @@ -59,48 +48,35 @@ for _, n in ipairs({1,3,5,7,9,12,14,16,18,19,21,23,25,27,30,32,34,36}) do RED_SET[n] = true end --- Geometry (computed in start()) local CX, CY local R_OUTER, R_POCKET_OUT, R_POCKET_IN, R_HUB -- Colours -local COL_BG = 0x050505 -local COL_RIM = 0x8B6914 -local COL_TRACK = 0x1A1A1A -local COL_RED = 0xC62828 -local COL_BLACK = 0x1C1C1C -local COL_GREEN = 0x1B5E20 -local COL_SEP = 0xB8860B -local COL_HUB = 0x2C2C2C -local COL_HUB_RING = 0x8B6914 -local COL_WHITE = 0xFFFFFF -local COL_BALL = 0xF0F0F0 -local COL_BALL_SHD = 0x444444 +local COL_BG = 0x050505 +local COL_RIM = 0x8B6914 +local COL_TRACK = 0x1A1A1A +local COL_RED = 0xC62828 +local COL_BLACK = 0x1C1C1C +local COL_GREEN = 0x1B5E20 +local COL_SEP = 0xB8860B +local COL_HUB = 0x2C2C2C +local COL_HUB_RING = 0x8B6914 +local COL_WHITE = 0xFFFFFF +local COL_BALL = 0xF0F0F0 +local COL_BALL_SHD = 0x444444 --- Physics tunables -local ROTOR_SPEED_MIN = 1.2 -- rad/s -local ROTOR_SPEED_MAX = 2.0 -local ROTOR_FRICTION = 0.06 -- rad/s² - -local BALL_SPEED_MIN = 7.0 -- rad/s (CCW → negative) -local BALL_SPEED_MAX = 11.0 -local TRACK_FRICTION = 0.38 -- rad/s² - --- Radial bounce: ball oscillates between the outer wall and an inner --- wall (the pocket-ring outer edge) while on the track. -local BALL_VR_INIT = 55.0 -- px/s initial inward radial speed -local WALL_RESTITUTION = 0.55 -- fraction of radial speed kept on bounce --- The "pyramid tip" deflector sits at this fraction of the track width --- inward from the outer wall. Ball can bounce off it before dropping. -local DEFLECTOR_FRAC = 0.62 -- 0 = outer wall, 1 = pocket-ring edge - -local DROP_SPEED = 1.4 -- rad/s — ball angular speed at which it finally - -- drops into the pocket ring -local DEFLECT_MAX = 0.22 -- rad — max random angular kick on final drop - -local POCKET_FRICTION = 1.4 -- rad/s² — higher friction in pocket ring - -local BALL_RADIUS = 8 -- px +-- Ball physics +local BALL_RADIUS = 8 -- px +local BALL_SPEED_MIN = 420 -- px/s initial tangential speed +local BALL_SPEED_MAX = 620 +local TRACK_RESTITUTION = 0.72 -- speed fraction kept on track-wall bounce +local POCKET_RESTITUTION = 0.45 -- speed fraction kept bouncing inside pocket ring +local FRICTION_TRACK = 0.992 -- multiplier per frame while in track (energy loss) +local FRICTION_POCKET = 0.970 -- higher damping once in pocket ring +-- Ball enters pocket ring when its speed drops below this +local DROP_SPEED = 90 -- px/s +-- Small random kick angle on each wall bounce +local BOUNCE_KICK_MAX = 0.12 -- rad ---------------------------------------------------------------------- -- GPU / pixel primitives @@ -156,7 +132,7 @@ local function px_text(str, x, y, fg, bg, size) end ---------------------------------------------------------------------- --- Wedge rasteriser (used at startup and for glow flashes only) +-- Wheel drawing (static — drawn once, never redrawn during spin) ---------------------------------------------------------------------- local function pocketColor(num) @@ -165,18 +141,20 @@ local function pocketColor(num) return COL_BLACK end -local function drawWedge(slotIdx, rotorAngle, glowing) +local FIXED_ROTOR = 0 -- wheel never rotates + +local function drawWedge(slotIdx, glowing) local halfArc = math.pi / NUM_POCKETS - local midAngle = rotorAngle + (slotIdx - 1) * TWO_PI / NUM_POCKETS + local midAngle = FIXED_ROTOR + (slotIdx - 1) * TWO_PI / NUM_POCKETS local a0 = midAngle - halfArc local a1 = midAngle + halfArc local num = WHEEL_ORDER[slotIdx] local col = pocketColor(num) if glowing then - local r = math.min(255, math.floor(col / 0x10000) + 60) - local g = math.min(255, math.floor((col % 0x10000) / 0x100) + 60) - local b = math.min(255, col % 0x100 + 60) + local r = math.min(255, math.floor(col / 0x10000) + 70) + local g = math.min(255, math.floor((col % 0x10000) / 0x100) + 70) + local b = math.min(255, col % 0x100 + 70) col = r * 0x10000 + g * 0x100 + b end @@ -185,18 +163,16 @@ local function drawWedge(slotIdx, rotorAngle, glowing) local bx1 = math.ceil (CX + ro) + 1 local by0 = math.floor(CY - ro) - 1 local by1 = math.ceil (CY + ro) + 1 - local arc = (a1 - a0) % TWO_PI + local arc = (a1 - a0) % TWO_PI for sy = by0, by1 do local runStart = nil for sx = bx0, bx1 do - local dx = sx - CX - local dy = sy - CY + local dx = sx - CX; local dy = sy - CY local dist = math.sqrt(dx*dx + dy*dy) - local inRing = dist >= ri and dist <= ro - local rel = (math.atan2(dy, dx) - a0) % TWO_PI + local inRing = dist >= ri and dist <= ro + local rel = (math.atan2(dy, dx) - a0) % TWO_PI local inWedge = rel <= arc - if inRing and inWedge then if not runStart then runStart = sx end else @@ -206,9 +182,7 @@ local function drawWedge(slotIdx, rotorAngle, glowing) end end end - if runStart then - px_rect(runStart, sy, bx1 - runStart + 1, 1, col) - end + if runStart then px_rect(runStart, sy, bx1 - runStart + 1, 1, col) end end px_spoke(CX, CY, ri, ro, a0, COL_SEP) @@ -220,33 +194,27 @@ local function drawWedge(slotIdx, rotorAngle, glowing) px_text(label, lx - (#label * 4), ly - 4, COL_WHITE, col, 1) end --- Draw ALL wedges then overlay static chrome. Yields between wedges --- so CC doesn't timeout; only called when the wheel needs a full repaint. -local function drawAllWedges(rotorAngle, glowSlot) +local function drawAllWedges(glowSlot) for i = 1, NUM_POCKETS do - drawWedge(i, rotorAngle, i == glowSlot) - sleep(0) -- yield once per wedge (37 yields, not thousands) + drawWedge(i, i == glowSlot) + sleep(0) end end local function drawChrome() - -- outer gold rim px_annulus(CX, CY, R_OUTER - 6, R_OUTER, COL_RIM) - -- ball track channel px_annulus(CX, CY, R_POCKET_OUT + 2, R_OUTER - 6, COL_TRACK) - -- inner/outer pocket borders px_annulus(CX, CY, R_POCKET_OUT, R_POCKET_OUT + 2, COL_RIM) px_annulus(CX, CY, R_POCKET_IN - 2, R_POCKET_IN, COL_RIM) - -- hub px_circle(CX, CY, R_HUB, COL_HUB) px_annulus(CX, CY, R_HUB - 4, R_HUB, COL_HUB_RING) px_circle(CX, CY, 6, COL_HUB_RING) px_circle(CX, CY, 3, COL_HUB) end -local function drawWheelFull(rotorAngle, glowSlot) +local function drawWheelFull(glowSlot) px_circle(CX, CY, R_OUTER, COL_BG) - drawAllWedges(rotorAngle, glowSlot) + drawAllWedges(glowSlot) drawChrome() end @@ -256,18 +224,18 @@ end local ballX, ballY = 0, 0 -local function bgColorAt(r) - -- What colour is behind the ball at radius r? - if r > R_POCKET_OUT + 2 then return COL_TRACK end - if r > R_POCKET_IN - 2 then return COL_BLACK end -- approximate — wedge redraws handle exact colour +local function bgAt(bx, by) + local d = math.sqrt((bx - CX)^2 + (by - CY)^2) + if d > R_POCKET_OUT then return COL_TRACK end + if d > R_POCKET_IN then + -- approximate — use average of red/black (dark grey) + return 0x181818 + end return COL_HUB end -local function eraseBall(bx, by, r) - -- Repaint the annulus region the ball touched. - -- Use COL_TRACK for track zone, COL_BLACK for pocket zone (close enough between full redraws). - local dist = math.sqrt((bx - CX)^2 + (by - CY)^2) - px_circle(math.floor(bx), math.floor(by), r + 2, bgColorAt(dist)) +local function eraseBall() + px_circle(ballX, ballY, BALL_RADIUS + 2, bgAt(ballX, ballY)) end local function drawBall(bx, by) @@ -278,13 +246,8 @@ local function drawBall(bx, by) px_circle(ballX - 2, ballY - 2, 2, COL_WHITE) end -local function ballPosAt(radius, angle) - return CX + math.cos(angle) * radius, - CY + math.sin(angle) * radius -end - ---------------------------------------------------------------------- --- Center text overlay +-- Center text ---------------------------------------------------------------------- local function drawCenterText(lines, textSize) @@ -303,141 +266,166 @@ local function drawCenterText(lines, textSize) end ---------------------------------------------------------------------- --- Physics spin +-- Physics spin — Cartesian 2-D ball, static wheel -- --- Phases: --- TRACK : ball on outer track, both rotor+ball decelerating. --- DROP : ball's centripetal support gone; gains inward radial velocity --- + small random deflector-pin kick. --- POCKET : ball in pocket ring, decelerates to rest. +-- Ball position: (bx, by) in pixel space +-- Ball velocity: (vx, vy) in px/s -- --- The wheel is only fully redrawn when the rotor has moved --- ROTOR_REDRAW_THRESH radians. Between redraws only the ball moves. +-- Track outer wall : circle of radius R_WALL_OUT centred on (CX, CY) +-- Track inner wall : circle of radius R_WALL_IN +-- Pocket ring : between R_POCKET_IN and R_POCKET_OUT +-- +-- Collision response: reflect velocity along the surface normal (radial +-- direction), apply restitution, add small random kick to angle. ---------------------------------------------------------------------- -local rotorAngle = 0 -local rotorSpeed = 0 -local lastDrawnRotor = 0 -- rotorAngle at last full wheel redraw - local function spin() local dt = FRAME_DELAY - rotorSpeed = ROTOR_SPEED_MIN + math.random() * (ROTOR_SPEED_MAX - ROTOR_SPEED_MIN) - local ballSpeed = -(BALL_SPEED_MIN + math.random() * (BALL_SPEED_MAX - BALL_SPEED_MIN)) - local ballAngle = math.random() * TWO_PI + local R_WALL_OUT = R_OUTER - 6 - BALL_RADIUS + local R_WALL_IN = R_POCKET_OUT + 2 + BALL_RADIUS + local R_PKT_OUT = R_POCKET_OUT - BALL_RADIUS + local R_PKT_IN = R_POCKET_IN + BALL_RADIUS + local R_SETTLE = (R_POCKET_IN + R_POCKET_OUT) / 2 - -- Track radii - local R_WALL_OUT = R_OUTER - 6 -- inner face of outer gold rim - local R_WALL_IN = R_POCKET_OUT + 2 -- outer face of pocket ring (inner track wall) - local R_DEFLECTOR = R_WALL_OUT - (R_WALL_OUT - R_WALL_IN) * DEFLECTOR_FRAC - local R_SETTLE = (R_POCKET_IN + R_POCKET_OUT) / 2 + -- Start ball at a random angle on the outer track, moving tangentially + local startAngle = math.random() * TWO_PI + local startSpeed = BALL_SPEED_MIN + math.random() * (BALL_SPEED_MAX - BALL_SPEED_MIN) + -- Tangential direction (perpendicular to radial, CCW = 90° CCW from outward normal) + -- Outward normal at angle a: (cos a, sin a) + -- CCW tangent: (-sin a, cos a) + local bx = CX + math.cos(startAngle) * (R_WALL_OUT - 2) + local by = CY + math.sin(startAngle) * (R_WALL_OUT - 2) + local vx = -math.sin(startAngle) * startSpeed + local vy = math.cos(startAngle) * startSpeed - -- Ball starts pressed against the outer wall with a small inward nudge. - local ballR = R_WALL_OUT - BALL_RADIUS - local ballVr = BALL_VR_INIT -- positive = moving inward + local inPocket = false + local elapsed = 0 + local MAX_TIME = 20.0 - local phase = "TRACK" -- "TRACK" | "POCKET" - - -- Initial full draw - drawWheelFull(rotorAngle, nil) - lastDrawnRotor = rotorAngle - local bx0, by0 = ballPosAt(ballR, ballAngle) - drawBall(bx0, by0) + -- Draw initial ball position (wheel already on screen) + drawBall(bx, by) gpu.sync() - while true do - -- ── Rotor ────────────────────────────────────────────────── - if rotorSpeed > 0 then - rotorSpeed = math.max(0, rotorSpeed - ROTOR_FRICTION * dt) - end - rotorAngle = (rotorAngle + rotorSpeed * dt) % TWO_PI + while elapsed < MAX_TIME do + local speed = math.sqrt(vx*vx + vy*vy) - -- ── Ball angular motion ───────────────────────────────────── - local angFriction = (phase == "POCKET") and POCKET_FRICTION or TRACK_FRICTION - if ballSpeed < 0 then - ballSpeed = math.min(0, ballSpeed + angFriction * dt) - else - ballSpeed = math.max(0, ballSpeed - angFriction * dt) - end - ballAngle = (ballAngle + ballSpeed * dt) % TWO_PI + -- Apply friction + local fric = inPocket and FRICTION_POCKET or FRICTION_TRACK + vx = vx * fric + vy = vy * fric - -- ── Radial motion (bounce in track channel) ───────────────── - if phase == "TRACK" then - ballR = ballR + ballVr * dt + -- Integrate + bx = bx + vx * dt + by = by + vy * dt - -- Bounce off outer wall - if ballR <= R_WALL_OUT - BALL_RADIUS then - ballR = R_WALL_OUT - BALL_RADIUS - ballVr = math.abs(ballVr) * WALL_RESTITUTION + -- Distance from centre + local dx = bx - CX + local dy = by - CY + local dist = math.sqrt(dx*dx + dy*dy) + -- Outward unit normal + local nx = dx / dist + local ny = dy / dist + + if not inPocket then + -- ── Outer wall bounce ─────────────────────────────────── + if dist > R_WALL_OUT then + -- Push back inside + bx = CX + nx * R_WALL_OUT + by = CY + ny * R_WALL_OUT + -- Reflect radial component + local vn = vx*nx + vy*ny + vx = vx - 2*vn*nx; vy = vy - 2*vn*ny + -- Apply restitution to the reflected (now inward) normal part + local vn2 = vx*nx + vy*ny + vx = vx - vn2*nx*(1 - TRACK_RESTITUTION) + vy = vy - vn2*ny*(1 - TRACK_RESTITUTION) + -- Small random angular kick + local kick = (math.random() - 0.5) * BOUNCE_KICK_MAX * 2 + local c, s = math.cos(kick), math.sin(kick) + vx, vy = vx*c - vy*s, vx*s + vy*c end - -- Bounce off deflector tip (inner pyramid tip) — only while fast enough - local angSpd = math.abs(ballSpeed) - if ballR >= R_DEFLECTOR and angSpd > DROP_SPEED then - ballR = R_DEFLECTOR - ballVr = -math.abs(ballVr) * WALL_RESTITUTION - -- Small random angular kick from the deflector tip - local kick = (math.random() * 2 - 1) * (DEFLECT_MAX * 0.4) - ballAngle = (ballAngle + kick) % TWO_PI + -- ── Enter pocket ring when slow enough ────────────────── + if speed < DROP_SPEED and dist >= R_WALL_IN - 4 then + inPocket = true end - -- Once angular speed is slow enough the ball can no longer - -- hold centripetal orbit — it falls past the deflector tip - -- into the pocket ring. - if angSpd <= DROP_SPEED and ballR >= R_DEFLECTOR then - phase = "POCKET" - ballVr = math.abs(ballVr) + 30 -- extra inward push - -- Final random deflector kick - local kick = (math.random() * 2 - 1) * DEFLECT_MAX - ballAngle = (ballAngle + kick) % TWO_PI - ballSpeed = ballSpeed * 0.55 + -- ── Inner wall bounce (deflector tip) ─────────────────── + if dist < R_WALL_IN and not inPocket then + bx = CX + nx * R_WALL_IN + by = CY + ny * R_WALL_IN + local vn = vx*nx + vy*ny + vx = vx - 2*vn*nx; vy = vy - 2*vn*ny + local vn2 = vx*nx + vy*ny + vx = vx - vn2*nx*(1 - TRACK_RESTITUTION) + vy = vy - vn2*ny*(1 - TRACK_RESTITUTION) + local kick = (math.random() - 0.5) * BOUNCE_KICK_MAX * 2 + local c, s = math.cos(kick), math.sin(kick) + vx, vy = vx*c - vy*s, vx*s + vy*c end else - -- POCKET phase: slide inward to R_SETTLE, then stop. - ballR = ballR + ballVr * dt - ballVr = ballVr * (1 - 5 * dt) - if ballR >= R_SETTLE then - ballR = R_SETTLE - ballVr = 0 + -- ── Inside pocket ring ─────────────────────────────────── + -- Bounce off outer pocket wall + if dist > R_PKT_OUT then + bx = CX + nx * R_PKT_OUT + by = CY + ny * R_PKT_OUT + local vn = vx*nx + vy*ny + vx = vx - 2*vn*nx; vy = vy - 2*vn*ny + local vn2 = vx*nx + vy*ny + vx = vx - vn2*nx*(1 - POCKET_RESTITUTION) + vy = vy - vn2*ny*(1 - POCKET_RESTITUTION) + local kick = (math.random() - 0.5) * BOUNCE_KICK_MAX * 2 + local c, s = math.cos(kick), math.sin(kick) + vx, vy = vx*c - vy*s, vx*s + vy*c end + -- Bounce off inner pocket wall + if dist < R_PKT_IN then + bx = CX + nx * R_PKT_IN + by = CY + ny * R_PKT_IN + local vn = vx*nx + vy*ny + vx = vx - 2*vn*nx; vy = vy - 2*vn*ny + local vn2 = vx*nx + vy*ny + vx = vx - vn2*nx*(1 - POCKET_RESTITUTION) + vy = vy - vn2*ny*(1 - POCKET_RESTITUTION) + local kick = (math.random() - 0.5) * BOUNCE_KICK_MAX * 2 + local c, s = math.cos(kick), math.sin(kick) + vx, vy = vx*c - vy*s, vx*s + vy*c + end + + -- Settled? + if speed < 6 then break end end - -- ── Redraw wheel if rotor has moved enough ────────────────── - local rotorDelta = math.abs(rotorAngle - lastDrawnRotor) - if rotorDelta > math.pi then rotorDelta = TWO_PI - rotorDelta end - - if rotorDelta >= ROTOR_REDRAW_THRESH then - eraseBall(ballX, ballY, BALL_RADIUS) - drawAllWedges(rotorAngle, nil) - drawChrome() - lastDrawnRotor = rotorAngle - end - - -- ── Ball render ───────────────────────────────────────────── - eraseBall(ballX, ballY, BALL_RADIUS) - local bx, by = ballPosAt(ballR, ballAngle) + eraseBall() drawBall(bx, by) gpu.sync() sleep(dt) - - -- ── Stop condition ────────────────────────────────────────── - if phase == "POCKET" and ballSpeed == 0 and ballVr == 0 then break end + elapsed = elapsed + dt end - -- Determine winning pocket - local relAngle = (ballAngle - rotorAngle) % TWO_PI + -- Final position + eraseBall() + drawBall(bx, by) + gpu.sync() + + -- Nearest pocket by angle + local finalAngle = math.atan2(by - CY, bx - CX) local bestSlot, bestDist = 1, math.huge for i = 1, NUM_POCKETS do local sa = ((i - 1) * TWO_PI / NUM_POCKETS) % TWO_PI - local diff = math.abs(sa - relAngle) + -- normalise finalAngle to [0, 2pi) + local fa = finalAngle % TWO_PI + local diff = math.abs(sa - fa) if diff > math.pi then diff = TWO_PI - diff end if diff < bestDist then bestDist = diff; bestSlot = i end end - -- Snap ball to pocket centre - local snapAngle = rotorAngle + (bestSlot - 1) * TWO_PI / NUM_POCKETS - local sx, sy = ballPosAt(R_SETTLE, snapAngle) - eraseBall(ballX, ballY, BALL_RADIUS) + -- Snap to pocket centre + local snapAngle = FIXED_ROTOR + (bestSlot - 1) * TWO_PI / NUM_POCKETS + local sx = CX + math.cos(snapAngle) * R_SETTLE + local sy = CY + math.sin(snapAngle) * R_SETTLE + eraseBall() drawBall(sx, sy) gpu.sync() @@ -450,15 +438,16 @@ end local function glowAnimation(slotIdx) local R_SETTLE = (R_POCKET_IN + R_POCKET_OUT) / 2 - local sa = rotorAngle + (slotIdx - 1) * TWO_PI / NUM_POCKETS - local bx, by = ballPosAt(R_SETTLE, sa) + local sa = FIXED_ROTOR + (slotIdx - 1) * TWO_PI / NUM_POCKETS + local bx = CX + math.cos(sa) * R_SETTLE + local by = CY + math.sin(sa) * R_SETTLE for flash = 1, 6 do - drawWedge(slotIdx, rotorAngle, flash % 2 == 1) + drawWedge(slotIdx, flash % 2 == 1) drawBall(bx, by) gpu.sync() - sleep(0.18) + sleep(0.15) end - drawWedge(slotIdx, rotorAngle, true) + drawWedge(slotIdx, true) drawBall(bx, by) gpu.sync() end @@ -505,7 +494,7 @@ local function start() R_HUB = math.floor(R_MAX * 0.38) gpu.fill(COL_BG) - drawWheelFull(rotorAngle, nil) + drawWheelFull(nil) drawCenterText({ "ROULETTE", "Pull lever" }) end @@ -518,7 +507,7 @@ local function main() waitForRedstonePulse() drawCenterText({ "SPINNING..." }) - sleep(0.2) + sleep(0.1) local num, slotIdx = spin() @@ -532,8 +521,9 @@ local function main() sleep(5) - drawWheelFull(rotorAngle, nil) - lastDrawnRotor = rotorAngle + -- Erase ball, redraw wheel clean + eraseBall() + drawChrome() drawCenterText({ "ROULETTE", "Pull lever" }) end end