Clone
// Pi digits after the "3."
const PI_DIGITS = "14159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848111745028410270193852110555964462294895493038196"
export const PiMemoryGame = {
start(container, channel) {
// Clear container safely
while (container.firstChild) container.removeChild(container.firstChild)
let position = 0
let gameOver = false
let startTime = null
// Build UI
const title = document.createElement("div")
title.className = "mg-title"
title.textContent = "Pi Memory Sprint"
const subtitle = document.createElement("div")
subtitle.className = "mg-subtitle"
subtitle.textContent = "Type the digits of Pi: 3."
const display = document.createElement("div")
display.className = "pi-display"
display.textContent = "3."
const scoreDisplay = document.createElement("div")
scoreDisplay.style.cssText = "font-size: 1.2rem; color: #a78bfa; margin: 0.5rem 0;"
scoreDisplay.textContent = "Digits: 0"
const raceContainer = document.createElement("div")
raceContainer.style.cssText = "margin: 0.5rem 0; min-height: 2rem;"
raceContainer.id = "pi-race"
const numpad = document.createElement("div")
numpad.className = "pi-numpad"
const resultArea = document.createElement("div")
resultArea.style.cssText = "margin: 1rem 0; min-height: 3rem;"
container.appendChild(title)
container.appendChild(subtitle)
container.appendChild(display)
container.appendChild(scoreDisplay)
container.appendChild(raceContainer)
container.appendChild(numpad)
container.appendChild(resultArea)
// Create numpad buttons
for (let i = 1; i <= 9; i++) {
const btn = document.createElement("button")
btn.textContent = String(i)
btn.addEventListener("click", () => handleDigit(String(i)))
numpad.appendChild(btn)
}
const zeroBtn = document.createElement("button")
zeroBtn.className = "zero"
zeroBtn.textContent = "0"
zeroBtn.addEventListener("click", () => handleDigit("0"))
numpad.appendChild(zeroBtn)
function handleDigit(digit) {
if (gameOver) return
if (!startTime) startTime = Date.now()
const expected = PI_DIGITS[position]
if (digit === expected) {
position++
if (window.SoundFX) window.SoundFX.correct()
display.textContent = "3." + PI_DIGITS.substring(0, position)
scoreDisplay.textContent = `Digits: ${position}`
// Scroll display to show latest digits
display.scrollLeft = display.scrollWidth
channel.push("pi_check_digit", { position: position - 1, digit })
// Milestone celebrations
if (position === 10 || position === 25 || position === 50 || position === 100) {
if (window.SoundFX) window.SoundFX.milestone()
const milestone = document.createElement("div")
milestone.style.cssText = "color: #22d3ee; font-size: 1.5rem; animation: pulse 0.5s;"
milestone.textContent = `${position} digits!`
resultArea.appendChild(milestone)
setTimeout(() => { if (milestone.parentNode) milestone.parentNode.removeChild(milestone) }, 2000)
}
} else {
// Wrong digit — game over
if (window.SoundFX) window.SoundFX.wrong()
gameOver = true
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
numpad.style.opacity = "0.3"
numpad.style.pointerEvents = "none"
const wrongIndicator = document.createElement("span")
wrongIndicator.style.cssText = "color: #ef4444; text-decoration: line-through;"
wrongIndicator.textContent = digit
display.appendChild(wrongIndicator)
const correctIndicator = document.createElement("span")
correctIndicator.style.cssText = "color: #22c55e;"
correctIndicator.textContent = ` (${expected})`
display.appendChild(correctIndicator)
// Submit score
channel.push("pi_game_over", { score: position })
.receive("ok", () => {
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.5rem; margin-bottom: 0.5rem;"
result.textContent = `${position} digits in ${elapsed}s!`
resultArea.appendChild(result)
const playAgain = document.createElement("button")
playAgain.className = "mg-btn"
playAgain.textContent = "Play Again"
playAgain.addEventListener("click", () => PiMemoryGame.start(container, channel))
resultArea.appendChild(playAgain)
const close = document.createElement("button")
close.className = "mg-btn secondary"
close.textContent = "Back to Hub"
close.addEventListener("click", () => window.piStation.closeMiniGame())
resultArea.appendChild(close)
})
}
}
// Listen for other players' progress
channel.on("pi_progress", ({ name, position: pos }) => {
updateRace(name, pos, raceContainer)
})
channel.on("pi_score", ({ name, score }) => {
const announcement = document.createElement("div")
announcement.style.cssText = "color: #a78bfa; font-size: 0.8rem; margin-top: 0.25rem;"
announcement.textContent = `${name} got ${score} digits!`
raceContainer.appendChild(announcement)
setTimeout(() => { if (announcement.parentNode) announcement.parentNode.removeChild(announcement) }, 5000)
})
// Keyboard support
function keyHandler(e) {
if (e.key >= "0" && e.key <= "9") {
handleDigit(e.key)
}
}
document.addEventListener("keydown", keyHandler)
// Cleanup on close
const origClose = window.piStation.closeMiniGame
window.piStation.closeMiniGame = function() {
document.removeEventListener("keydown", keyHandler)
channel.off("pi_progress")
channel.off("pi_score")
window.piStation.closeMiniGame = origClose
origClose()
}
}
}
function updateRace(name, position, raceContainer) {
let entry = raceContainer.querySelector(`[data-player="${CSS.escape(name)}"]`)
if (!entry) {
entry = document.createElement("div")
entry.setAttribute("data-player", name)
entry.style.cssText = "display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;color:#e2e8f0;margin:0.15rem 0;"
raceContainer.appendChild(entry)
}
// Clear and rebuild entry
while (entry.firstChild) entry.removeChild(entry.firstChild)
const nameSpan = document.createElement("span")
nameSpan.style.cssText = "width:60px;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"
nameSpan.textContent = name
const barOuter = document.createElement("div")
barOuter.style.cssText = "flex:1;height:8px;background:rgba(255,255,255,0.1);border-radius:4px;overflow:hidden;"
const barInner = document.createElement("div")
const width = Math.min(position, 100)
barInner.style.cssText = `height:100%;background:linear-gradient(90deg,#06b6d4,#8b5cf6);border-radius:4px;width:${width}%;transition:width 0.3s;`
barOuter.appendChild(barInner)
const posSpan = document.createElement("span")
posSpan.style.cssText = "color:#22d3ee;min-width:2rem;"
posSpan.textContent = position
entry.appendChild(nameSpan)
entry.appendChild(barOuter)
entry.appendChild(posSpan)
}