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).
|
||||
--
|
||||
-- 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
|
||||
|
||||
Reference in New Issue
Block a user