{"id":620,"date":"2026-01-19T14:30:14","date_gmt":"2026-01-19T20:30:14","guid":{"rendered":"http:\/\/pastcasts.com\/?page_id=620"},"modified":"2026-01-19T14:58:31","modified_gmt":"2026-01-19T20:58:31","slug":"ergot-games","status":"publish","type":"page","link":"http:\/\/pastcasts.com\/?page_id=620","title":{"rendered":"Ergot Games"},"content":{"rendered":"\n<!doctype html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\" \/>\n  <title>Ergot Sorter: Mini-Game<\/title>\n  <style>\n    :root { --pad: 14px; --radius: 16px; --shadow: 0 10px 28px rgba(0,0,0,.18); }\n    html, body { height: 100%; margin: 0; background: #111; color: #f3f3f3; font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; }\n    .wrap {\n      height: 100%;\n      display: grid;\n      grid-template-rows: auto 1fr auto;\n      gap: 10px;\n      padding: var(--pad);\n      box-sizing: border-box;\n      touch-action: none; \/* important for iPad drag *\/\n      user-select: none;\n      -webkit-user-select: none;\n    }\n\n    header {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      gap: 10px;\n      flex-wrap: wrap;\n    }\n    .title { font-weight: 700; font-size: 18px; letter-spacing: .2px; }\n    .hud {\n      display: flex;\n      gap: 10px;\n      flex-wrap: wrap;\n      align-items: center;\n      font-size: 14px;\n      opacity: .95;\n    }\n    .pill {\n      background: rgba(255,255,255,.08);\n      border: 1px solid rgba(255,255,255,.10);\n      border-radius: 999px;\n      padding: 8px 10px;\n      display: inline-flex;\n      gap: 8px;\n      align-items: baseline;\n    }\n    .pill b { font-size: 15px; }\n\n    .stage {\n      position: relative;\n      background: radial-gradient(1200px 800px at 50% 40%, rgba(255,255,255,.08), rgba(255,255,255,0) 65%),\n                  linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.02));\n      border: 1px solid rgba(255,255,255,.12);\n      border-radius: var(--radius);\n      box-shadow: var(--shadow);\n      overflow: hidden;\n    }\n\n    canvas { width: 100%; height: 100%; display: block; }\n\n    .overlay {\n      position: absolute;\n      inset: 0;\n      display: grid;\n      place-items: center;\n      padding: 20px;\n      background: rgba(0,0,0,.55);\n      backdrop-filter: blur(6px);\n      -webkit-backdrop-filter: blur(6px);\n    }\n    .card {\n      width: min(760px, 96%);\n      background: rgba(20,20,20,.9);\n      border: 1px solid rgba(255,255,255,.14);\n      border-radius: 18px;\n      padding: 18px;\n      box-shadow: var(--shadow);\n    }\n    .card h1 { margin: 0 0 8px 0; font-size: 22px; }\n    .card p { margin: 8px 0; line-height: 1.35; color: rgba(255,255,255,.9); }\n    .rules { display: grid; gap: 8px; margin-top: 10px; font-size: 15px; }\n    .rules span { opacity: .95; }\n    .btnrow { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }\n    button {\n      appearance: none;\n      border: 1px solid rgba(255,255,255,.18);\n      background: rgba(255,255,255,.08);\n      color: #fff;\n      border-radius: 12px;\n      padding: 12px 14px;\n      font-weight: 650;\n      font-size: 16px;\n      cursor: pointer;\n    }\n    button:active { transform: translateY(1px); }\n    .small { font-size: 13px; opacity: .9; }\n\n    footer {\n      display: grid;\n      grid-template-columns: 1fr;\n      gap: 10px;\n    }\n\n    .bins {\n      display: grid;\n      grid-template-columns: 1fr 1fr 1fr;\n      gap: 10px;\n    }\n    .bin {\n      border-radius: var(--radius);\n      border: 1px solid rgba(255,255,255,.12);\n      background: rgba(255,255,255,.06);\n      padding: 10px;\n      min-height: 78px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      text-align: center;\n      box-shadow: inset 0 0 0 1px rgba(0,0,0,.12);\n    }\n    .bin .label { font-weight: 800; letter-spacing: .5px; font-size: 16px; }\n    .bin .sub { display: block; margin-top: 4px; font-size: 12px; opacity: .85; }\n\n    .shake {\n      animation: shake 0.22s linear 0s 1;\n    }\n    @keyframes shake {\n      0% { transform: translate(0,0); }\n      25% { transform: translate(2px,-1px); }\n      50% { transform: translate(-2px,1px); }\n      75% { transform: translate(1px,2px); }\n      100% { transform: translate(0,0); }\n    }\n  <\/style>\n<\/head>\n<body>\n<div class=\"wrap\">\n  <header>\n    <div class=\"title\">Ergot Sorter \u2014 Mini-Game<\/div>\n    <div class=\"hud\">\n      <div class=\"pill\">Time <b id=\"timeLeft\">60<\/b>s<\/div>\n      <div class=\"pill\">Purity <b id=\"purity\">\u2014<\/b><\/div>\n      <div class=\"pill\">Recovery <b id=\"recovery\">\u2014<\/b><\/div>\n      <div class=\"pill\">Waste <b id=\"waste\">\u2014<\/b><\/div>\n    <\/div>\n  <\/header>\n\n  <div class=\"stage\" id=\"stage\">\n    <canvas id=\"c\"><\/canvas>\n\n    <div class=\"overlay\" id=\"startOverlay\">\n      <div class=\"card\">\n        <h1>Keep it clean. Save the ergot.<\/h1>\n        <p>You\u2019re sorting a mixed stream. Work fast, but don\u2019t contaminate the clean grain.<\/p>\n        <div class=\"rules\">\n          <span>&#x1f449; <b>Drag<\/b> dark pieces into <b>ERGOT<\/b><\/span>\n          <span>&#x1f449; <b>Flick<\/b> toward <b>CLEAN<\/b> to send grain quickly<\/span>\n          <span>&#x1f449; <b>Tap<\/b> a <b>questionable<\/b> piece to send it to <b>RE-RUN<\/b><\/span>\n          <span class=\"small\">Every ~12 seconds, the machine \u201cpulses\u201d and the tray shakes.<\/span>\n        <\/div>\n        <div class=\"btnrow\">\n          <button id=\"startBtn\">Start (60s)<\/button>\n          <button id=\"practiceBtn\">Practice (no timer)<\/button>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"overlay\" id=\"endOverlay\" style=\"display:none;\">\n      <div class=\"card\">\n        <h1>Round complete<\/h1>\n        <p id=\"endSummary\"><\/p>\n        <div class=\"btnrow\">\n          <button id=\"restartBtn\">Play Again<\/button>\n          <button id=\"backBtn\">Back to Start<\/button>\n        <\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <footer>\n    <div class=\"bins\" id=\"bins\">\n      <div class=\"bin\" id=\"binClean\">\n        <div>\n          <div class=\"label\">CLEAN<\/div>\n          <span class=\"sub\">Flick here<\/span>\n        <\/div>\n      <\/div>\n      <div class=\"bin\" id=\"binErgot\">\n        <div>\n          <div class=\"label\">ERGOT<\/div>\n          <span class=\"sub\">Drag dark pieces<\/span>\n        <\/div>\n      <\/div>\n      <div class=\"bin\" id=\"binRerun\">\n        <div>\n          <div class=\"label\">RE-RUN<\/div>\n          <span class=\"sub\">Tap questionable<\/span>\n        <\/div>\n      <\/div>\n    <\/div>\n  <\/footer>\n<\/div>\n\n<script>\n(() => {\n  \/\/ ---------- Utilities ----------\n  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));\n  const dist2 = (ax, ay, bx, by) => (ax-bx)**2 + (ay-by)**2;\n  const now = () => performance.now();\n\n  \/\/ ---------- DOM ----------\n  const stage = document.getElementById('stage');\n  const canvas = document.getElementById('c');\n  const ctx = canvas.getContext('2d');\n\n  const startOverlay = document.getElementById('startOverlay');\n  const endOverlay = document.getElementById('endOverlay');\n\n  const startBtn = document.getElementById('startBtn');\n  const practiceBtn = document.getElementById('practiceBtn');\n  const restartBtn = document.getElementById('restartBtn');\n  const backBtn = document.getElementById('backBtn');\n\n  const timeLeftEl = document.getElementById('timeLeft');\n  const purityEl = document.getElementById('purity');\n  const recoveryEl = document.getElementById('recovery');\n  const wasteEl = document.getElementById('waste');\n  const endSummary = document.getElementById('endSummary');\n\n  const binClean = document.getElementById('binClean');\n  const binErgot = document.getElementById('binErgot');\n  const binRerun = document.getElementById('binRerun');\n\n  \/\/ ---------- Layout \/ regions ----------\n  let W=0, H=0, dpr=1;\n  let tray = { x: 0, y: 0, w: 0, h: 0 };\n  let spawnLine = { x1: 0, x2: 0, y: 0 };\n  let bins = { clean: null, ergot: null, rerun: null };\n\n  function updateRects() {\n    const r = stage.getBoundingClientRect();\n    W = Math.floor(r.width);\n    H = Math.floor(r.height);\n    dpr = Math.max(1, Math.floor(window.devicePixelRatio || 1));\n\n    canvas.width = W * dpr;\n    canvas.height = H * dpr;\n    canvas.style.width = W + 'px';\n    canvas.style.height = H + 'px';\n    ctx.setTransform(dpr,0,0,dpr,0,0);\n\n    \/\/ Tray area is the upper ~72% of stage\n    const pad = 18;\n    tray.x = pad;\n    tray.y = pad;\n    tray.w = W - pad*2;\n    tray.h = Math.floor(H * 0.72) - pad;\n\n    \/\/ Spawn line near top of tray\n    spawnLine.y = tray.y + 42;\n    spawnLine.x1 = tray.x + 26;\n    spawnLine.x2 = tray.x + tray.w - 26;\n\n    \/\/ Map footer bins into stage coords\n    const stageRect = stage.getBoundingClientRect();\n    const rC = binClean.getBoundingClientRect();\n    const rE = binErgot.getBoundingClientRect();\n    const rR = binRerun.getBoundingClientRect();\n\n    bins.clean = { x: rC.left - stageRect.left, y: rC.top - stageRect.top, w: rC.width, h: rC.height };\n    bins.ergot = { x: rE.left - stageRect.left, y: rE.top - stageRect.top, w: rE.width, h: rE.height };\n    bins.rerun = { x: rR.left - stageRect.left, y: rR.top - stageRect.top, w: rR.width, h: rR.height };\n  }\n\n  window.addEventListener('resize', () => { updateRects(); });\n\n  \/\/ ---------- Game state ----------\n  const TYPE = { CLEAN: 'clean', ERGOT: 'ergot', Q: 'questionable' };\n  const pieces = [];\n  let running = false;\n  let practiceMode = false;\n\n  \/\/ Timer\n  let roundSeconds = 60;\n  let timeLeft = roundSeconds;\n  let lastTick = 0;\n\n  \/\/ Spawning\n  let spawnRatePerSec = 10;         \/\/ average pieces per second\n  let spawnAccumulator = 0;\n\n  \/\/ Pulse\n  let pulseEvery = 12_000;          \/\/ ms\n  let lastPulseAt = 0;\n\n  \/\/ Dragging\n  let activeId = null; \/\/ pointerId\n  let grabbed = null;  \/\/ piece ref\n  let grabOffset = { x: 0, y: 0 };\n  let moveTrack = [];  \/\/ for velocity (last points)\n\n  \/\/ Stats\n  let totalErgotSpawned = 0;\n  let sorted = {\n    cleanBin: { clean:0, ergot:0, q:0, total:0 },\n    ergotBin: { clean:0, ergot:0, q:0, total:0 },\n    rerunBin: { clean:0, ergot:0, q:0, total:0 },\n  };\n\n  function resetStats() {\n    totalErgotSpawned = 0;\n    sorted.cleanBin = { clean:0, ergot:0, q:0, total:0 };\n    sorted.ergotBin = { clean:0, ergot:0, q:0, total:0 };\n    sorted.rerunBin = { clean:0, ergot:0, q:0, total:0 };\n    updateHUD();\n  }\n\n  \/\/ ---------- Piece factory ----------\n  function rand(a,b){ return a + Math.random()*(b-a); }\n  function choiceWeighted() {\n    const r = Math.random();\n    if (r < 0.70) return TYPE.CLEAN;\n    if (r < 0.90) return TYPE.ERGOT;\n    return TYPE.Q;\n  }\n\n  function makePiece() {\n    const type = choiceWeighted();\n    const x = rand(spawnLine.x1, spawnLine.x2);\n    const y = spawnLine.y + rand(-6, 6);\n\n    \/\/ Shape sizes per type\n    let w, h, rot;\n    if (type === TYPE.ERGOT) {\n      \/\/ CHANGED: longer ergot so it reads immediately\n      w = rand(34, 54);\n      h = rand(8, 12);\n    }\n    else if (type === TYPE.Q) { w = rand(18, 26); h = rand(6, 9); }\n    else { w = rand(14, 20); h = rand(5, 7); }\n    rot = rand(-0.9, 0.9);\n\n    const p = {\n      id: Math.random().toString(16).slice(2),\n      type,\n      x, y,\n      vx: rand(-10, 10),\n      vy: rand(25, 65),\n      w, h, rot,\n      grabbed: false,\n      homeX: x, homeY: y,\n      bornAt: now()\n    };\n\n    \/\/ CHANGED: slightly heavier feel for ergot\n    if (type === TYPE.ERGOT) {\n      p.vx *= 0.6;\n      p.vy *= 0.9;\n    }\n\n    if (type === TYPE.ERGOT) totalErgotSpawned++;\n    return p;\n  }\n\n  \/\/ ---------- Scoring ----------\n  function updateHUD() {\n    const cb = sorted.cleanBin;\n    const purity = cb.total === 0 ? null : (1 - (cb.ergot + cb.q)\/cb.total) * 100;\n\n    const eb = sorted.ergotBin;\n    const recovery = totalErgotSpawned === 0 ? null : (eb.ergot \/ totalErgotSpawned) * 100;\n\n    const wasteCount = sorted.ergotBin.clean + sorted.cleanBin.ergot;\n    const processed = cb.total + eb.total + sorted.rerunBin.total;\n    const wastePct = processed === 0 ? null : (wasteCount \/ processed) * 100;\n\n    purityEl.textContent = purity === null ? '\u2014' : `${Math.round(purity)}%`;\n    recoveryEl.textContent = recovery === null ? '\u2014' : `${Math.round(recovery)}%`;\n    wasteEl.textContent = wastePct === null ? '\u2014' : `${Math.round(wastePct)}%`;\n  }\n\n  function tallyToBin(pieceType, binName) {\n    const map = { clean: 'cleanBin', ergot: 'ergotBin', rerun: 'rerunBin' };\n    const target = sorted[map[binName]];\n    if (pieceType === TYPE.CLEAN) target.clean++;\n    else if (pieceType === TYPE.ERGOT) target.ergot++;\n    else target.q++;\n    target.total++;\n    updateHUD();\n  }\n\n  \/\/ ---------- Bin detection ----------\n  function pointInRect(x,y,r){\n    return x>=r.x && x<=r.x+r.w &#038;&#038; y>=r.y && y<=r.y+r.h;\n  }\n\n  function whichBin(x,y){\n    if (pointInRect(x,y,bins.clean)) return 'clean';\n    if (pointInRect(x,y,bins.ergot)) return 'ergot';\n    if (pointInRect(x,y,bins.rerun)) return 'rerun';\n    return null;\n  }\n\n  function sendToBin(p, binName) {\n    const r = bins[binName];\n    const tx = r.x + r.w\/2;\n    const ty = r.y + r.h\/2;\n    p.vx = (tx - p.x) * 6;\n    p.vy = (ty - p.y) * 6;\n    p.flyTo = { x: tx, y: ty, binName, t0: now() };\n    p.grabbed = false;\n  }\n\n  \/\/ ---------- Game control ----------\n  function startRound({ practice=false } = {}) {\n    practiceMode = practice;\n    running = true;\n\n    pieces.length = 0;\n    resetStats();\n\n    spawnAccumulator = 0;\n    lastPulseAt = now();\n    lastTick = now();\n\n    timeLeft = roundSeconds;\n    timeLeftEl.textContent = practiceMode ? '\u221e' : String(timeLeft);\n\n    startOverlay.style.display = 'none';\n    endOverlay.style.display = 'none';\n\n    for (let i=0;i<24;i++) pieces.push(makePiece());\n  }\n\n  function endRound() {\n    running = false;\n    const cb = sorted.cleanBin;\n    const purity = cb.total === 0 ? 0 : (1 - (cb.ergot + cb.q)\/cb.total) * 100;\n    const recovery = totalErgotSpawned === 0 ? 0 : (sorted.ergotBin.ergot \/ totalErgotSpawned) * 100;\n    const wasteCount = sorted.ergotBin.clean + sorted.cleanBin.ergot;\n\n    let msg = `Purity: ${Math.round(purity)}%. Recovery: ${Math.round(recovery)}%. `;\n    msg += `Mistakes (waste\/contamination events): ${wasteCount}. `;\n    msg += (purity >= 95 && recovery >= 70)\n      ? `Excellent\u2014fast and careful.`\n      : (purity < 90)\n        ? `You\u2019re moving quickly, but the clean bin is getting contaminated. Try using RE-RUN more for questionable pieces.`\n        : `Solid purity\u2014try improving ERGOT recovery by catching more dark pieces.`;\n    endSummary.textContent = msg;\n    endOverlay.style.display = 'grid';\n  }\n\n  startBtn.addEventListener('click', () => startRound({ practice:false }));\n  practiceBtn.addEventListener('click', () => startRound({ practice:true }));\n  restartBtn.addEventListener('click', () => startRound({ practice:practiceMode }));\n  backBtn.addEventListener('click', () => { endOverlay.style.display='none'; startOverlay.style.display='grid'; });\n\n  \/\/ ---------- Input handling ----------\n  function findPieceAt(x,y){\n    for (let i=pieces.length-1;i>=0;i--){\n      const p = pieces[i];\n      const rr = Math.max(p.w, p.h) * 0.75;\n      if (dist2(x,y,p.x,p.y) <= rr*rr) return p;\n    }\n    return null;\n  }\n\n  function onPointerDown(e){\n    if (!running) return;\n\n    const rect = canvas.getBoundingClientRect();\n    const x = (e.clientX - rect.left);\n    const y = (e.clientY - rect.top);\n\n    const p = findPieceAt(x,y);\n    if (!p) return;\n\n    activeId = e.pointerId;\n    grabbed = p;\n    grabbed.grabbed = true;\n\n    const idx = pieces.indexOf(grabbed);\n    if (idx >= 0) { pieces.splice(idx,1); pieces.push(grabbed); }\n\n    grabOffset.x = x - grabbed.x;\n    grabOffset.y = y - grabbed.y;\n\n    moveTrack = [{ t: now(), x, y }];\n\n    canvas.setPointerCapture(e.pointerId);\n  }\n\n  function onPointerMove(e){\n    if (!running) return;\n    if (e.pointerId !== activeId || !grabbed) return;\n\n    const rect = canvas.getBoundingClientRect();\n    const x = (e.clientX - rect.left);\n    const y = (e.clientY - rect.top);\n\n    grabbed.x = x - grabOffset.x;\n    grabbed.y = y - grabOffset.y;\n    grabbed.vx = 0; grabbed.vy = 0;\n\n    moveTrack.push({ t: now(), x, y });\n    if (moveTrack.length > 8) moveTrack.shift();\n  }\n\n  function onPointerUp(e){\n    if (!running) return;\n    if (e.pointerId !== activeId || !grabbed) return;\n\n    const rect = canvas.getBoundingClientRect();\n    const x = (e.clientX - rect.left);\n    const y = (e.clientY - rect.top);\n\n    let vx=0, vy=0;\n    if (moveTrack.length >= 2){\n      const a = moveTrack[0];\n      const b = moveTrack[moveTrack.length-1];\n      const dt = Math.max(1, (b.t - a.t));\n      vx = (b.x - a.x) \/ dt; \/\/ px\/ms\n      vy = (b.y - a.y) \/ dt;\n    }\n\n    const moved = moveTrack.length >= 2 ? Math.hypot(moveTrack.at(-1).x - moveTrack[0].x, moveTrack.at(-1).y - moveTrack[0].y) : 0;\n    const isTap = moved < 10;\n\n    if (isTap &#038;&#038; grabbed.type === TYPE.Q) {\n      sendToBin(grabbed, 'rerun');\n    } else {\n      const speed = Math.hypot(vx, vy); \/\/ px\/ms\n      const flickThreshold = 0.9;\n      const cleanCenter = { x: bins.clean.x + bins.clean.w\/2, y: bins.clean.y + bins.clean.h\/2 };\n      const dirToClean = { x: cleanCenter.x - grabbed.x, y: cleanCenter.y - grabbed.y };\n      const dirLen = Math.max(1, Math.hypot(dirToClean.x, dirToClean.y));\n      const dirNorm = { x: dirToClean.x\/dirLen, y: dirToClean.y\/dirLen };\n      const vNormLen = Math.max(1e-6, Math.hypot(vx, vy));\n      const vNorm = { x: vx\/vNormLen, y: vy\/vNormLen };\n      const alignment = (dirNorm.x*vNorm.x + dirNorm.y*vNorm.y);\n\n      if (speed > flickThreshold && alignment > 0.55) {\n        sendToBin(grabbed, 'clean');\n      } else {\n        const b = whichBin(x,y);\n        if (b) sendToBin(grabbed, b);\n        else {\n          grabbed.vx = (grabbed.homeX - grabbed.x) * 2;\n          grabbed.vy = (grabbed.homeY - grabbed.y) * 2;\n        }\n      }\n    }\n\n    grabbed.grabbed = false;\n    grabbed = null;\n    activeId = null;\n    moveTrack = [];\n  }\n\n  canvas.addEventListener('pointerdown', onPointerDown);\n  canvas.addEventListener('pointermove', onPointerMove);\n  canvas.addEventListener('pointerup', onPointerUp);\n  canvas.addEventListener('pointercancel', onPointerUp);\n\n  \/\/ ---------- Simulation ----------\n  function pulse() {\n    stage.classList.remove('shake');\n    void stage.offsetWidth;\n    stage.classList.add('shake');\n\n    for (const p of pieces) {\n      if (p.grabbed) continue;\n      p.vx += rand(-80, 80);\n      p.vy += rand(-30, 30);\n      p.rot += rand(-0.4, 0.4);\n    }\n  }\n\n  function update(dt) {\n    if (!running) return;\n\n    if (!practiceMode) {\n      const t = now();\n      if (t - lastTick >= 1000) {\n        timeLeft = Math.max(0, timeLeft - 1);\n        timeLeftEl.textContent = String(timeLeft);\n        lastTick = t;\n        if (timeLeft <= 0) { endRound(); return; }\n      }\n    } else {\n      timeLeftEl.textContent = '\u221e';\n    }\n\n    if (now() - lastPulseAt > pulseEvery) {\n      lastPulseAt = now();\n      pulse();\n    }\n\n    spawnAccumulator += dt * spawnRatePerSec;\n    while (spawnAccumulator >= 1) {\n      spawnAccumulator -= 1;\n      if (pieces.length < 120) pieces.push(makePiece());\n    }\n\n    const friction = 0.985;\n    const gravity = 28;\n\n    for (let i=pieces.length-1; i>=0; i--) {\n      const p = pieces[i];\n\n      if (p.flyTo) {\n        const { x:tx, y:ty, binName, t0 } = p.flyTo;\n        const age = now() - t0;\n        const k = clamp(age \/ 140, 0, 1);\n\n        p.x = p.x + (tx - p.x) * (0.20 + 0.22*k);\n        p.y = p.y + (ty - p.y) * (0.20 + 0.22*k);\n        p.rot *= 0.9;\n\n        if (age > 160) {\n          tallyToBin(p.type, binName);\n          pieces.splice(i,1);\n        }\n        continue;\n      }\n\n      if (p.grabbed) continue;\n\n      p.vy += gravity * dt;\n      p.x += p.vx * dt;\n      p.y += p.vy * dt;\n\n      p.vx *= friction;\n      p.vy *= friction;\n      p.rot *= 0.995;\n\n      const left = tray.x + 8;\n      const right = tray.x + tray.w - 8;\n      const top = tray.y + 8;\n      const bottom = tray.y + tray.h - 8;\n\n      if (p.x < left) { p.x = left; p.vx *= -0.55; }\n      if (p.x > right) { p.x = right; p.vx *= -0.55; }\n      if (p.y < top) { p.y = top; p.vy *= -0.55; }\n      if (p.y > bottom) {\n        p.y = bottom;\n        p.vy *= -0.35;\n        p.vx *= 0.92;\n      }\n\n      p.homeX = p.homeX + (p.x - p.homeX) * 0.01;\n      p.homeY = p.homeY + (p.y - p.homeY) * 0.01;\n\n      if (now() - p.bornAt > 18_000 && pieces.length > 60) {\n        pieces.splice(i,1);\n      }\n    }\n  }\n\n  \/\/ ---------- Rendering ----------\n  function roundRect(x, y, w, h, r) {\n    const rr = Math.min(r, w\/2, h\/2);\n    ctx.beginPath();\n    ctx.moveTo(x+rr, y);\n    ctx.arcTo(x+w, y, x+w, y+h, rr);\n    ctx.arcTo(x+w, y+h, x, y+h, rr);\n    ctx.arcTo(x, y+h, x, y, rr);\n    ctx.arcTo(x, y, x+w, y, rr);\n    ctx.closePath();\n  }\n\n  function drawTrayBackground() {\n    ctx.save();\n    ctx.fillStyle = 'rgba(255,255,255,0.06)';\n    roundRect(tray.x, tray.y, tray.w, tray.h, 18);\n    ctx.fill();\n    ctx.strokeStyle = 'rgba(255,255,255,0.14)';\n    ctx.lineWidth = 1;\n    ctx.stroke();\n\n    ctx.globalAlpha = 0.25;\n    ctx.strokeStyle = 'rgba(255,255,255,0.25)';\n    ctx.beginPath();\n    ctx.moveTo(spawnLine.x1, spawnLine.y);\n    ctx.lineTo(spawnLine.x2, spawnLine.y);\n    ctx.stroke();\n    ctx.restore();\n  }\n\n  function drawPiece(p) {\n    ctx.save();\n    ctx.translate(p.x, p.y);\n    ctx.rotate(p.rot);\n\n    \/\/ Style by type\n    if (p.type === TYPE.ERGOT) {\n      \/\/ CHANGED: deeper black, subtle edge for readability\n      ctx.fillStyle = 'rgba(0,0,0,0.96)';\n      ctx.strokeStyle = 'rgba(255,255,255,0.14)';\n    } else if (p.type === TYPE.Q) {\n      ctx.fillStyle = 'rgba(120,120,120,0.85)';\n      ctx.strokeStyle = 'rgba(255,255,255,0.14)';\n    } else {\n      ctx.fillStyle = 'rgba(235,235,235,0.85)';\n      ctx.strokeStyle = 'rgba(0,0,0,0.15)';\n    }\n\n    \/\/ Grain\/ergot shape (rounded capsule)\n    const w = p.w, h = p.h;\n    roundRect(-w\/2, -h\/2, w, h, h\/2);\n    ctx.fill();\n    ctx.lineWidth = 1;\n    ctx.stroke();\n\n    \/\/ CHANGED: rougher texture for ergot (specks + notch lines)\n    if (p.type === TYPE.ERGOT) {\n      \/\/ Speckle texture\n      ctx.save();\n      ctx.globalAlpha = 0.22;\n      ctx.fillStyle = 'rgba(255,255,255,0.55)';\n      const specks = 9;\n      for (let i = 0; i < specks; i++) {\n        const sx = rand(-p.w * 0.42, p.w * 0.42);\n        const sy = rand(-p.h * 0.35, p.h * 0.35);\n        const r = rand(0.6, 1.3);\n        ctx.beginPath();\n        ctx.arc(sx, sy, r, 0, Math.PI * 2);\n        ctx.fill();\n      }\n      ctx.restore();\n\n      \/\/ Slight \"notch\" lines along the length\n      ctx.save();\n      ctx.globalAlpha = 0.18;\n      ctx.strokeStyle = 'rgba(255,255,255,0.45)';\n      ctx.lineWidth = 1;\n      ctx.beginPath();\n      const segments = 4;\n      for (let s = 0; s < segments; s++) {\n        const px = -p.w * 0.35 + (s \/ (segments - 1)) * (p.w * 0.7);\n        ctx.moveTo(px, -p.h * 0.22);\n        ctx.lineTo(px + rand(-2, 2), p.h * 0.22);\n      }\n      ctx.stroke();\n      ctx.restore();\n    }\n\n    \/\/ Texture line (subtle baseline for all pieces)\n    ctx.globalAlpha = 0.25;\n    ctx.strokeStyle = 'rgba(0,0,0,0.25)';\n    ctx.beginPath();\n    ctx.moveTo(-w*0.25, 0);\n    ctx.lineTo(w*0.25, 0);\n    ctx.stroke();\n\n    \/\/ Highlight questionable pieces a bit so taps make sense\n    if (p.type === TYPE.Q) {\n      ctx.globalAlpha = 0.30;\n      ctx.strokeStyle = 'rgba(255,255,255,0.45)';\n      ctx.lineWidth = 2;\n      roundRect(-w\/2-2, -h\/2-2, w+4, h+4, (h\/2)+2);\n      ctx.stroke();\n    }\n\n    ctx.restore();\n  }\n\n  function render() {\n    ctx.clearRect(0,0,W,H);\n    drawTrayBackground();\n    for (const p of pieces) drawPiece(p);\n  }\n\n  \/\/ ---------- Main loop ----------\n  let lastFrame = now();\n  function frame() {\n    const t = now();\n    const dt = clamp((t - lastFrame) \/ 1000, 0, 0.05);\n    lastFrame = t;\n\n    update(dt);\n    render();\n    requestAnimationFrame(frame);\n  }\n\n  \/\/ ---------- Init ----------\n  updateRects();\n  updateHUD();\n  requestAnimationFrame(frame);\n\n})();\n<\/script>\n<\/body>\n<\/html>\n","protected":false},"excerpt":{"rendered":"<p>Ergot Sorter: Mini-Game Ergot Sorter \u2014 Mini-Game Time 60s Purity \u2014 Recovery \u2014 Waste \u2014 Keep it clean. Save the ergot. You\u2019re sorting a mixed stream. Work fast, but don\u2019t contaminate the clean grain. &#x1f449; Drag dark pieces into ERGOT &#x1f449; Flick toward CLEAN to send grain quickly &#x1f449; Tap a questionable piece to send&hellip; <a href=\"http:\/\/pastcasts.com\/?page_id=620\">Read More<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"jetpack_post_was_ever_published":false,"footnotes":""},"class_list":["post-620","page","type-page","status-publish","hentry"],"jetpack_shortlink":"https:\/\/wp.me\/P3kcV8-a0","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"http:\/\/pastcasts.com\/index.php?rest_route=\/wp\/v2\/pages\/620","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/pastcasts.com\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"http:\/\/pastcasts.com\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"http:\/\/pastcasts.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/pastcasts.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=620"}],"version-history":[{"count":3,"href":"http:\/\/pastcasts.com\/index.php?rest_route=\/wp\/v2\/pages\/620\/revisions"}],"predecessor-version":[{"id":626,"href":"http:\/\/pastcasts.com\/index.php?rest_route=\/wp\/v2\/pages\/620\/revisions\/626"}],"wp:attachment":[{"href":"http:\/\/pastcasts.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=620"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}