updated
This commit is contained in:
@@ -146,9 +146,12 @@ local function buildBoard()
|
|||||||
local startX = math.floor(PW / 2 - rowW / 2)
|
local startX = math.floor(PW / 2 - rowW / 2)
|
||||||
local py = boardTop + (row - 1) * rowSpacing
|
local py = boardTop + (row - 1) * rowSpacing
|
||||||
for col = 1, pegsInRow do
|
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, {
|
table.insert(pegs, {
|
||||||
x = startX + (col - 1) * colSpacing,
|
x = startX + (col - 1) * colSpacing + jx,
|
||||||
y = py,
|
y = py + jy,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -272,14 +275,23 @@ local function drawBall(bx, by)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function physicsLoop()
|
local function physicsLoop()
|
||||||
-- Random drop X between the leftmost and rightmost peg in the first row.
|
-- Re-randomise peg positions each drop
|
||||||
local firstPeg = pegs[1]
|
buildBoard()
|
||||||
local lastFirst = pegs[PEG_COLS_TOP]
|
drawBoard()
|
||||||
local dropX = firstPeg.x + math.random() * (lastFirst.x - firstPeg.x)
|
gpu.sync()
|
||||||
|
|
||||||
|
-- Random drop X anywhere across the board width
|
||||||
|
local dropX = boardLeft + math.random() * (boardRight - boardLeft)
|
||||||
local dropY = boardTop - rowSpacing * 0.6
|
local dropY = boardTop - rowSpacing * 0.6
|
||||||
|
|
||||||
local bx, by = dropX, dropY
|
-- Random launch angle: mostly downward but tilted ±35°
|
||||||
local vx, vy = (math.random() - 0.5) * 40, 60
|
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 lastBx, lastBy = bx, by
|
||||||
local elapsed = 0
|
local elapsed = 0
|
||||||
@@ -514,23 +526,14 @@ local function main()
|
|||||||
while true do
|
while true do
|
||||||
waitForRedstonePulse()
|
waitForRedstonePulse()
|
||||||
|
|
||||||
-- Erase subtitle, show "dropping"
|
-- Run physics — redraws board with fresh peg positions, then drops
|
||||||
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
|
|
||||||
local winIdx = physicsLoop()
|
local winIdx = physicsLoop()
|
||||||
|
|
||||||
-- Celebrate
|
-- Celebrate
|
||||||
flashBucket(winIdx)
|
flashBucket(winIdx)
|
||||||
showResult(buckets[winIdx].mult)
|
showResult(buckets[winIdx].mult)
|
||||||
|
|
||||||
-- Redraw clean board
|
-- Redraw clean board (fresh pegs already in place from physicsLoop)
|
||||||
drawBoard()
|
drawBoard()
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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).
|
-- 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:
|
-- Result: whichever pocket the ball is closest to when it stops.
|
||||||
-- Rotor : spins CW, decelerates under friction (heavy wheel, slow).
|
-- The wheel is drawn once at startup; only the ball moves each frame.
|
||||||
-- 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.
|
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- GPU discovery
|
-- GPU discovery
|
||||||
@@ -39,14 +33,9 @@ end
|
|||||||
-- Constants
|
-- Constants
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
local FRAME_DELAY = 0.05 -- ~20 fps (keeps CC happy)
|
local FRAME_DELAY = 0.03 -- ~33 fps
|
||||||
local TWO_PI = math.pi * 2
|
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 = {
|
local WHEEL_ORDER = {
|
||||||
0, 32, 15, 19, 4, 21, 2, 25, 17, 34, 6, 27, 13, 36,
|
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,
|
11, 30, 8, 23, 10, 5, 24, 16, 33, 1, 20, 14, 31, 9,
|
||||||
@@ -59,7 +48,6 @@ 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
|
RED_SET[n] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Geometry (computed in start())
|
|
||||||
local CX, CY
|
local CX, CY
|
||||||
local R_OUTER, R_POCKET_OUT, R_POCKET_IN, R_HUB
|
local R_OUTER, R_POCKET_OUT, R_POCKET_IN, R_HUB
|
||||||
|
|
||||||
@@ -77,30 +65,18 @@ local COL_WHITE = 0xFFFFFF
|
|||||||
local COL_BALL = 0xF0F0F0
|
local COL_BALL = 0xF0F0F0
|
||||||
local COL_BALL_SHD = 0x444444
|
local COL_BALL_SHD = 0x444444
|
||||||
|
|
||||||
-- Physics tunables
|
-- Ball physics
|
||||||
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
|
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
|
-- GPU / pixel primitives
|
||||||
@@ -156,7 +132,7 @@ local function px_text(str, x, y, fg, bg, size)
|
|||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Wedge rasteriser (used at startup and for glow flashes only)
|
-- Wheel drawing (static — drawn once, never redrawn during spin)
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
local function pocketColor(num)
|
local function pocketColor(num)
|
||||||
@@ -165,18 +141,20 @@ local function pocketColor(num)
|
|||||||
return COL_BLACK
|
return COL_BLACK
|
||||||
end
|
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 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 a0 = midAngle - halfArc
|
||||||
local a1 = midAngle + halfArc
|
local a1 = midAngle + halfArc
|
||||||
|
|
||||||
local num = WHEEL_ORDER[slotIdx]
|
local num = WHEEL_ORDER[slotIdx]
|
||||||
local col = pocketColor(num)
|
local col = pocketColor(num)
|
||||||
if glowing then
|
if glowing then
|
||||||
local r = math.min(255, math.floor(col / 0x10000) + 60)
|
local r = math.min(255, math.floor(col / 0x10000) + 70)
|
||||||
local g = math.min(255, math.floor((col % 0x10000) / 0x100) + 60)
|
local g = math.min(255, math.floor((col % 0x10000) / 0x100) + 70)
|
||||||
local b = math.min(255, col % 0x100 + 60)
|
local b = math.min(255, col % 0x100 + 70)
|
||||||
col = r * 0x10000 + g * 0x100 + b
|
col = r * 0x10000 + g * 0x100 + b
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -190,13 +168,11 @@ local function drawWedge(slotIdx, rotorAngle, glowing)
|
|||||||
for sy = by0, by1 do
|
for sy = by0, by1 do
|
||||||
local runStart = nil
|
local runStart = nil
|
||||||
for sx = bx0, bx1 do
|
for sx = bx0, bx1 do
|
||||||
local dx = sx - CX
|
local dx = sx - CX; local dy = sy - CY
|
||||||
local dy = sy - CY
|
|
||||||
local dist = math.sqrt(dx*dx + dy*dy)
|
local dist = math.sqrt(dx*dx + dy*dy)
|
||||||
local inRing = dist >= ri and dist <= ro
|
local inRing = dist >= ri and dist <= ro
|
||||||
local rel = (math.atan2(dy, dx) - a0) % TWO_PI
|
local rel = (math.atan2(dy, dx) - a0) % TWO_PI
|
||||||
local inWedge = rel <= arc
|
local inWedge = rel <= arc
|
||||||
|
|
||||||
if inRing and inWedge then
|
if inRing and inWedge then
|
||||||
if not runStart then runStart = sx end
|
if not runStart then runStart = sx end
|
||||||
else
|
else
|
||||||
@@ -206,9 +182,7 @@ local function drawWedge(slotIdx, rotorAngle, glowing)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if runStart then
|
if runStart then px_rect(runStart, sy, bx1 - runStart + 1, 1, col) end
|
||||||
px_rect(runStart, sy, bx1 - runStart + 1, 1, col)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
px_spoke(CX, CY, ri, ro, a0, COL_SEP)
|
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)
|
px_text(label, lx - (#label * 4), ly - 4, COL_WHITE, col, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Draw ALL wedges then overlay static chrome. Yields between wedges
|
local function drawAllWedges(glowSlot)
|
||||||
-- so CC doesn't timeout; only called when the wheel needs a full repaint.
|
|
||||||
local function drawAllWedges(rotorAngle, glowSlot)
|
|
||||||
for i = 1, NUM_POCKETS do
|
for i = 1, NUM_POCKETS do
|
||||||
drawWedge(i, rotorAngle, i == glowSlot)
|
drawWedge(i, i == glowSlot)
|
||||||
sleep(0) -- yield once per wedge (37 yields, not thousands)
|
sleep(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function drawChrome()
|
local function drawChrome()
|
||||||
-- outer gold rim
|
|
||||||
px_annulus(CX, CY, R_OUTER - 6, R_OUTER, COL_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)
|
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_OUT, R_POCKET_OUT + 2, COL_RIM)
|
||||||
px_annulus(CX, CY, R_POCKET_IN - 2, R_POCKET_IN, 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_circle(CX, CY, R_HUB, COL_HUB)
|
||||||
px_annulus(CX, CY, R_HUB - 4, R_HUB, COL_HUB_RING)
|
px_annulus(CX, CY, R_HUB - 4, R_HUB, COL_HUB_RING)
|
||||||
px_circle(CX, CY, 6, COL_HUB_RING)
|
px_circle(CX, CY, 6, COL_HUB_RING)
|
||||||
px_circle(CX, CY, 3, COL_HUB)
|
px_circle(CX, CY, 3, COL_HUB)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function drawWheelFull(rotorAngle, glowSlot)
|
local function drawWheelFull(glowSlot)
|
||||||
px_circle(CX, CY, R_OUTER, COL_BG)
|
px_circle(CX, CY, R_OUTER, COL_BG)
|
||||||
drawAllWedges(rotorAngle, glowSlot)
|
drawAllWedges(glowSlot)
|
||||||
drawChrome()
|
drawChrome()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -256,18 +224,18 @@ end
|
|||||||
|
|
||||||
local ballX, ballY = 0, 0
|
local ballX, ballY = 0, 0
|
||||||
|
|
||||||
local function bgColorAt(r)
|
local function bgAt(bx, by)
|
||||||
-- What colour is behind the ball at radius r?
|
local d = math.sqrt((bx - CX)^2 + (by - CY)^2)
|
||||||
if r > R_POCKET_OUT + 2 then return COL_TRACK end
|
if d > R_POCKET_OUT then return COL_TRACK end
|
||||||
if r > R_POCKET_IN - 2 then return COL_BLACK end -- approximate — wedge redraws handle exact colour
|
if d > R_POCKET_IN then
|
||||||
|
-- approximate — use average of red/black (dark grey)
|
||||||
|
return 0x181818
|
||||||
|
end
|
||||||
return COL_HUB
|
return COL_HUB
|
||||||
end
|
end
|
||||||
|
|
||||||
local function eraseBall(bx, by, r)
|
local function eraseBall()
|
||||||
-- Repaint the annulus region the ball touched.
|
px_circle(ballX, ballY, BALL_RADIUS + 2, bgAt(ballX, ballY))
|
||||||
-- 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))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function drawBall(bx, by)
|
local function drawBall(bx, by)
|
||||||
@@ -278,13 +246,8 @@ local function drawBall(bx, by)
|
|||||||
px_circle(ballX - 2, ballY - 2, 2, COL_WHITE)
|
px_circle(ballX - 2, ballY - 2, 2, COL_WHITE)
|
||||||
end
|
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)
|
local function drawCenterText(lines, textSize)
|
||||||
@@ -303,141 +266,166 @@ local function drawCenterText(lines, textSize)
|
|||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Physics spin
|
-- Physics spin — Cartesian 2-D ball, static wheel
|
||||||
--
|
--
|
||||||
-- Phases:
|
-- Ball position: (bx, by) in pixel space
|
||||||
-- TRACK : ball on outer track, both rotor+ball decelerating.
|
-- Ball velocity: (vx, vy) in px/s
|
||||||
-- DROP : ball's centripetal support gone; gains inward radial velocity
|
|
||||||
-- + small random deflector-pin kick.
|
|
||||||
-- POCKET : ball in pocket ring, decelerates to rest.
|
|
||||||
--
|
--
|
||||||
-- The wheel is only fully redrawn when the rotor has moved
|
-- Track outer wall : circle of radius R_WALL_OUT centred on (CX, CY)
|
||||||
-- ROTOR_REDRAW_THRESH radians. Between redraws only the ball moves.
|
-- 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 function spin()
|
||||||
local dt = FRAME_DELAY
|
local dt = FRAME_DELAY
|
||||||
|
|
||||||
rotorSpeed = ROTOR_SPEED_MIN + math.random() * (ROTOR_SPEED_MAX - ROTOR_SPEED_MIN)
|
local R_WALL_OUT = R_OUTER - 6 - BALL_RADIUS
|
||||||
local ballSpeed = -(BALL_SPEED_MIN + math.random() * (BALL_SPEED_MAX - BALL_SPEED_MIN))
|
local R_WALL_IN = R_POCKET_OUT + 2 + BALL_RADIUS
|
||||||
local ballAngle = math.random() * TWO_PI
|
local R_PKT_OUT = R_POCKET_OUT - BALL_RADIUS
|
||||||
|
local R_PKT_IN = R_POCKET_IN + BALL_RADIUS
|
||||||
-- 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
|
local R_SETTLE = (R_POCKET_IN + R_POCKET_OUT) / 2
|
||||||
|
|
||||||
-- Ball starts pressed against the outer wall with a small inward nudge.
|
-- Start ball at a random angle on the outer track, moving tangentially
|
||||||
local ballR = R_WALL_OUT - BALL_RADIUS
|
local startAngle = math.random() * TWO_PI
|
||||||
local ballVr = BALL_VR_INIT -- positive = moving inward
|
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
|
||||||
|
|
||||||
local phase = "TRACK" -- "TRACK" | "POCKET"
|
local inPocket = false
|
||||||
|
local elapsed = 0
|
||||||
|
local MAX_TIME = 20.0
|
||||||
|
|
||||||
-- Initial full draw
|
-- Draw initial ball position (wheel already on screen)
|
||||||
drawWheelFull(rotorAngle, nil)
|
drawBall(bx, by)
|
||||||
lastDrawnRotor = rotorAngle
|
|
||||||
local bx0, by0 = ballPosAt(ballR, ballAngle)
|
|
||||||
drawBall(bx0, by0)
|
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
|
|
||||||
while true do
|
while elapsed < MAX_TIME do
|
||||||
-- ── Rotor ──────────────────────────────────────────────────
|
local speed = math.sqrt(vx*vx + vy*vy)
|
||||||
if rotorSpeed > 0 then
|
|
||||||
rotorSpeed = math.max(0, rotorSpeed - ROTOR_FRICTION * dt)
|
|
||||||
end
|
|
||||||
rotorAngle = (rotorAngle + rotorSpeed * dt) % TWO_PI
|
|
||||||
|
|
||||||
-- ── Ball angular motion ─────────────────────────────────────
|
-- Apply friction
|
||||||
local angFriction = (phase == "POCKET") and POCKET_FRICTION or TRACK_FRICTION
|
local fric = inPocket and FRICTION_POCKET or FRICTION_TRACK
|
||||||
if ballSpeed < 0 then
|
vx = vx * fric
|
||||||
ballSpeed = math.min(0, ballSpeed + angFriction * dt)
|
vy = vy * fric
|
||||||
else
|
|
||||||
ballSpeed = math.max(0, ballSpeed - angFriction * dt)
|
|
||||||
end
|
|
||||||
ballAngle = (ballAngle + ballSpeed * dt) % TWO_PI
|
|
||||||
|
|
||||||
-- ── Radial motion (bounce in track channel) ─────────────────
|
-- Integrate
|
||||||
if phase == "TRACK" then
|
bx = bx + vx * dt
|
||||||
ballR = ballR + ballVr * dt
|
by = by + vy * dt
|
||||||
|
|
||||||
-- Bounce off outer wall
|
-- Distance from centre
|
||||||
if ballR <= R_WALL_OUT - BALL_RADIUS then
|
local dx = bx - CX
|
||||||
ballR = R_WALL_OUT - BALL_RADIUS
|
local dy = by - CY
|
||||||
ballVr = math.abs(ballVr) * WALL_RESTITUTION
|
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
|
end
|
||||||
|
|
||||||
-- Bounce off deflector tip (inner pyramid tip) — only while fast enough
|
-- ── Enter pocket ring when slow enough ──────────────────
|
||||||
local angSpd = math.abs(ballSpeed)
|
if speed < DROP_SPEED and dist >= R_WALL_IN - 4 then
|
||||||
if ballR >= R_DEFLECTOR and angSpd > DROP_SPEED then
|
inPocket = true
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Once angular speed is slow enough the ball can no longer
|
-- ── Inner wall bounce (deflector tip) ───────────────────
|
||||||
-- hold centripetal orbit — it falls past the deflector tip
|
if dist < R_WALL_IN and not inPocket then
|
||||||
-- into the pocket ring.
|
bx = CX + nx * R_WALL_IN
|
||||||
if angSpd <= DROP_SPEED and ballR >= R_DEFLECTOR then
|
by = CY + ny * R_WALL_IN
|
||||||
phase = "POCKET"
|
local vn = vx*nx + vy*ny
|
||||||
ballVr = math.abs(ballVr) + 30 -- extra inward push
|
vx = vx - 2*vn*nx; vy = vy - 2*vn*ny
|
||||||
-- Final random deflector kick
|
local vn2 = vx*nx + vy*ny
|
||||||
local kick = (math.random() * 2 - 1) * DEFLECT_MAX
|
vx = vx - vn2*nx*(1 - TRACK_RESTITUTION)
|
||||||
ballAngle = (ballAngle + kick) % TWO_PI
|
vy = vy - vn2*ny*(1 - TRACK_RESTITUTION)
|
||||||
ballSpeed = ballSpeed * 0.55
|
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
|
end
|
||||||
else
|
else
|
||||||
-- POCKET phase: slide inward to R_SETTLE, then stop.
|
-- ── Inside pocket ring ───────────────────────────────────
|
||||||
ballR = ballR + ballVr * dt
|
-- Bounce off outer pocket wall
|
||||||
ballVr = ballVr * (1 - 5 * dt)
|
if dist > R_PKT_OUT then
|
||||||
if ballR >= R_SETTLE then
|
bx = CX + nx * R_PKT_OUT
|
||||||
ballR = R_SETTLE
|
by = CY + ny * R_PKT_OUT
|
||||||
ballVr = 0
|
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
|
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
|
end
|
||||||
|
|
||||||
-- ── Redraw wheel if rotor has moved enough ──────────────────
|
-- Settled?
|
||||||
local rotorDelta = math.abs(rotorAngle - lastDrawnRotor)
|
if speed < 6 then break end
|
||||||
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
|
end
|
||||||
|
|
||||||
-- ── Ball render ─────────────────────────────────────────────
|
eraseBall()
|
||||||
eraseBall(ballX, ballY, BALL_RADIUS)
|
|
||||||
local bx, by = ballPosAt(ballR, ballAngle)
|
|
||||||
drawBall(bx, by)
|
drawBall(bx, by)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
sleep(dt)
|
sleep(dt)
|
||||||
|
elapsed = elapsed + dt
|
||||||
-- ── Stop condition ──────────────────────────────────────────
|
|
||||||
if phase == "POCKET" and ballSpeed == 0 and ballVr == 0 then break end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Determine winning pocket
|
-- Final position
|
||||||
local relAngle = (ballAngle - rotorAngle) % TWO_PI
|
eraseBall()
|
||||||
|
drawBall(bx, by)
|
||||||
|
gpu.sync()
|
||||||
|
|
||||||
|
-- Nearest pocket by angle
|
||||||
|
local finalAngle = math.atan2(by - CY, bx - CX)
|
||||||
local bestSlot, bestDist = 1, math.huge
|
local bestSlot, bestDist = 1, math.huge
|
||||||
for i = 1, NUM_POCKETS do
|
for i = 1, NUM_POCKETS do
|
||||||
local sa = ((i - 1) * TWO_PI / NUM_POCKETS) % TWO_PI
|
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 > math.pi then diff = TWO_PI - diff end
|
||||||
if diff < bestDist then bestDist = diff; bestSlot = i end
|
if diff < bestDist then bestDist = diff; bestSlot = i end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Snap ball to pocket centre
|
-- Snap to pocket centre
|
||||||
local snapAngle = rotorAngle + (bestSlot - 1) * TWO_PI / NUM_POCKETS
|
local snapAngle = FIXED_ROTOR + (bestSlot - 1) * TWO_PI / NUM_POCKETS
|
||||||
local sx, sy = ballPosAt(R_SETTLE, snapAngle)
|
local sx = CX + math.cos(snapAngle) * R_SETTLE
|
||||||
eraseBall(ballX, ballY, BALL_RADIUS)
|
local sy = CY + math.sin(snapAngle) * R_SETTLE
|
||||||
|
eraseBall()
|
||||||
drawBall(sx, sy)
|
drawBall(sx, sy)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
|
|
||||||
@@ -450,15 +438,16 @@ end
|
|||||||
|
|
||||||
local function glowAnimation(slotIdx)
|
local function glowAnimation(slotIdx)
|
||||||
local R_SETTLE = (R_POCKET_IN + R_POCKET_OUT) / 2
|
local R_SETTLE = (R_POCKET_IN + R_POCKET_OUT) / 2
|
||||||
local sa = rotorAngle + (slotIdx - 1) * TWO_PI / NUM_POCKETS
|
local sa = FIXED_ROTOR + (slotIdx - 1) * TWO_PI / NUM_POCKETS
|
||||||
local bx, by = ballPosAt(R_SETTLE, sa)
|
local bx = CX + math.cos(sa) * R_SETTLE
|
||||||
|
local by = CY + math.sin(sa) * R_SETTLE
|
||||||
for flash = 1, 6 do
|
for flash = 1, 6 do
|
||||||
drawWedge(slotIdx, rotorAngle, flash % 2 == 1)
|
drawWedge(slotIdx, flash % 2 == 1)
|
||||||
drawBall(bx, by)
|
drawBall(bx, by)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
sleep(0.18)
|
sleep(0.15)
|
||||||
end
|
end
|
||||||
drawWedge(slotIdx, rotorAngle, true)
|
drawWedge(slotIdx, true)
|
||||||
drawBall(bx, by)
|
drawBall(bx, by)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
end
|
end
|
||||||
@@ -505,7 +494,7 @@ local function start()
|
|||||||
R_HUB = math.floor(R_MAX * 0.38)
|
R_HUB = math.floor(R_MAX * 0.38)
|
||||||
|
|
||||||
gpu.fill(COL_BG)
|
gpu.fill(COL_BG)
|
||||||
drawWheelFull(rotorAngle, nil)
|
drawWheelFull(nil)
|
||||||
drawCenterText({ "ROULETTE", "Pull lever" })
|
drawCenterText({ "ROULETTE", "Pull lever" })
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -518,7 +507,7 @@ local function main()
|
|||||||
waitForRedstonePulse()
|
waitForRedstonePulse()
|
||||||
|
|
||||||
drawCenterText({ "SPINNING..." })
|
drawCenterText({ "SPINNING..." })
|
||||||
sleep(0.2)
|
sleep(0.1)
|
||||||
|
|
||||||
local num, slotIdx = spin()
|
local num, slotIdx = spin()
|
||||||
|
|
||||||
@@ -532,8 +521,9 @@ local function main()
|
|||||||
|
|
||||||
sleep(5)
|
sleep(5)
|
||||||
|
|
||||||
drawWheelFull(rotorAngle, nil)
|
-- Erase ball, redraw wheel clean
|
||||||
lastDrawnRotor = rotorAngle
|
eraseBall()
|
||||||
|
drawChrome()
|
||||||
drawCenterText({ "ROULETTE", "Pull lever" })
|
drawCenterText({ "ROULETTE", "Pull lever" })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user