This commit is contained in:
2026-05-05 19:36:25 -04:00
parent 20c5ebac1d
commit 987cb86ee6
2 changed files with 341 additions and 310 deletions

View File

@@ -1,11 +1,12 @@
-- Plinko Machine (fully physics-based)
-- Plichinko Machine (fully physics-based)
-- Tom's Peripherals GPU + screen wall (any size).
--
-- A ball is dropped from a random X position near the top.
-- It falls under gravity and collides with circular pegs using
-- proper elastic circle-circle collision response (reflect velocity
-- along the contact normal). The bucket it lands in is determined
-- entirely by the physics — no predetermined outcome.
-- along the contact normal). The ball also bounces off the side
-- 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.
@@ -33,10 +34,13 @@ end
local FRAME_DELAY = 0.02 -- s (~50 fps)
local DT = FRAME_DELAY
local GRAVITY = 700 -- px/s²
local RESTITUTION = 0.55 -- fraction of normal speed kept after bounce
local FRICTION = 0.88 -- fraction of tangential speed kept after bounce
local WALL_RESTITUTION = 0.35
local RESTITUTION = 0.55 -- fraction of normal speed kept after peg bounce
local FRICTION = 0.88 -- fraction of tangential speed kept after peg bounce
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 SETTLE_VY = 18 -- px/s below this vertical speed the ball is "at rest"
local PEG_RADIUS = 7 -- px
local BALL_RADIUS = 9 -- px
@@ -230,8 +234,8 @@ local function drawBoard()
for _, p in ipairs(pegs) do drawPeg(p) end
drawAllBuckets()
px_text_centre("PLINKO", 8, 0xFFD600, COL_BG, 3)
px_text_centre("Pull lever to drop", 38, 0x607080, COL_BG, 1)
px_text_centre("Plichinko", 8, 0xFFD600, COL_BG, 3)
px_text_centre("Pull lever to drop", 46, 0x607080, COL_BG, 1)
end
----------------------------------------------------------------------
@@ -268,36 +272,42 @@ local function drawBall(bx, by)
end
local function physicsLoop()
-- 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
-- Random drop X between the leftmost and rightmost peg in the first row.
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 dropY = boardTop - rowSpacing * 0.6
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 elapsed = 0
local MAX_TIME = 12.0
local elapsed = 0
local MAX_TIME = 14.0
-- Per-peg cooldown: after hitting a peg, ignore it for a short time
-- to prevent the ball getting stuck vibrating against one peg.
local pegCooldown = {} -- peg index -> time remaining
-- Per-peg cooldown to prevent vibrating stuck against one peg.
local pegCooldown = {}
-- 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
drawBall(bx, by)
gpu.sync()
while elapsed < MAX_TIME do
-- Step
vy = vy + GRAVITY * DT
bx = bx + vx * DT
by = by + vy * DT
-- Clamp speed
-- Speed clamp
local spd = math.sqrt(vx*vx + vy*vy)
if spd > MAX_SPEED then
local s = MAX_SPEED / spd
@@ -310,64 +320,89 @@ local function physicsLoop()
if pegCooldown[k] <= 0 then pegCooldown[k] = nil end
end
-- Peg collisions (circle vs circle)
for i, p in ipairs(pegs) do
if not pegCooldown[i] then
local dx = bx - p.x
local dy = by - p.y
local distSq = dx*dx + dy*dy
if distSq < COMBINED_R * COMBINED_R and distSq > 0 then
local dist = math.sqrt(distSq)
-- Contact normal (from peg centre to ball centre)
local nx = dx / dist
local ny = dy / dist
-- ── Peg collisions (only while above bucket zone) ───────────
if not inBucket then
for i, p in ipairs(pegs) do
if not pegCooldown[i] then
local dx = bx - p.x
local dy = by - p.y
local distSq = dx*dx + dy*dy
if distSq < COMBINED_R * COMBINED_R and distSq > 0 then
local dist = math.sqrt(distSq)
local nx = dx / dist
local ny = dy / dist
-- Push ball out of overlap
local overlap = COMBINED_R - dist
bx = bx + nx * overlap
by = by + ny * overlap
-- Depenetrate
local overlap = COMBINED_R - dist
bx = bx + nx * overlap
by = by + ny * overlap
-- Decompose velocity into normal and tangential components
local vn = vx * nx + vy * ny -- normal component (scalar)
local vnx = vn * nx
local vny = vn * ny
local vtx = vx - vnx
local vty = vy - vny
-- Velocity reflection
local vn = vx * nx + vy * ny
local vnx = vn * nx; local vny = vn * ny
local vtx = vx - vnx; local vty = vy - vny
vx = -vnx * RESTITUTION + vtx * FRICTION
vy = -vny * RESTITUTION + vty * FRICTION
-- Reflect normal component, damp tangential (friction)
vx = -vnx * RESTITUTION + vtx * FRICTION
vy = -vny * RESTITUTION + vty * FRICTION
-- Guarantee separation
local newVn = vx * nx + vy * ny
if newVn < 0 then
vx = vx - newVn * nx
vy = vy - newVn * ny
end
-- Ensure ball moves away from peg
local newVn = vx * nx + vy * ny
if newVn < 0 then
vx = vx - newVn * nx
vy = vy - newVn * ny
pegCooldown[i] = 0.08
drawPeg(p, COL_PEG_HIT)
end
pegCooldown[i] = 0.08 -- ignore this peg for 80 ms
-- Flash peg
drawPeg(p, COL_PEG_HIT)
-- (will be restored on next erase)
end
end
end
-- Left/right wall collisions
-- ── Side wall collisions ─────────────────────────────────────
if bx - BALL_RADIUS < boardLeft then
bx = boardLeft + BALL_RADIUS
vx = math.abs(vx) * WALL_RESTITUTION
vx = math.abs(vx) * WALL_RESTITUTION
end
if bx + BALL_RADIUS > boardRight then
bx = boardRight - BALL_RADIUS
vx = -math.abs(vx) * WALL_RESTITUTION
end
-- Reached bucket level?
-- ── Entered bucket zone? ─────────────────────────────────────
if by + BALL_RADIUS >= bucketTop then
by = bucketTop - BALL_RADIUS
break
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
end
end
end
end
-- Render
@@ -380,7 +415,7 @@ local function physicsLoop()
elapsed = elapsed + DT
end
-- Final render at rest
-- Final render
eraseBall(math.floor(lastBx), math.floor(lastBy))
drawBall(bx, by)
gpu.sync()
@@ -452,20 +487,20 @@ local function start()
math.randomseed(os.epoch("utc"))
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()
sleep(0)
gpu.setSize(64)
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
error(("GPU size %dx%d too small."):format(PW or 0, PH or 0))
end
buildBoard()
print(("[plinko] %d pegs, %d buckets"):format(#pegs, NUM_BUCKETS))
print(("[plichinko] %d pegs, %d buckets"):format(#pegs, NUM_BUCKETS))
drawBoard()
gpu.sync()
@@ -480,12 +515,12 @@ local function main()
waitForRedstonePulse()
-- Erase subtitle, show "dropping"
px_text_centre("Pull lever to drop", 38, COL_BG, COL_BG, 1)
px_text_centre(" DROPPING... ", 38, 0xFFD600, COL_BG, 1)
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... ", 38, COL_BG, COL_BG, 1)
px_text_centre("Pull lever to drop", 38, 0x607080, COL_BG, 1)
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