Spaces:
Running on Zero
Running on Zero
| /** | |
| * Interactive Mini-Game Button | |
| */ | |
| class GameButton { | |
| constructor(buttonId) { | |
| this.button = document.getElementById(buttonId) | |
| this.runButton = document.getElementById("runBtn") | |
| if (!this.button) { | |
| console.error("GameButton: Target button not found") | |
| return | |
| } | |
| // --- Injection Logic --- | |
| this.button.classList.add("game-btn") // Add our styling class | |
| // Wrap existing text to animate it | |
| if (!this.button.querySelector(".btn-text")) { | |
| const textSpan = document.createElement("span") | |
| textSpan.className = "btn-text" | |
| while (this.button.firstChild) { | |
| textSpan.appendChild(this.button.firstChild) | |
| } | |
| this.button.appendChild(textSpan) | |
| } | |
| // Create Game Container | |
| if (!this.button.querySelector(".game-container")) { | |
| this.gameContainer = document.createElement("div") | |
| this.gameContainer.className = "game-container" | |
| this.gameContainer.id = "gameContainer" | |
| this.button.appendChild(this.gameContainer) | |
| } else { | |
| this.gameContainer = this.button.querySelector(".game-container") | |
| } | |
| // Create Progress Bar | |
| if (!this.button.querySelector(".game-button-progress-bar")) { | |
| const progBar = document.createElement("div") | |
| progBar.className = "game-button-progress-bar" | |
| this.progressFill = document.createElement("div") | |
| this.progressFill.className = "progress-fill" | |
| this.progressFill.id = "progressFill" | |
| progBar.appendChild(this.progressFill) | |
| this.button.appendChild(progBar) | |
| } else { | |
| this.progressFill = this.button.querySelector(".progress-fill") | |
| } | |
| // Create Completion Controls | |
| if (!this.button.querySelector(".completion-controls")) { | |
| const controls = document.createElement("div") | |
| controls.className = "completion-controls" | |
| this.compText = document.createElement("span") | |
| this.compText.className = "completion-text" | |
| this.compText.innerText = "Fertig!" | |
| controls.appendChild(this.compText) | |
| this.closeBtn = document.createElement("button") | |
| this.closeBtn.className = "close-game-btn" | |
| this.closeBtn.title = "Close Game" | |
| // Use innerHTML for SVG icon | |
| this.closeBtn.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>' | |
| // Prevent close button click from triggering main button | |
| this.closeBtn.addEventListener("click", (e) => { | |
| e.stopPropagation() | |
| this.reset() | |
| }) | |
| controls.appendChild(this.closeBtn) | |
| this.button.appendChild(controls) | |
| } else { | |
| this.closeBtn = this.button.querySelector(".close-game-btn") | |
| this.compText = this.button.querySelector(".completion-text") | |
| this.closeBtn.addEventListener("click", (e) => { | |
| e.stopPropagation() | |
| this.reset() | |
| }) | |
| } | |
| if (!this.button.querySelector(".points-wrapper")) { | |
| const pointsWrapper = document.createElement("div") | |
| pointsWrapper.className = "points-wrapper" | |
| const pointsTextWrapper = document.createElement("span") | |
| pointsTextWrapper.className = "points-text-wrapper" | |
| const pointsText = document.createElement("span") | |
| pointsText.className = "points-text" | |
| pointsText.innerText = "p" | |
| this.points = document.createElement("span") | |
| this.points.className = "points" | |
| this.points.innerText = "0" | |
| pointsTextWrapper.appendChild(this.points) | |
| pointsTextWrapper.appendChild(pointsText) | |
| pointsWrapper.appendChild(pointsTextWrapper) | |
| this.button.appendChild(pointsWrapper) | |
| } else { | |
| this.points = this.button.querySelector(".points") | |
| } | |
| // ----------------------- | |
| this.durationInput = document.getElementById("durationInput") | |
| this.state = "IDLE" | |
| this.processDuration = 5000 | |
| this.startTime = 0 | |
| this.rafId = null | |
| this.gamePoints = 0 | |
| this.games = ["snake", "memory", "simon"] | |
| this.currentGame = null | |
| this.button.addEventListener("click", (e) => { | |
| if (this.button.classList.contains("active")) e.preventDefault() | |
| if (this.state === "IDLE") this.activate() | |
| }) | |
| // Pause on blur | |
| this.button.addEventListener("blur", () => { | |
| // if (this.currentGame && (this.state === "ACTIVE" || this.state === "COMPLETE")) { | |
| // this.currentGame.pause() | |
| // this.button.classList.add("paused") | |
| // } | |
| }) | |
| // Resume on focus | |
| this.button.addEventListener("focus", () => { | |
| if (this.currentGame && (this.state === "ACTIVE" || this.state === "COMPLETE")) { | |
| this.currentGame.resume() | |
| this.button.classList.remove("paused") | |
| } | |
| }) | |
| this.button.addEventListener("focusout", (e) => { | |
| if (this.currentGame && (this.state === "ACTIVE" || this.state === "COMPLETE")) { | |
| if (e.currentTarget.contains(e.relatedTarget)) { | |
| /* Focus will still be within the container */ | |
| this.currentGame.resume() | |
| this.button.classList.remove("paused") | |
| } else { | |
| /* Focus will leave the container */ | |
| this.currentGame.pause() | |
| this.button.classList.add("paused") | |
| } | |
| } | |
| }) | |
| } | |
| activate() { | |
| if (this.state !== "IDLE") return | |
| this.runButton.click() | |
| // Get duration from input if present | |
| if (this.durationInput) { | |
| const val = parseInt(this.durationInput.value) | |
| if (val && val > 0) this.processDuration = val | |
| } | |
| this.state = "ACTIVE" | |
| this.button.classList.add("active", "process-running") | |
| this.button.classList.remove("complete") | |
| // Start the fake process | |
| this.startTime = Date.now() | |
| this.progressFill.style.width = "0%" | |
| setTimeout(() => this.updateProgress(), 200) | |
| // Pick a random game | |
| setTimeout(() => { | |
| if (this.state === "ACTIVE") { | |
| this.launchRandomGame() | |
| } | |
| }, 500) | |
| } | |
| updateProgress() { | |
| if (this.state === "IDLE") return | |
| const elapsed = Date.now() - this.startTime | |
| const firstActiveProgressBar = document.querySelectorAll("div.progress-bar")[0] | |
| const progressError = document.querySelectorAll('div[data-testid="status-tracker"] span.error')[0] | |
| if (firstActiveProgressBar) { | |
| const progress = firstActiveProgressBar.style.width | |
| this.progressFill.style.width = `${progress}` | |
| } | |
| // if (progress >= 100 && this.state !== "COMPLETE") { | |
| // this.complete() | |
| // } | |
| if (progressError) { | |
| console.log("complete error") | |
| this.completeError() | |
| } | |
| if (this.button.classList.contains("process-running") === false && this.state !== "COMPLETE") { | |
| console.log("complete") | |
| this.progressFill.style.width = "100%" | |
| this.complete() | |
| } | |
| if (this.state !== "IDLE") { | |
| if (this.button.classList.contains("process-running")) { | |
| this.rafId = requestAnimationFrame(() => this.updateProgress()) | |
| } | |
| } | |
| } | |
| completeError() { | |
| this.state = "COMPLETE" | |
| this.compText.innerText = "ERROR!" | |
| this.button.classList.add("complete", "error") | |
| this.button.classList.remove("process-running") | |
| // Game continues running! No cleanup here. | |
| } | |
| complete() { | |
| this.state = "COMPLETE" | |
| this.button.classList.add("complete") | |
| // Game continues running! No cleanup here. | |
| } | |
| reset() { | |
| this.state = "IDLE" | |
| this.button.classList.remove("active") | |
| this.button.classList.remove("complete", "error") | |
| this.compText.innerText = "Fertig!" | |
| this.progressFill.style.width = "0%" | |
| if (this.currentGame) { | |
| this.currentGame.cleanup() | |
| this.currentGame = null | |
| } | |
| this.gameContainer.innerHTML = "" | |
| // Remove paused class if present | |
| this.button.classList.remove("paused") | |
| } | |
| launchRandomGame() { | |
| // Simple random | |
| const gameType = this.games[Math.floor(Math.random() * this.games.length)] | |
| // const gameType = "snake" | |
| this.gameContainer.innerHTML = "" | |
| const w = this.button.offsetWidth | |
| const h = this.button.offsetHeight | |
| if (gameType === "snake") { | |
| this.currentGame = new SnakeGame(this.gameContainer, w, h) | |
| } else if (gameType === "memory") { | |
| this.currentGame = new MemoryGame(this.gameContainer) | |
| } else if (gameType === "simon") { | |
| this.currentGame = new SimonGame(this.gameContainer) | |
| } | |
| } | |
| } | |
| /** | |
| * Snake Game Implementation | |
| */ | |
| class SnakeGame { | |
| constructor(container, width, height) { | |
| this.canvas = document.createElement("canvas") | |
| this.canvas.id = "snakeCanvas" | |
| this.canvas.width = Math.min(width - 40, 400) | |
| this.canvas.height = 70 | |
| container.appendChild(this.canvas) | |
| this.ctx = this.canvas.getContext("2d") | |
| this.gridSize = 10 | |
| const startX = Math.floor(this.canvas.width / this.gridSize / 2) | |
| const startY = Math.floor(this.canvas.height / this.gridSize / 2) | |
| this.snake = [{ x: startX, y: startY }] | |
| this.dx = 1 | |
| this.dy = 0 | |
| this.food = this.spawnFood() | |
| this.score = 0 | |
| this.gameOver = false | |
| this.interval = setInterval(() => this.loop(), 120) | |
| this.handleKey = this.handleKey.bind(this) | |
| document.addEventListener("keydown", this.handleKey) | |
| this.canvas.addEventListener("mousedown", (e) => this.handleClick(e)) | |
| } | |
| spawnFood() { | |
| return { | |
| x: Math.floor(Math.random() * (this.canvas.width / this.gridSize)), | |
| y: Math.floor(Math.random() * (this.canvas.height / this.gridSize)), | |
| } | |
| } | |
| handleKey(e) { | |
| if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) { | |
| e.preventDefault() | |
| } | |
| switch (e.key) { | |
| case "ArrowUp": | |
| if (this.dy === 0) { | |
| this.dx = 0 | |
| this.dy = -1 | |
| } | |
| break | |
| case "ArrowDown": | |
| if (this.dy === 0) { | |
| this.dx = 0 | |
| this.dy = 1 | |
| } | |
| break | |
| case "ArrowLeft": | |
| if (this.dx === 0) { | |
| this.dx = -1 | |
| this.dy = 0 | |
| } | |
| break | |
| case "ArrowRight": | |
| if (this.dx === 0) { | |
| this.dx = 1 | |
| this.dy = 0 | |
| } | |
| break | |
| } | |
| } | |
| handleClick(e) { | |
| e.stopPropagation() | |
| const rect = this.canvas.getBoundingClientRect() | |
| const clickX = e.clientX - rect.left | |
| const clickY = e.clientY - rect.top | |
| const headScreenX = this.snake[0].x * this.gridSize + this.gridSize / 2 | |
| const headScreenY = this.snake[0].y * this.gridSize + this.gridSize / 2 | |
| const diffX = clickX - headScreenX | |
| const diffY = clickY - headScreenY | |
| if (Math.abs(diffX) > Math.abs(diffY)) { | |
| if (diffX > 0 && this.dx === 0) { | |
| this.dx = 1 | |
| this.dy = 0 | |
| } else if (diffX < 0 && this.dx === 0) { | |
| this.dx = -1 | |
| this.dy = 0 | |
| } | |
| } else { | |
| if (diffY > 0 && this.dy === 0) { | |
| this.dx = 0 | |
| this.dy = 1 | |
| } else if (diffY < 0 && this.dy === 0) { | |
| this.dx = 0 | |
| this.dy = -1 | |
| } | |
| } | |
| } | |
| loop() { | |
| if (this.gameOver) return | |
| const head = { x: this.snake[0].x + this.dx, y: this.snake[0].y + this.dy } | |
| const cols = Math.floor(this.canvas.width / this.gridSize) | |
| const rows = Math.floor(this.canvas.height / this.gridSize) | |
| if (head.x < 0) head.x = cols - 1 | |
| if (head.x >= cols) head.x = 0 | |
| if (head.y < 0) head.y = rows - 1 | |
| if (head.y >= rows) head.y = 0 | |
| if (this.snake.some((s) => s.x === head.x && s.y === head.y)) { | |
| this.snake = [{ x: Math.floor(cols / 2), y: Math.floor(rows / 2) }] | |
| this.gameOver = true | |
| this.canvas.classList.add("game-over") | |
| this.restart(4000) | |
| return | |
| } | |
| this.snake.unshift(head) | |
| if (head.x === this.food.x && head.y === this.food.y) { | |
| this.score += 10 | |
| document.querySelector(".points").innerText = this.score | |
| this.food = this.spawnFood() | |
| } else { | |
| this.snake.pop() | |
| } | |
| this.draw() | |
| } | |
| draw() { | |
| this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) | |
| this.ctx.fillStyle = "#10b981" | |
| this.snake.forEach((s) => { | |
| this.ctx.fillRect(s.x * this.gridSize, s.y * this.gridSize, this.gridSize - 1, this.gridSize - 1) | |
| }) | |
| this.ctx.fillStyle = "#ef4444" | |
| this.ctx.fillRect(this.food.x * this.gridSize, this.food.y * this.gridSize, this.gridSize - 1, this.gridSize - 1) | |
| } | |
| cleanup() { | |
| clearInterval(this.interval) | |
| this.restart() | |
| document.removeEventListener("keydown", this.handleKey) | |
| } | |
| pause() { | |
| clearInterval(this.interval) | |
| } | |
| resume() { | |
| clearInterval(this.interval) | |
| this.canvas.classList.remove("game-over") | |
| this.interval = setInterval(() => this.loop(), 120) | |
| } | |
| restart(time) { | |
| setTimeout(() => { | |
| this.snake = [{ x: Math.floor(this.canvas.width / this.gridSize / 2), y: Math.floor(this.canvas.height / this.gridSize / 2) }] | |
| this.dx = 1 | |
| this.dy = 0 | |
| this.food = this.spawnFood() | |
| this.score = 0 | |
| document.querySelector(".points").innerText = this.score | |
| this.gameOver = false | |
| }, time || 0) | |
| } | |
| } | |
| /** | |
| * Memory Game Implementation | |
| */ | |
| class MemoryGame { | |
| constructor(container) { | |
| this.grid = document.createElement("div") | |
| this.grid.className = "memory-grid" | |
| container.appendChild(this.grid) | |
| const icons = ["🐌", "🦕", "♥️", "⚡", "🌱", "🤖"] | |
| this.cards = [...icons, ...icons] | |
| this.cards.sort(() => Math.random() - 0.5) | |
| this.flipped = [] | |
| this.matched = [] | |
| this.render() | |
| } | |
| render() { | |
| this.cards.forEach((icon, index) => { | |
| const { r, g, b } = getAverageColor(icon) | |
| const card = document.createElement("div") | |
| card.style.setProperty("--card-color", `rgb(${r},${g},${b})`) | |
| // card.style.setProperty("--card-color", `rgba(${r},${g},${b}, 0.5)`) | |
| // card.style.setProperty("--card-color", `color(from rgb(${r},${g},${b}) h calc(from rgb(${r},${g},${b}) h + 180)`) | |
| card.className = "memory-card" | |
| card.dataset.icon = icon | |
| card.addEventListener("mousedown", (e) => e.preventDefault()) | |
| card.addEventListener("click", (e) => { | |
| e.stopPropagation() | |
| this.flip(card, index) | |
| }) | |
| this.grid.appendChild(card) | |
| }) | |
| } | |
| flip(card, index) { | |
| if (this.flipped.length >= 2 || this.flipped.includes(index) || this.matched.includes(index)) return | |
| card.innerText = this.cards[index] | |
| card.classList.add("flipped") | |
| this.flipped.push(index) | |
| if (this.flipped.length === 2) { | |
| this.checkMatch() | |
| } | |
| } | |
| checkMatch() { | |
| const [idx1, idx2] = this.flipped | |
| const card1 = this.grid.children[idx1] | |
| const card2 = this.grid.children[idx2] | |
| if (this.cards[idx1] === this.cards[idx2]) { | |
| this.matched.push(idx1, idx2) | |
| card1.classList.add("matched") | |
| card2.classList.add("matched") | |
| this.flipped = [] | |
| if (this.matched.length === this.cards.length) { | |
| setTimeout(() => { | |
| this.matched = [] | |
| this.flipped = [] | |
| this.cards.sort(() => Math.random() - 0.5) | |
| this.grid.innerHTML = "" | |
| this.render() | |
| }, 2000) | |
| } | |
| } else { | |
| setTimeout(() => { | |
| card1.classList.remove("flipped") | |
| card1.innerText = "" | |
| card2.classList.remove("flipped") | |
| card2.innerText = "" | |
| this.flipped = [] | |
| }, 800) | |
| } | |
| } | |
| cleanup() {} | |
| pause() {} | |
| resume() {} | |
| } | |
| /** | |
| * Simon Game Implementation | |
| */ | |
| class SimonGame { | |
| constructor(container) { | |
| this.board = document.createElement("div") | |
| this.board.className = "simon-board" | |
| container.appendChild(this.board) | |
| const colors = ["simon-green", "simon-red", "simon-yellow", "simon-blue"] | |
| this.sequence = [] | |
| this.playerSequence = [] | |
| this.buttons = [] | |
| this.score = 0 | |
| colors.forEach((color, i) => { | |
| const btn = document.createElement("div") | |
| btn.className = `simon-btn ${color}` | |
| btn.dataset.id = i | |
| btn.addEventListener("mousedown", (e) => e.preventDefault()) | |
| btn.addEventListener("click", (e) => { | |
| e.stopPropagation() | |
| this.handleInput(i) | |
| }) | |
| this.board.appendChild(btn) | |
| this.buttons.push(btn) | |
| }) | |
| this.isActive = false | |
| this.timer = setTimeout(() => this.nextRound(), 600) | |
| } | |
| nextRound() { | |
| this.sequence.push(Math.floor(Math.random() * 4)) | |
| this.playerSequence = [] | |
| this.playSequence() | |
| } | |
| playSequence() { | |
| this.isActive = false | |
| let i = 0 | |
| this.interval = setInterval(() => { | |
| this.flash(this.sequence[i]) | |
| i++ | |
| if (i >= this.sequence.length) { | |
| clearInterval(this.interval) | |
| this.isActive = true | |
| } | |
| }, 800) | |
| } | |
| flash(btnIndex) { | |
| if (!this.buttons[btnIndex]) return | |
| const btn = this.buttons[btnIndex] | |
| btn.classList.add("lit") | |
| setTimeout(() => btn.classList.remove("lit"), 300) | |
| } | |
| handleInput(index) { | |
| if (!this.isActive) return | |
| this.flash(index) | |
| this.playerSequence.push(index) | |
| const currentStep = this.playerSequence.length - 1 | |
| if (this.playerSequence[currentStep] !== this.sequence[currentStep]) { | |
| // Fail | |
| this.sequence = [] | |
| this.isActive = false | |
| this.board.classList.add("game-over") | |
| setTimeout(() => { | |
| this.score = 0 | |
| document.querySelector(".points").innerText = this.score | |
| this.board.classList.remove("game-over") | |
| this.nextRound() | |
| }, 4000) | |
| return | |
| } | |
| if (this.playerSequence.length === this.sequence.length) { | |
| this.isActive = false | |
| this.score += 10 | |
| document.querySelector(".points").innerText = this.score | |
| setTimeout(() => this.nextRound(), 1000) | |
| } | |
| } | |
| cleanup() { | |
| clearTimeout(this.timer) | |
| this.score = 0 | |
| document.querySelector(".points").innerText = this.score | |
| this.board.classList.remove("game-over") | |
| if (this.interval) clearInterval(this.interval) | |
| } | |
| pause() { | |
| this.paused = true | |
| clearTimeout(this.timer) | |
| if (this.interval) clearInterval(this.interval) | |
| this.buttons.forEach((b) => b.classList.remove("lit")) | |
| } | |
| resume() { | |
| if (!this.paused) return | |
| this.paused = false | |
| this.playSequence() | |
| } | |
| } | |
| // Initialize | |
| const initInterval = setInterval(() => { | |
| if (document.querySelector("#gameBtn")) { | |
| console.log("Game Button found!") | |
| const btn = new GameButton("gameBtn") | |
| clearInterval(initInterval) | |
| } | |
| }, 250) | |
| const getAverageColor = (emoji) => { | |
| const canvas = document.createElement("canvas") | |
| const ctx = canvas.getContext("2d") | |
| const size = 30 | |
| canvas.width = size | |
| canvas.height = size | |
| ctx.textBaseline = "middle" | |
| ctx.textAlign = "center" | |
| ctx.font = `${size - 4}px sans-serif` | |
| ctx.fillText(emoji, size / 2, size / 2) | |
| const data = ctx.getImageData(0, 0, size, size).data | |
| let r = 0, | |
| g = 0, | |
| b = 0, | |
| count = 0 | |
| for (let i = 0; i < data.length; i += 4) { | |
| const alpha = data[i + 3] | |
| if (alpha > 50) { | |
| r += data[i] | |
| g += data[i + 1] | |
| b += data[i + 2] | |
| count++ | |
| } | |
| } | |
| if (count > 0) { | |
| r = Math.floor(r / count) | |
| g = Math.floor(g / count) | |
| b = Math.floor(b / count) | |
| } else { | |
| r = 255 | |
| g = 255 | |
| b = 255 | |
| } | |
| return { r, g, b } | |
| } | |