updated
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
-- Plinko Machine (fully physics-based)
|
-- Plichinko Machine (fully physics-based)
|
||||||
-- Tom's Peripherals GPU + screen wall (any size).
|
-- Tom's Peripherals GPU + screen wall (any size).
|
||||||
--
|
--
|
||||||
-- A ball is dropped from a random X position near the top.
|
-- A ball is dropped from a random X position near the top.
|
||||||
-- It falls under gravity and collides with circular pegs using
|
-- It falls under gravity and collides with circular pegs using
|
||||||
-- proper elastic circle-circle collision response (reflect velocity
|
-- proper elastic circle-circle collision response (reflect velocity
|
||||||
-- along the contact normal). The bucket it lands in is determined
|
-- along the contact normal). The ball also bounces off the side
|
||||||
-- entirely by the physics — no predetermined outcome.
|
-- walls and the bucket dividers. The bucket it lands in is
|
||||||
|
-- determined entirely by the physics — no predetermined outcome.
|
||||||
--
|
--
|
||||||
-- Trigger: redstone pulse on any side.
|
-- Trigger: redstone pulse on any side.
|
||||||
|
|
||||||
@@ -33,10 +34,13 @@ end
|
|||||||
local FRAME_DELAY = 0.02 -- s (~50 fps)
|
local FRAME_DELAY = 0.02 -- s (~50 fps)
|
||||||
local DT = FRAME_DELAY
|
local DT = FRAME_DELAY
|
||||||
local GRAVITY = 700 -- px/s²
|
local GRAVITY = 700 -- px/s²
|
||||||
local RESTITUTION = 0.55 -- fraction of normal speed kept after bounce
|
local RESTITUTION = 0.55 -- fraction of normal speed kept after peg bounce
|
||||||
local FRICTION = 0.88 -- fraction of tangential speed kept after bounce
|
local FRICTION = 0.88 -- fraction of tangential speed kept after peg bounce
|
||||||
local WALL_RESTITUTION = 0.35
|
local WALL_RESTITUTION = 0.45 -- side wall bounce
|
||||||
|
local FLOOR_RESTITUTION = 0.38 -- bucket-floor bounce (ball settles after a few hops)
|
||||||
|
local DIVIDER_RESTITUTION = 0.30 -- bucket divider bounce
|
||||||
local MAX_SPEED = 1200 -- px/s (clamp to avoid tunnelling)
|
local MAX_SPEED = 1200 -- px/s (clamp to avoid tunnelling)
|
||||||
|
local SETTLE_VY = 18 -- px/s below this vertical speed the ball is "at rest"
|
||||||
|
|
||||||
local PEG_RADIUS = 7 -- px
|
local PEG_RADIUS = 7 -- px
|
||||||
local BALL_RADIUS = 9 -- px
|
local BALL_RADIUS = 9 -- px
|
||||||
@@ -230,8 +234,8 @@ local function drawBoard()
|
|||||||
for _, p in ipairs(pegs) do drawPeg(p) end
|
for _, p in ipairs(pegs) do drawPeg(p) end
|
||||||
drawAllBuckets()
|
drawAllBuckets()
|
||||||
|
|
||||||
px_text_centre("PLINKO", 8, 0xFFD600, COL_BG, 3)
|
px_text_centre("Plichinko", 8, 0xFFD600, COL_BG, 3)
|
||||||
px_text_centre("Pull lever to drop", 38, 0x607080, COL_BG, 1)
|
px_text_centre("Pull lever to drop", 46, 0x607080, COL_BG, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
@@ -268,36 +272,42 @@ 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
|
-- Random drop X between the leftmost and rightmost peg in the first row.
|
||||||
-- (first row has PEG_COLS_TOP pegs; their positions were added first).
|
|
||||||
local firstRowPegs = PEG_COLS_TOP
|
|
||||||
local firstPeg = pegs[1]
|
local firstPeg = pegs[1]
|
||||||
local lastFirst = pegs[firstRowPegs]
|
local lastFirst = pegs[PEG_COLS_TOP]
|
||||||
local dropX = firstPeg.x + math.random() * (lastFirst.x - firstPeg.x)
|
local dropX = firstPeg.x + math.random() * (lastFirst.x - firstPeg.x)
|
||||||
local dropY = boardTop - rowSpacing * 0.6
|
local dropY = boardTop - rowSpacing * 0.6
|
||||||
|
|
||||||
local bx, by = dropX, dropY
|
local bx, by = dropX, dropY
|
||||||
local vx, vy = (math.random() - 0.5) * 40, 60 -- tiny initial nudge
|
local vx, vy = (math.random() - 0.5) * 40, 60
|
||||||
|
|
||||||
local lastBx, lastBy = bx, by
|
local lastBx, lastBy = bx, by
|
||||||
local elapsed = 0
|
local elapsed = 0
|
||||||
local MAX_TIME = 12.0
|
local MAX_TIME = 14.0
|
||||||
|
|
||||||
-- Per-peg cooldown: after hitting a peg, ignore it for a short time
|
-- Per-peg cooldown to prevent vibrating stuck against one peg.
|
||||||
-- to prevent the ball getting stuck vibrating against one peg.
|
local pegCooldown = {}
|
||||||
local pegCooldown = {} -- peg index -> time remaining
|
|
||||||
|
-- Pre-build bucket divider X positions (walls between buckets).
|
||||||
|
-- Each divider is a thin vertical wall from bucketTop downward.
|
||||||
|
local dividers = {}
|
||||||
|
for i = 1, NUM_BUCKETS - 1 do
|
||||||
|
table.insert(dividers, buckets[i].x + buckets[i].w)
|
||||||
|
end
|
||||||
|
local bucketFloor = bucketTop + BUCKET_H
|
||||||
|
|
||||||
|
local inBucket = false -- true once ball has entered bucket zone
|
||||||
|
|
||||||
-- Draw initial ball
|
-- Draw initial ball
|
||||||
drawBall(bx, by)
|
drawBall(bx, by)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
|
|
||||||
while elapsed < MAX_TIME do
|
while elapsed < MAX_TIME do
|
||||||
-- Step
|
|
||||||
vy = vy + GRAVITY * DT
|
vy = vy + GRAVITY * DT
|
||||||
bx = bx + vx * DT
|
bx = bx + vx * DT
|
||||||
by = by + vy * DT
|
by = by + vy * DT
|
||||||
|
|
||||||
-- Clamp speed
|
-- Speed clamp
|
||||||
local spd = math.sqrt(vx*vx + vy*vy)
|
local spd = math.sqrt(vx*vx + vy*vy)
|
||||||
if spd > MAX_SPEED then
|
if spd > MAX_SPEED then
|
||||||
local s = MAX_SPEED / spd
|
local s = MAX_SPEED / spd
|
||||||
@@ -310,7 +320,8 @@ local function physicsLoop()
|
|||||||
if pegCooldown[k] <= 0 then pegCooldown[k] = nil end
|
if pegCooldown[k] <= 0 then pegCooldown[k] = nil end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Peg collisions (circle vs circle)
|
-- ── Peg collisions (only while above bucket zone) ───────────
|
||||||
|
if not inBucket then
|
||||||
for i, p in ipairs(pegs) do
|
for i, p in ipairs(pegs) do
|
||||||
if not pegCooldown[i] then
|
if not pegCooldown[i] then
|
||||||
local dx = bx - p.x
|
local dx = bx - p.x
|
||||||
@@ -318,43 +329,36 @@ local function physicsLoop()
|
|||||||
local distSq = dx*dx + dy*dy
|
local distSq = dx*dx + dy*dy
|
||||||
if distSq < COMBINED_R * COMBINED_R and distSq > 0 then
|
if distSq < COMBINED_R * COMBINED_R and distSq > 0 then
|
||||||
local dist = math.sqrt(distSq)
|
local dist = math.sqrt(distSq)
|
||||||
-- Contact normal (from peg centre to ball centre)
|
|
||||||
local nx = dx / dist
|
local nx = dx / dist
|
||||||
local ny = dy / dist
|
local ny = dy / dist
|
||||||
|
|
||||||
-- Push ball out of overlap
|
-- Depenetrate
|
||||||
local overlap = COMBINED_R - dist
|
local overlap = COMBINED_R - dist
|
||||||
bx = bx + nx * overlap
|
bx = bx + nx * overlap
|
||||||
by = by + ny * overlap
|
by = by + ny * overlap
|
||||||
|
|
||||||
-- Decompose velocity into normal and tangential components
|
-- Velocity reflection
|
||||||
local vn = vx * nx + vy * ny -- normal component (scalar)
|
local vn = vx * nx + vy * ny
|
||||||
local vnx = vn * nx
|
local vnx = vn * nx; local vny = vn * ny
|
||||||
local vny = vn * ny
|
local vtx = vx - vnx; local vty = vy - vny
|
||||||
local vtx = vx - vnx
|
|
||||||
local vty = vy - vny
|
|
||||||
|
|
||||||
-- Reflect normal component, damp tangential (friction)
|
|
||||||
vx = -vnx * RESTITUTION + vtx * FRICTION
|
vx = -vnx * RESTITUTION + vtx * FRICTION
|
||||||
vy = -vny * RESTITUTION + vty * FRICTION
|
vy = -vny * RESTITUTION + vty * FRICTION
|
||||||
|
|
||||||
-- Ensure ball moves away from peg
|
-- Guarantee separation
|
||||||
local newVn = vx * nx + vy * ny
|
local newVn = vx * nx + vy * ny
|
||||||
if newVn < 0 then
|
if newVn < 0 then
|
||||||
vx = vx - newVn * nx
|
vx = vx - newVn * nx
|
||||||
vy = vy - newVn * ny
|
vy = vy - newVn * ny
|
||||||
end
|
end
|
||||||
|
|
||||||
pegCooldown[i] = 0.08 -- ignore this peg for 80 ms
|
pegCooldown[i] = 0.08
|
||||||
|
|
||||||
-- Flash peg
|
|
||||||
drawPeg(p, COL_PEG_HIT)
|
drawPeg(p, COL_PEG_HIT)
|
||||||
-- (will be restored on next erase)
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Left/right wall collisions
|
-- ── Side wall collisions ─────────────────────────────────────
|
||||||
if bx - BALL_RADIUS < boardLeft then
|
if bx - BALL_RADIUS < boardLeft then
|
||||||
bx = boardLeft + BALL_RADIUS
|
bx = boardLeft + BALL_RADIUS
|
||||||
vx = math.abs(vx) * WALL_RESTITUTION
|
vx = math.abs(vx) * WALL_RESTITUTION
|
||||||
@@ -364,11 +368,42 @@ local function physicsLoop()
|
|||||||
vx = -math.abs(vx) * WALL_RESTITUTION
|
vx = -math.abs(vx) * WALL_RESTITUTION
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Reached bucket level?
|
-- ── Entered bucket zone? ─────────────────────────────────────
|
||||||
if by + BALL_RADIUS >= bucketTop then
|
if by + BALL_RADIUS >= bucketTop then
|
||||||
by = bucketTop - BALL_RADIUS
|
inBucket = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Bucket physics (dividers + floor) ────────────────────────
|
||||||
|
if inBucket then
|
||||||
|
-- Bucket divider walls (thin vertical pillars)
|
||||||
|
for _, dx in ipairs(dividers) do
|
||||||
|
if bx + BALL_RADIUS > dx and bx - BALL_RADIUS < dx then
|
||||||
|
if vx > 0 then
|
||||||
|
bx = dx - BALL_RADIUS
|
||||||
|
else
|
||||||
|
bx = dx + BALL_RADIUS
|
||||||
|
end
|
||||||
|
vx = -vx * DIVIDER_RESTITUTION
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Bucket floor bounce
|
||||||
|
if by + BALL_RADIUS >= bucketFloor then
|
||||||
|
by = bucketFloor - BALL_RADIUS
|
||||||
|
vy = -math.abs(vy) * FLOOR_RESTITUTION
|
||||||
|
vx = vx * 0.80 -- extra horizontal damping on floor hit
|
||||||
|
|
||||||
|
-- Settle: if vertical bounce is tiny, stop
|
||||||
|
if math.abs(vy) < SETTLE_VY then
|
||||||
|
vy = 0
|
||||||
|
vx = vx * 0.5
|
||||||
|
if math.abs(vx) < 4 then
|
||||||
|
vx = 0
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Render
|
-- Render
|
||||||
eraseBall(math.floor(lastBx), math.floor(lastBy))
|
eraseBall(math.floor(lastBx), math.floor(lastBy))
|
||||||
@@ -380,7 +415,7 @@ local function physicsLoop()
|
|||||||
elapsed = elapsed + DT
|
elapsed = elapsed + DT
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Final render at rest
|
-- Final render
|
||||||
eraseBall(math.floor(lastBx), math.floor(lastBy))
|
eraseBall(math.floor(lastBx), math.floor(lastBy))
|
||||||
drawBall(bx, by)
|
drawBall(bx, by)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
@@ -452,20 +487,20 @@ local function start()
|
|||||||
math.randomseed(os.epoch("utc"))
|
math.randomseed(os.epoch("utc"))
|
||||||
|
|
||||||
gpu = findGPU()
|
gpu = findGPU()
|
||||||
if not gpu then error("[plinko] No GPU peripheral found.") end
|
if not gpu then error("[plichinko] No GPU peripheral found.") end
|
||||||
|
|
||||||
gpu.refreshSize()
|
gpu.refreshSize()
|
||||||
sleep(0)
|
sleep(0)
|
||||||
gpu.setSize(64)
|
gpu.setSize(64)
|
||||||
|
|
||||||
PW, PH = gpu.getSize()
|
PW, PH = gpu.getSize()
|
||||||
print(("[plinko] GPU %dx%d px"):format(PW, PH))
|
print(("[plichinko] GPU %dx%d px"):format(PW, PH))
|
||||||
if not PW or PW < 128 or PH < 128 then
|
if not PW or PW < 128 or PH < 128 then
|
||||||
error(("GPU size %dx%d too small."):format(PW or 0, PH or 0))
|
error(("GPU size %dx%d too small."):format(PW or 0, PH or 0))
|
||||||
end
|
end
|
||||||
|
|
||||||
buildBoard()
|
buildBoard()
|
||||||
print(("[plinko] %d pegs, %d buckets"):format(#pegs, NUM_BUCKETS))
|
print(("[plichinko] %d pegs, %d buckets"):format(#pegs, NUM_BUCKETS))
|
||||||
|
|
||||||
drawBoard()
|
drawBoard()
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
@@ -480,12 +515,12 @@ local function main()
|
|||||||
waitForRedstonePulse()
|
waitForRedstonePulse()
|
||||||
|
|
||||||
-- Erase subtitle, show "dropping"
|
-- Erase subtitle, show "dropping"
|
||||||
px_text_centre("Pull lever to drop", 38, COL_BG, COL_BG, 1)
|
px_text_centre("Pull lever to drop", 46, COL_BG, COL_BG, 1)
|
||||||
px_text_centre(" DROPPING... ", 38, 0xFFD600, COL_BG, 1)
|
px_text_centre(" DROPPING... ", 46, 0xFFD600, COL_BG, 1)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
sleep(0.25)
|
sleep(0.25)
|
||||||
px_text_centre(" DROPPING... ", 38, COL_BG, COL_BG, 1)
|
px_text_centre(" DROPPING... ", 46, COL_BG, COL_BG, 1)
|
||||||
px_text_centre("Pull lever to drop", 38, 0x607080, COL_BG, 1)
|
px_text_centre("Pull lever to drop", 46, 0x607080, COL_BG, 1)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
|
|
||||||
-- Run physics — outcome determined by simulation
|
-- Run physics — outcome determined by simulation
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
-- Roulette Machine — circular wheel, top-down view
|
-- Roulette Machine — circular wheel, top-down view
|
||||||
-- Tom's Peripherals GPU + 512×512 screen wall.
|
-- Tom's Peripherals GPU + screen wall (any size).
|
||||||
--
|
--
|
||||||
-- Wheel layout (polar, centred on screen):
|
-- Authentic European pocket order (37 pockets, 0–36).
|
||||||
-- R_OUTER : outer rim / ball orbit track
|
|
||||||
-- R_POCKET : outer edge of wedge ring
|
|
||||||
-- R_INNER : inner edge of wedge ring (border of centre hub)
|
|
||||||
-- Centre hub : dark disc with "ROULETTE" label
|
|
||||||
--
|
--
|
||||||
-- Authentic European pocket order (37 pockets, 0–36):
|
-- Physics:
|
||||||
-- 0, 32, 15, 19, 4, 21, 2, 25, 17, 34, 6, 27, 13, 36,
|
-- Rotor : spins CW, decelerates under friction (heavy wheel, slow).
|
||||||
-- 11, 30, 8, 23, 10, 5, 24, 16, 33, 1, 20, 14, 31, 9,
|
-- Ball : orbits outer track CCW at higher speed, decelerates faster.
|
||||||
-- 22, 18, 29, 7, 28, 12, 35, 3, 26
|
-- 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.
|
||||||
--
|
--
|
||||||
-- Animation:
|
-- Rendering strategy:
|
||||||
-- Rotor : wedges spin at ROTOR_SPEED_* rad/s, slowing with friction
|
-- The full wheel (37 wedges) is expensive to rasterise, so it is only
|
||||||
-- Ball : orbits outer track (opposite dir) at BALL_SPEED_*, also decelerates,
|
-- redrawn when the rotor has rotated more than ROTOR_REDRAW_THRESH rad
|
||||||
-- then spirals inward toward the pocket ring radius when slow enough
|
-- since the last draw. Between redraws only the ball is erased/repainted
|
||||||
-- Result : determined by relative angle of ball vs rotor when ball settles
|
-- over the static background — keeping the frame rate smooth.
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- GPU discovery
|
-- GPU discovery
|
||||||
@@ -39,10 +39,14 @@ end
|
|||||||
-- Constants
|
-- Constants
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
local FRAME_DELAY = 0.02 -- ~50 fps
|
local FRAME_DELAY = 0.05 -- ~20 fps (keeps CC happy)
|
||||||
local TWO_PI = math.pi * 2
|
local TWO_PI = math.pi * 2
|
||||||
|
|
||||||
-- Authentic European wheel order
|
-- 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,
|
||||||
@@ -50,33 +54,54 @@ local WHEEL_ORDER = {
|
|||||||
}
|
}
|
||||||
local NUM_POCKETS = #WHEEL_ORDER -- 37
|
local NUM_POCKETS = #WHEEL_ORDER -- 37
|
||||||
|
|
||||||
-- Red numbers (standard European set)
|
|
||||||
local RED_SET = {}
|
local RED_SET = {}
|
||||||
for _, n in ipairs({1,3,5,7,9,12,14,16,18,19,21,23,25,27,30,32,34,36}) do
|
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 (set in start(), depends on PW/PH)
|
-- Geometry (computed in start())
|
||||||
local CX, CY -- wheel centre px
|
local CX, CY
|
||||||
local R_OUTER -- outer rim radius (ball track)
|
local R_OUTER, R_POCKET_OUT, R_POCKET_IN, R_HUB
|
||||||
local R_POCKET_OUT -- outer edge of pocket wedges
|
|
||||||
local R_POCKET_IN -- inner edge of pocket wedges
|
|
||||||
local R_HUB -- centre hub radius
|
|
||||||
|
|
||||||
-- Colours
|
-- Colours
|
||||||
local COL_BG = 0x050505
|
local COL_BG = 0x050505
|
||||||
local COL_RIM = 0x8B6914 -- brass/gold outer rim
|
local COL_RIM = 0x8B6914
|
||||||
local COL_TRACK = 0x1A1A1A -- ball track channel
|
local COL_TRACK = 0x1A1A1A
|
||||||
local COL_RED = 0xC62828
|
local COL_RED = 0xC62828
|
||||||
local COL_BLACK = 0x1C1C1C
|
local COL_BLACK = 0x1C1C1C
|
||||||
local COL_GREEN = 0x1B5E20
|
local COL_GREEN = 0x1B5E20
|
||||||
local COL_SEPARATOR = 0xB8860B -- gold dividers between wedges
|
local COL_SEP = 0xB8860B
|
||||||
local COL_HUB = 0x2C2C2C
|
local COL_HUB = 0x2C2C2C
|
||||||
local COL_HUB_RING = 0x8B6914
|
local COL_HUB_RING = 0x8B6914
|
||||||
local COL_WHITE = 0xFFFFFF
|
local COL_WHITE = 0xFFFFFF
|
||||||
local COL_BALL = 0xF0F0F0
|
local COL_BALL = 0xF0F0F0
|
||||||
local COL_BALL_SHD = 0x444444
|
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
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- GPU / pixel primitives
|
-- GPU / pixel primitives
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
@@ -103,7 +128,6 @@ local function px_circle(cx, cy, r, col)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Filled annulus (ring) between r1 and r2 (r1 < r2)
|
|
||||||
local function px_annulus(cx, cy, r1, r2, col)
|
local function px_annulus(cx, cy, r1, r2, col)
|
||||||
cx = math.floor(cx); cy = math.floor(cy)
|
cx = math.floor(cx); cy = math.floor(cy)
|
||||||
r1 = math.floor(r1); r2 = math.floor(r2)
|
r1 = math.floor(r1); r2 = math.floor(r2)
|
||||||
@@ -119,16 +143,11 @@ local function px_annulus(cx, cy, r1, r2, col)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Draw a radial line from r1 to r2 at angle a (radians, 0=right, CW)
|
|
||||||
local function px_spoke(cx, cy, r1, r2, angle, col)
|
local function px_spoke(cx, cy, r1, r2, angle, col)
|
||||||
local cos_a = math.cos(angle)
|
local ca, sa = math.cos(angle), math.sin(angle)
|
||||||
local sin_a = math.sin(angle)
|
for i = 0, r2 - r1 do
|
||||||
local steps = r2 - r1
|
|
||||||
for i = 0, steps do
|
|
||||||
local r = r1 + i
|
local r = r1 + i
|
||||||
local x = math.floor(cx + cos_a * r + 0.5)
|
px_rect(math.floor(cx + ca*r + 0.5), math.floor(cy + sa*r + 0.5), 1, 1, col)
|
||||||
local y = math.floor(cy + sin_a * r + 0.5)
|
|
||||||
px_rect(x, y, 1, 1, col)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -137,10 +156,7 @@ local function px_text(str, x, y, fg, bg, size)
|
|||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Wedge drawing
|
-- Wedge rasteriser (used at startup and for glow flashes only)
|
||||||
-- Each wedge is a pie-slice from R_POCKET_IN to R_POCKET_OUT.
|
|
||||||
-- We rasterise it by scanning every pixel in the bounding box and
|
|
||||||
-- testing polar coords — fast enough for 37 wedges at init time.
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
local function pocketColor(num)
|
local function pocketColor(num)
|
||||||
@@ -149,52 +165,36 @@ local function pocketColor(num)
|
|||||||
return COL_BLACK
|
return COL_BLACK
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Draw one wedge. rotorAngle shifts the whole rotor.
|
|
||||||
local function drawWedge(slotIdx, rotorAngle, glowing)
|
local function drawWedge(slotIdx, rotorAngle, glowing)
|
||||||
local n = NUM_POCKETS
|
local halfArc = math.pi / NUM_POCKETS
|
||||||
local halfArc = math.pi / n -- half-angle of one wedge
|
local midAngle = rotorAngle + (slotIdx - 1) * TWO_PI / NUM_POCKETS
|
||||||
-- centre angle for this slot
|
|
||||||
local midAngle = rotorAngle + (slotIdx - 1) * TWO_PI / n
|
|
||||||
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
|
||||||
-- brighten the colour slightly
|
local r = math.min(255, math.floor(col / 0x10000) + 60)
|
||||||
local r = math.floor(col / 0x10000)
|
local g = math.min(255, math.floor((col % 0x10000) / 0x100) + 60)
|
||||||
local g = math.floor((col % 0x10000) / 0x100)
|
local b = math.min(255, col % 0x100 + 60)
|
||||||
local b = col % 0x100
|
|
||||||
r = math.min(255, r + 60)
|
|
||||||
g = math.min(255, g + 60)
|
|
||||||
b = math.min(255, b + 60)
|
|
||||||
col = r * 0x10000 + g * 0x100 + b
|
col = r * 0x10000 + g * 0x100 + b
|
||||||
end
|
end
|
||||||
|
|
||||||
local ri = R_POCKET_IN
|
local ri, ro = R_POCKET_IN, R_POCKET_OUT
|
||||||
local ro = R_POCKET_OUT
|
|
||||||
|
|
||||||
-- Bounding box
|
|
||||||
local bx0 = math.floor(CX - ro) - 1
|
local bx0 = math.floor(CX - ro) - 1
|
||||||
local bx1 = math.ceil (CX + ro) + 1
|
local bx1 = math.ceil (CX + ro) + 1
|
||||||
local by0 = math.floor(CY - ro) - 1
|
local by0 = math.floor(CY - ro) - 1
|
||||||
local by1 = math.ceil (CY + ro) + 1
|
local by1 = math.ceil (CY + ro) + 1
|
||||||
|
local arc = (a1 - a0) % TWO_PI
|
||||||
|
|
||||||
-- Scan row by row, yield every pixel row to stay within CC tick budget.
|
|
||||||
for sy = by0, by1 do
|
for sy = by0, by1 do
|
||||||
sleep(0)
|
|
||||||
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 ang = math.atan2(dy, dx)
|
local rel = (math.atan2(dy, dx) - a0) % TWO_PI
|
||||||
-- normalise ang into [a0, a0+2pi) space
|
|
||||||
local rel = ang - a0
|
|
||||||
-- bring rel into [0, 2pi)
|
|
||||||
rel = rel % TWO_PI
|
|
||||||
local arc = (a1 - a0) % TWO_PI
|
|
||||||
local inWedge = rel <= arc
|
local inWedge = rel <= arc
|
||||||
|
|
||||||
if inRing and inWedge then
|
if inRing and inWedge then
|
||||||
@@ -211,220 +211,234 @@ local function drawWedge(slotIdx, rotorAngle, glowing)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Gold separator spoke at a0 edge
|
px_spoke(CX, CY, ri, ro, a0, COL_SEP)
|
||||||
px_spoke(CX, CY, ri, ro, a0, COL_SEPARATOR)
|
|
||||||
|
|
||||||
-- Number label: placed at mid-radius, mid-angle
|
local labelR = (ri + ro) / 2
|
||||||
local labelR = (ri + ro) / 2 + (ro - ri) * 0.05
|
|
||||||
local lx = CX + math.cos(midAngle) * labelR
|
local lx = CX + math.cos(midAngle) * labelR
|
||||||
local ly = CY + math.sin(midAngle) * labelR
|
local ly = CY + math.sin(midAngle) * labelR
|
||||||
local label = tostring(num)
|
local label = tostring(num)
|
||||||
local textFg = (num == 0) and COL_WHITE or
|
px_text(label, lx - (#label * 4), ly - 4, COL_WHITE, col, 1)
|
||||||
(RED_SET[num] and COL_WHITE or COL_WHITE)
|
|
||||||
-- size 1 labels (6px) — small enough to fit inside wedge
|
|
||||||
px_text(label, lx - (#label * 4), ly - 4, textFg, col, 1)
|
|
||||||
end
|
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(rotorAngle, glowSlot)
|
||||||
for i = 1, NUM_POCKETS do
|
for i = 1, NUM_POCKETS do
|
||||||
drawWedge(i, rotorAngle, i == glowSlot)
|
drawWedge(i, rotorAngle, i == glowSlot)
|
||||||
-- yield between wedges so CC doesn't kill us during init
|
sleep(0) -- yield once per wedge (37 yields, not thousands)
|
||||||
sleep(0)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
local function drawChrome()
|
||||||
-- Static wheel parts: rim, track, hub
|
-- outer gold rim
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
local function drawRim()
|
|
||||||
-- Outer decorative rim (gold ring)
|
|
||||||
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 (dark)
|
-- 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 rim border
|
-- 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)
|
||||||
-- Inner pocket border
|
|
||||||
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)
|
||||||
end
|
-- hub
|
||||||
|
|
||||||
local function drawHub()
|
|
||||||
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)
|
||||||
-- Centre dot
|
|
||||||
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 drawWheelStatic(rotorAngle, glowSlot)
|
local function drawWheelFull(rotorAngle, glowSlot)
|
||||||
-- Background disc
|
|
||||||
px_circle(CX, CY, R_OUTER, COL_BG)
|
px_circle(CX, CY, R_OUTER, COL_BG)
|
||||||
drawAllWedges(rotorAngle, glowSlot)
|
drawAllWedges(rotorAngle, glowSlot)
|
||||||
drawRim()
|
drawChrome()
|
||||||
drawHub()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Ball
|
-- Ball helpers
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
local ballX, ballY = 0, 0
|
local ballX, ballY = 0, 0
|
||||||
local BALL_RADIUS = 8 -- px
|
|
||||||
|
|
||||||
local function eraseBallAt(x, y, r, bgCol)
|
local function bgColorAt(r)
|
||||||
-- redraw the wheel region under the ball rather than fill a square
|
-- What colour is behind the ball at radius r?
|
||||||
-- We just overdraw a circle with COL_TRACK (ball is always in track or wedge area)
|
if r > R_POCKET_OUT + 2 then return COL_TRACK end
|
||||||
px_circle(math.floor(x), math.floor(y), r + 2, bgCol or COL_TRACK)
|
if r > R_POCKET_IN - 2 then return COL_BLACK end -- approximate — wedge redraws handle exact colour
|
||||||
|
return COL_HUB
|
||||||
end
|
end
|
||||||
|
|
||||||
local function drawBallAt(x, y)
|
local function eraseBall(bx, by, r)
|
||||||
ballX = math.floor(x)
|
-- Repaint the annulus region the ball touched.
|
||||||
ballY = math.floor(y)
|
-- 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
|
||||||
|
|
||||||
|
local function drawBall(bx, by)
|
||||||
|
ballX = math.floor(bx)
|
||||||
|
ballY = math.floor(by)
|
||||||
px_circle(ballX + 2, ballY + 2, BALL_RADIUS, COL_BALL_SHD)
|
px_circle(ballX + 2, ballY + 2, BALL_RADIUS, COL_BALL_SHD)
|
||||||
px_circle(ballX, ballY, BALL_RADIUS, COL_BALL)
|
px_circle(ballX, ballY, BALL_RADIUS, COL_BALL)
|
||||||
-- glint
|
|
||||||
px_circle(ballX - 2, ballY - 2, 2, COL_WHITE)
|
px_circle(ballX - 2, ballY - 2, 2, COL_WHITE)
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
-- Pocket geometry helpers
|
|
||||||
----------------------------------------------------------------------
|
|
||||||
|
|
||||||
-- Angle of slot i's centre with rotor at rotorAngle
|
|
||||||
local function slotAngle(i, rotorAngle)
|
|
||||||
return rotorAngle + (i - 1) * TWO_PI / NUM_POCKETS
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Ball position on a given orbit radius at angle a
|
|
||||||
local function ballPosAt(radius, angle)
|
local function ballPosAt(radius, angle)
|
||||||
return CX + math.cos(angle) * radius,
|
return CX + math.cos(angle) * radius,
|
||||||
CY + math.sin(angle) * radius
|
CY + math.sin(angle) * radius
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Spin animation
|
-- Center text overlay
|
||||||
-- - Rotor and ball both spin; rotor CW, ball CCW (standard physics)
|
|
||||||
-- - Ball decelerates faster than rotor
|
|
||||||
-- - When ball slows to SPIRAL_SPEED it drifts from R_ORBIT to R_SETTLE
|
|
||||||
-- (pocket mid-radius) over SPIRAL_TIME seconds
|
|
||||||
-- - Landing pocket = slot whose centre angle is closest to ball angle
|
|
||||||
-- at settle time (in rotor-relative coordinates)
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function drawCenterText(lines, textSize)
|
||||||
|
textSize = textSize or 2
|
||||||
|
local r = R_HUB - 8
|
||||||
|
px_circle(CX, CY, r, COL_HUB)
|
||||||
|
local lineH = 13 * textSize
|
||||||
|
local totalH = #lines * lineH
|
||||||
|
local startY = CY - math.floor(totalH / 2)
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
local lx = CX - math.floor(#line * 6 * textSize / 2)
|
||||||
|
px_text(line, lx, startY + (i-1) * lineH, COL_WHITE, COL_HUB, textSize)
|
||||||
|
end
|
||||||
|
px_annulus(CX, CY, R_HUB - 4, R_HUB, COL_HUB_RING)
|
||||||
|
gpu.sync()
|
||||||
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Spin — fully physics-based
|
-- Physics spin
|
||||||
--
|
--
|
||||||
-- Phases:
|
-- Phases:
|
||||||
-- 1. TRACK : ball rolls on outer track, decelerating under friction.
|
-- TRACK : ball on outer track, both rotor+ball decelerating.
|
||||||
-- Rotor also decelerates (much slower — heavy wheel).
|
-- DROP : ball's centripetal support gone; gains inward radial velocity
|
||||||
-- 2. DROP : when ball tangential speed drops below DROP_SPEED, centripetal
|
-- + small random deflector-pin kick.
|
||||||
-- force is insufficient; ball gains inward radial velocity.
|
-- POCKET : ball in pocket ring, decelerates to rest.
|
||||||
-- A small random angular deflection simulates diamond/pin bounce.
|
--
|
||||||
-- 3. POCKET : ball is now at pocket-ring radius, still has angular momentum;
|
-- The wheel is only fully redrawn when the rotor has moved
|
||||||
-- decelerates under higher friction until stopped.
|
-- ROTOR_REDRAW_THRESH radians. Between redraws only the ball moves.
|
||||||
-- Result = nearest pocket by angle relative to rotor.
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
local ROTOR_SPEED_MIN = 1.2 -- rad/s rotor initial speed (CW, positive)
|
local rotorAngle = 0
|
||||||
local ROTOR_SPEED_MAX = 2.0
|
|
||||||
local ROTOR_FRICTION = 0.08 -- rad/s² rotor deceleration (heavy wheel, slow)
|
|
||||||
|
|
||||||
local BALL_SPEED_MIN = 7.0 -- rad/s ball initial speed (CCW, negative)
|
|
||||||
local BALL_SPEED_MAX = 11.0
|
|
||||||
local TRACK_FRICTION = 0.40 -- rad/s² ball deceleration on outer track
|
|
||||||
|
|
||||||
local DROP_SPEED = 1.8 -- rad/s ball speed at which it leaves the track
|
|
||||||
local DROP_VEL = 80.0 -- px/s inward radial speed when drop triggers
|
|
||||||
local DEFLECT_MAX = 0.18 -- rad max angular kick from deflector pin
|
|
||||||
|
|
||||||
local POCKET_FRICTION = 1.2 -- rad/s² ball deceleration once in pocket ring
|
|
||||||
|
|
||||||
local rotorAngle = 0 -- persists between spins
|
|
||||||
local rotorSpeed = 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
|
||||||
|
|
||||||
-- Initial conditions (only speeds are random — outcome determined by physics)
|
|
||||||
rotorSpeed = ROTOR_SPEED_MIN + math.random() * (ROTOR_SPEED_MAX - ROTOR_SPEED_MIN)
|
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 ballSpeed = -(BALL_SPEED_MIN + math.random() * (BALL_SPEED_MAX - BALL_SPEED_MIN))
|
||||||
local ballAngle = math.random() * TWO_PI
|
local ballAngle = math.random() * TWO_PI
|
||||||
|
|
||||||
local R_TRACK_MID = (R_OUTER - 6 + R_POCKET_OUT + 2) / 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
|
local R_SETTLE = (R_POCKET_IN + R_POCKET_OUT) / 2
|
||||||
local ballR = R_TRACK_MID
|
|
||||||
local ballVr = 0 -- radial velocity (positive = inward)
|
|
||||||
|
|
||||||
local phase = "TRACK" -- "TRACK" | "DROP" | "POCKET"
|
-- 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 phase = "TRACK" -- "TRACK" | "POCKET"
|
||||||
|
|
||||||
|
-- Initial full draw
|
||||||
|
drawWheelFull(rotorAngle, nil)
|
||||||
|
lastDrawnRotor = rotorAngle
|
||||||
|
local bx0, by0 = ballPosAt(ballR, ballAngle)
|
||||||
|
drawBall(bx0, by0)
|
||||||
|
gpu.sync()
|
||||||
|
|
||||||
while true do
|
while true do
|
||||||
-- ── Rotor ─────────────────────────────────────────────────
|
-- ── Rotor ──────────────────────────────────────────────────
|
||||||
if rotorSpeed > 0 then
|
if rotorSpeed > 0 then
|
||||||
rotorSpeed = math.max(0, rotorSpeed - ROTOR_FRICTION * dt)
|
rotorSpeed = math.max(0, rotorSpeed - ROTOR_FRICTION * dt)
|
||||||
end
|
end
|
||||||
rotorAngle = (rotorAngle + rotorSpeed * dt) % TWO_PI
|
rotorAngle = (rotorAngle + rotorSpeed * dt) % TWO_PI
|
||||||
|
|
||||||
-- ── Ball angular motion ────────────────────────────────────
|
-- ── Ball angular motion ─────────────────────────────────────
|
||||||
local friction = (phase == "POCKET") and POCKET_FRICTION or TRACK_FRICTION
|
local angFriction = (phase == "POCKET") and POCKET_FRICTION or TRACK_FRICTION
|
||||||
if ballSpeed < 0 then
|
if ballSpeed < 0 then
|
||||||
ballSpeed = math.min(0, ballSpeed + friction * dt)
|
ballSpeed = math.min(0, ballSpeed + angFriction * dt)
|
||||||
else
|
else
|
||||||
ballSpeed = math.max(0, ballSpeed - friction * dt)
|
ballSpeed = math.max(0, ballSpeed - angFriction * dt)
|
||||||
end
|
end
|
||||||
ballAngle = (ballAngle + ballSpeed * dt) % TWO_PI
|
ballAngle = (ballAngle + ballSpeed * dt) % TWO_PI
|
||||||
|
|
||||||
-- ── Phase transitions ──────────────────────────────────────
|
-- ── Radial motion (bounce in track channel) ─────────────────
|
||||||
if phase == "TRACK" and math.abs(ballSpeed) <= DROP_SPEED then
|
if phase == "TRACK" then
|
||||||
phase = "DROP"
|
ballR = ballR + ballVr * dt
|
||||||
ballVr = DROP_VEL
|
|
||||||
-- Deflector pin: small random angular kick
|
-- Bounce off outer wall
|
||||||
local kick = (math.random() * 2 - 1) * DEFLECT_MAX
|
if ballR <= R_WALL_OUT - BALL_RADIUS then
|
||||||
ballAngle = (ballAngle + kick) % TWO_PI
|
ballR = R_WALL_OUT - BALL_RADIUS
|
||||||
ballSpeed = ballSpeed * 0.6 -- loses some speed on the pin
|
ballVr = math.abs(ballVr) * WALL_RESTITUTION
|
||||||
end
|
end
|
||||||
|
|
||||||
if phase == "DROP" then
|
-- 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
|
||||||
|
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
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- POCKET phase: slide inward to R_SETTLE, then stop.
|
||||||
ballR = ballR + ballVr * dt
|
ballR = ballR + ballVr * dt
|
||||||
-- Slow the inward rush as ball approaches pocket radius (damped)
|
ballVr = ballVr * (1 - 5 * dt)
|
||||||
ballVr = ballVr * (1 - 4 * dt)
|
|
||||||
if ballR >= R_SETTLE then
|
if ballR >= R_SETTLE then
|
||||||
ballR = R_SETTLE
|
ballR = R_SETTLE
|
||||||
ballVr = 0
|
ballVr = 0
|
||||||
phase = "POCKET"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ── Render ────────────────────────────────────────────────
|
-- ── Redraw wheel if rotor has moved enough ──────────────────
|
||||||
px_circle(ballX, ballY, BALL_RADIUS + 3, COL_TRACK)
|
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)
|
local bx, by = ballPosAt(ballR, ballAngle)
|
||||||
drawBallAt(bx, by)
|
drawBall(bx, by)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
sleep(dt)
|
sleep(dt)
|
||||||
|
|
||||||
-- ── Stop condition ─────────────────────────────────────────
|
-- ── Stop condition ──────────────────────────────────────────
|
||||||
if phase == "POCKET" and ballSpeed == 0 then break end
|
if phase == "POCKET" and ballSpeed == 0 and ballVr == 0 then break end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Winning pocket: closest slot centre angle to ball's final angle,
|
-- Determine winning pocket
|
||||||
-- measured in the rotor's frame of reference
|
|
||||||
local relAngle = (ballAngle - rotorAngle) % TWO_PI
|
local relAngle = (ballAngle - rotorAngle) % TWO_PI
|
||||||
local bestSlot = 1
|
local bestSlot, bestDist = 1, math.huge
|
||||||
local bestDist = 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)
|
local diff = math.abs(sa - relAngle)
|
||||||
if diff > math.pi then diff = TWO_PI - diff end
|
if diff > math.pi then diff = TWO_PI - diff end
|
||||||
if diff < bestDist then
|
if diff < bestDist then bestDist = diff; bestSlot = i end
|
||||||
bestDist = diff
|
|
||||||
bestSlot = i
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Snap ball to pocket centre (world angle)
|
-- Snap ball to pocket centre
|
||||||
local snapAngle = slotAngle(bestSlot, rotorAngle)
|
local snapAngle = rotorAngle + (bestSlot - 1) * TWO_PI / NUM_POCKETS
|
||||||
local sx, sy = ballPosAt(R_SETTLE, snapAngle)
|
local sx, sy = ballPosAt(R_SETTLE, snapAngle)
|
||||||
px_circle(ballX, ballY, BALL_RADIUS + 3, COL_TRACK)
|
eraseBall(ballX, ballY, BALL_RADIUS)
|
||||||
drawBallAt(sx, sy)
|
drawBall(sx, sy)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
|
|
||||||
return WHEEL_ORDER[bestSlot], bestSlot
|
return WHEEL_ORDER[bestSlot], bestSlot
|
||||||
@@ -436,40 +450,30 @@ 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 = slotAngle(slotIdx, rotorAngle)
|
local sa = rotorAngle + (slotIdx - 1) * TWO_PI / NUM_POCKETS
|
||||||
local bx, by = ballPosAt(R_SETTLE, sa)
|
local bx, by = ballPosAt(R_SETTLE, sa)
|
||||||
for flash = 1, 6 do
|
for flash = 1, 6 do
|
||||||
drawWedge(slotIdx, rotorAngle, flash % 2 == 1)
|
drawWedge(slotIdx, rotorAngle, flash % 2 == 1)
|
||||||
drawBallAt(bx, by)
|
drawBall(bx, by)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
sleep(0.18)
|
sleep(0.18)
|
||||||
end
|
end
|
||||||
-- Leave glowing
|
|
||||||
drawWedge(slotIdx, rotorAngle, true)
|
drawWedge(slotIdx, rotorAngle, true)
|
||||||
drawBallAt(bx, by)
|
drawBall(bx, by)
|
||||||
gpu.sync()
|
gpu.sync()
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
-- Center text overlay
|
-- Redstone helper
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
local function drawCenterText(lines, textSize)
|
local function waitForRedstonePulse()
|
||||||
textSize = textSize or 2
|
while true do
|
||||||
local r = R_HUB - 8
|
os.pullEvent("redstone")
|
||||||
px_circle(CX, CY, r, COL_HUB)
|
for _, side in ipairs(redstone.getSides()) do
|
||||||
local lineH = 9 * textSize
|
if redstone.getInput(side) then return end
|
||||||
local totalH = #lines * lineH
|
end
|
||||||
local startY = CY - math.floor(totalH / 2)
|
|
||||||
for i, line in ipairs(lines) do
|
|
||||||
local charW = 6 * textSize
|
|
||||||
local approxW = #line * charW
|
|
||||||
local lx = CX - math.floor(approxW / 2)
|
|
||||||
px_text(line, lx, startY + (i-1) * lineH, COL_WHITE, COL_HUB, textSize)
|
|
||||||
end
|
end
|
||||||
-- redraw hub ring on top
|
|
||||||
px_annulus(CX, CY, R_HUB - 4, R_HUB, COL_HUB_RING)
|
|
||||||
gpu.sync()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
@@ -492,7 +496,6 @@ local function start()
|
|||||||
error(("GPU pixel size %dx%d too small."):format(PW or 0, PH or 0))
|
error(("GPU pixel size %dx%d too small."):format(PW or 0, PH or 0))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Set geometry based on screen size
|
|
||||||
CX = math.floor(PW / 2)
|
CX = math.floor(PW / 2)
|
||||||
CY = math.floor(PH / 2)
|
CY = math.floor(PH / 2)
|
||||||
local R_MAX = math.floor(math.min(PW, PH) / 2) - 4
|
local R_MAX = math.floor(math.min(PW, PH) / 2) - 4
|
||||||
@@ -502,7 +505,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)
|
||||||
drawWheelStatic(rotorAngle, nil)
|
drawWheelFull(rotorAngle, nil)
|
||||||
drawCenterText({ "ROULETTE", "Pull lever" })
|
drawCenterText({ "ROULETTE", "Pull lever" })
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -510,20 +513,12 @@ local function stop()
|
|||||||
if gpu then gpu.fill(COL_BG); gpu.sync() end
|
if gpu then gpu.fill(COL_BG); gpu.sync() end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function waitForRedstonePulse()
|
|
||||||
while true do
|
|
||||||
os.pullEvent("redstone")
|
|
||||||
for _, side in ipairs(redstone.getSides()) do
|
|
||||||
if redstone.getInput(side) then return side end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function main()
|
local function main()
|
||||||
while true do
|
while true do
|
||||||
waitForRedstonePulse()
|
waitForRedstonePulse()
|
||||||
|
|
||||||
drawCenterText({ "SPINNING..." })
|
drawCenterText({ "SPINNING..." })
|
||||||
|
sleep(0.2)
|
||||||
|
|
||||||
local num, slotIdx = spin()
|
local num, slotIdx = spin()
|
||||||
|
|
||||||
@@ -537,7 +532,8 @@ local function main()
|
|||||||
|
|
||||||
sleep(5)
|
sleep(5)
|
||||||
|
|
||||||
drawWheelStatic(rotorAngle, nil)
|
drawWheelFull(rotorAngle, nil)
|
||||||
|
lastDrawnRotor = rotorAngle
|
||||||
drawCenterText({ "ROULETTE", "Pull lever" })
|
drawCenterText({ "ROULETTE", "Pull lever" })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user