Clone
MonteCarloGame.js
export const MonteCarloGame = {
  start(container, channel) {
    while (container.firstChild) container.removeChild(container.firstChild)

    const CANVAS_SIZE = 300
    const MAX_DARTS = 200
    let dartsInCircle = 0
    let totalDarts = 0
    let submitted = false

    // Build UI
    const title = document.createElement("div")
    title.className = "mg-title"
    title.textContent = "Monte Carlo Pi"

    const subtitle = document.createElement("div")
    subtitle.className = "mg-subtitle"
    subtitle.textContent = "Tap to throw darts! Estimate Pi by the ratio of hits inside the circle."

    const canvas = document.createElement("canvas")
    canvas.id = "mc-canvas"
    canvas.width = CANVAS_SIZE
    canvas.height = CANVAS_SIZE
    canvas.style.cssText = "border-radius:0.75rem;border:2px solid rgba(255,255,255,0.2);touch-action:none;cursor:crosshair;"
    const ctx = canvas.getContext("2d")

    const stats = document.createElement("div")
    stats.className = "mc-stats"

    const dartsStatEl = createStat("0 / " + MAX_DARTS, "Darts")
    const estimateStatEl = createStat("?.????", "Pi Estimate")
    const actualStatEl = createStat("3.1416", "Actual Pi")
    stats.appendChild(dartsStatEl.container)
    stats.appendChild(estimateStatEl.container)
    stats.appendChild(actualStatEl.container)

    const resultArea = document.createElement("div")
    resultArea.style.cssText = "margin-top: 1rem; min-height: 3rem;"

    container.appendChild(title)
    container.appendChild(subtitle)
    container.appendChild(canvas)
    container.appendChild(stats)
    container.appendChild(resultArea)

    // Draw initial state
    drawBackground(ctx, CANVAS_SIZE)

    // Handle dart throws
    canvas.addEventListener("pointerdown", (e) => {
      if (totalDarts >= MAX_DARTS || submitted) return

      const rect = canvas.getBoundingClientRect()
      const scaleX = CANVAS_SIZE / rect.width
      const scaleY = CANVAS_SIZE / rect.height
      const x = (e.clientX - rect.left) * scaleX
      const y = (e.clientY - rect.top) * scaleY

      throwDart(x, y)
    })

    // Also support rapid tapping / random throw button
    const rapidBtn = document.createElement("button")
    rapidBtn.className = "mg-btn"
    rapidBtn.textContent = "Throw 10 Random Darts"
    rapidBtn.addEventListener("click", () => {
      for (let i = 0; i < 10 && totalDarts < MAX_DARTS; i++) {
        throwDart(Math.random() * CANVAS_SIZE, Math.random() * CANVAS_SIZE)
      }
    })
    container.appendChild(rapidBtn)

    function throwDart(x, y) {
      totalDarts++

      // Check if inside circle (centered, radius = CANVAS_SIZE/2)
      const cx = CANVAS_SIZE / 2
      const cy = CANVAS_SIZE / 2
      const r = CANVAS_SIZE / 2
      const dist = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2)
      const inside = dist <= r

      if (inside) dartsInCircle++

      if (window.SoundFX) window.SoundFX.dart()

      // Draw dart
      ctx.beginPath()
      ctx.arc(x, y, 3, 0, Math.PI * 2)
      ctx.fillStyle = inside ? "#22d3ee" : "#ef4444"
      ctx.fill()

      // Update stats
      const estimate = totalDarts > 0 ? (4 * dartsInCircle / totalDarts) : 0
      dartsStatEl.value.textContent = `${totalDarts} / ${MAX_DARTS}`
      estimateStatEl.value.textContent = estimate.toFixed(4)

      // Color estimate based on accuracy
      const error = Math.abs(estimate - Math.PI)
      if (error < 0.05) {
        estimateStatEl.value.style.color = "#22c55e"
      } else if (error < 0.2) {
        estimateStatEl.value.style.color = "#f59e0b"
      } else {
        estimateStatEl.value.style.color = "#22d3ee"
      }

      // Auto-submit when all darts thrown
      if (totalDarts >= MAX_DARTS && !submitted) {
        submitted = true
        const finalEstimate = 4 * dartsInCircle / totalDarts

        channel.push("mc_submit", {
          estimate: parseFloat(finalEstimate.toFixed(6)),
          darts: totalDarts
        }).receive("ok", ({ score, error: err }) => {
          if (window.SoundFX) window.SoundFX.score()
          while (resultArea.firstChild) resultArea.removeChild(resultArea.firstChild)

          const result = document.createElement("div")
          result.style.cssText = "color: #22d3ee; font-size: 1.3rem; margin-bottom: 0.5rem;"
          result.textContent = `Your estimate: ${finalEstimate.toFixed(4)}`

          const errorEl = document.createElement("div")
          errorEl.style.cssText = "color: #a78bfa; margin-bottom: 0.5rem;"
          errorEl.textContent = `Error: ${err} | Score: ${score} points`

          const playAgain = document.createElement("button")
          playAgain.className = "mg-btn"
          playAgain.textContent = "Play Again"
          playAgain.addEventListener("click", () => MonteCarloGame.start(container, channel))

          const close = document.createElement("button")
          close.className = "mg-btn secondary"
          close.textContent = "Back to Hub"
          close.addEventListener("click", () => window.piStation.closeMiniGame())

          resultArea.appendChild(result)
          resultArea.appendChild(errorEl)
          resultArea.appendChild(playAgain)
          resultArea.appendChild(close)
        })

        rapidBtn.style.display = "none"
      }
    }

    // Listen for other players' results
    channel.on("mc_result", ({ name, estimate, score }) => {
      const announcement = document.createElement("div")
      announcement.style.cssText = "color: #a78bfa; font-size: 0.8rem; margin-top: 0.25rem;"
      announcement.textContent = `${name} estimated ${estimate.toFixed(4)} (${score} pts)`
      resultArea.appendChild(announcement)
      setTimeout(() => { if (announcement.parentNode) announcement.parentNode.removeChild(announcement) }, 5000)
    })
  }
}

function drawBackground(ctx, size) {
  // Square background
  ctx.fillStyle = "#1e1b4b"
  ctx.fillRect(0, 0, size, size)

  // Circle
  ctx.beginPath()
  ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2)
  ctx.fillStyle = "rgba(99, 102, 241, 0.15)"
  ctx.fill()
  ctx.strokeStyle = "rgba(99, 102, 241, 0.5)"
  ctx.lineWidth = 2
  ctx.stroke()

  // Crosshairs
  ctx.strokeStyle = "rgba(255, 255, 255, 0.1)"
  ctx.lineWidth = 1
  ctx.beginPath()
  ctx.moveTo(size / 2, 0); ctx.lineTo(size / 2, size)
  ctx.moveTo(0, size / 2); ctx.lineTo(size, size / 2)
  ctx.stroke()
}

function createStat(initialValue, label) {
  const c = document.createElement("div")
  c.className = "mc-stat"

  const v = document.createElement("div")
  v.className = "mc-stat-value"
  v.textContent = initialValue

  const l = document.createElement("div")
  l.className = "mc-stat-label"
  l.textContent = label

  c.appendChild(v)
  c.appendChild(l)

  return { container: c, value: v, label: l }
}