diff --git a/__init__.py b/__init__.py index f7495f5..8a690db 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,11 @@ +import os +from nodes import EXTENSION_WEB_DIRS + from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS __version__ = "1.0.1" -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] +# Register web directory for Snake game +EXTENSION_WEB_DIRS["crash-utils"] = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'js') + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/js/snake_game.js b/js/snake_game.js new file mode 100644 index 0000000..349b1f0 --- /dev/null +++ b/js/snake_game.js @@ -0,0 +1,258 @@ +import { app } from "../../scripts/app.js"; + +const CELL_SIZE = 8; +const GRID_SIZE = 20; +const CANVAS_SIZE = CELL_SIZE * GRID_SIZE; +const GAME_SPEED = 150; // ms per frame, Nokia 3210 speed + +class SnakeGame { + constructor(node) { + this.node = node; + this.reset(); + } + + reset() { + this.snake = [{ x: 10, y: 10 }]; + this.direction = { x: 1, y: 0 }; + this.nextDirection = { x: 1, y: 0 }; + this.food = this.spawnFood(); + this.score = 0; + this.gameOver = false; + this.paused = false; + } + + spawnFood() { + let food; + do { + food = { + x: Math.floor(Math.random() * GRID_SIZE), + y: Math.floor(Math.random() * GRID_SIZE) + }; + } while (this.snake.some(segment => segment.x === food.x && segment.y === food.y)); + return food; + } + + update() { + if (this.gameOver || this.paused) return; + + // Update direction + this.direction = this.nextDirection; + + // Calculate new head position + const head = { + x: (this.snake[0].x + this.direction.x + GRID_SIZE) % GRID_SIZE, + y: (this.snake[0].y + this.direction.y + GRID_SIZE) % GRID_SIZE + }; + + // Check self collision + if (this.snake.some(segment => segment.x === head.x && segment.y === head.y)) { + this.gameOver = true; + return; + } + + this.snake.unshift(head); + + // Check food collision + if (head.x === this.food.x && head.y === this.food.y) { + this.score++; + this.food = this.spawnFood(); + } else { + this.snake.pop(); + } + } + + changeDirection(newDir) { + // Prevent 180 degree turns + if (newDir.x === -this.direction.x && newDir.y === -this.direction.y) { + return; + } + this.nextDirection = newDir; + } + + draw(ctx) { + // Clear canvas with Nokia-style background + ctx.fillStyle = '#c7f0d8'; + ctx.fillRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); + + // Draw grid (optional, Nokia style) + ctx.strokeStyle = '#a8d5ba'; + ctx.lineWidth = 0.5; + for (let i = 0; i <= GRID_SIZE; i++) { + ctx.beginPath(); + ctx.moveTo(i * CELL_SIZE, 0); + ctx.lineTo(i * CELL_SIZE, CANVAS_SIZE); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, i * CELL_SIZE); + ctx.lineTo(CANVAS_SIZE, i * CELL_SIZE); + ctx.stroke(); + } + + // Draw snake + ctx.fillStyle = '#000000'; + this.snake.forEach(segment => { + ctx.fillRect( + segment.x * CELL_SIZE + 1, + segment.y * CELL_SIZE + 1, + CELL_SIZE - 2, + CELL_SIZE - 2 + ); + }); + + // Draw food + ctx.fillStyle = '#000000'; + ctx.fillRect( + this.food.x * CELL_SIZE + 2, + this.food.y * CELL_SIZE + 2, + CELL_SIZE - 4, + CELL_SIZE - 4 + ); + + // Draw score + ctx.fillStyle = '#000000'; + ctx.font = '10px monospace'; + ctx.fillText(`Score: ${this.score}`, 5, CANVAS_SIZE - 5); + + // Draw game over + if (this.gameOver) { + ctx.fillStyle = 'rgba(199, 240, 216, 0.8)'; + ctx.fillRect(0, CANVAS_SIZE / 2 - 20, CANVAS_SIZE, 40); + ctx.fillStyle = '#000000'; + ctx.font = 'bold 12px monospace'; + ctx.textAlign = 'center'; + ctx.fillText('GAME OVER', CANVAS_SIZE / 2, CANVAS_SIZE / 2 - 5); + ctx.font = '10px monospace'; + ctx.fillText('Press SPACE', CANVAS_SIZE / 2, CANVAS_SIZE / 2 + 10); + ctx.textAlign = 'left'; + } + } +} + +app.registerExtension({ + name: "Comfy.SnakeGame", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "SnakeGame") { + const onNodeCreated = nodeType.prototype.onNodeCreated; + + nodeType.prototype.onNodeCreated = function() { + // Call original onNodeCreated first + const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + + // Set node properties early to avoid conflicts + this.properties = this.properties || {}; + this.serialize_widgets = false; + this.widgets = this.widgets || []; + + // Prevent re-initialization + if (this.snakeGameInitialized) { + return result; + } + this.snakeGameInitialized = true; + + // Create canvas + const canvas = document.createElement('canvas'); + canvas.width = CANVAS_SIZE; + canvas.height = CANVAS_SIZE; + canvas.style.width = `${CANVAS_SIZE * 2}px`; + canvas.style.height = `${CANVAS_SIZE * 2}px`; + canvas.style.imageRendering = 'pixelated'; + + const ctx = canvas.getContext('2d'); + + // Initialize game + const game = new SnakeGame(this); + this.snakeGame = game; + + // Game loop + let lastUpdate = Date.now(); + let animationId = null; + const gameLoop = () => { + const now = Date.now(); + if (now - lastUpdate >= GAME_SPEED) { + game.update(); + game.draw(ctx); + lastUpdate = now; + } + animationId = requestAnimationFrame(gameLoop); + }; + gameLoop(); + + // Store animation ID for cleanup + this.snakeGameAnimationId = animationId; + + // Set node size - account for title bar (~30px) and padding + const nodeWidth = CANVAS_SIZE * 2 + 20; + const nodeHeight = CANVAS_SIZE * 2 + 60; // Extra space for title and padding + this.size = [nodeWidth, nodeHeight]; + this.setSize([nodeWidth, nodeHeight]); + + // Add canvas widget + const widget = this.addDOMWidget("snake_canvas", "canvas", canvas, { + serialize: false, + hideOnZoom: false + }); + + // Handle keyboard input + const handleKeyDown = (e) => { + if (!this.snakeGame) return; + + switch(e.key) { + case 'ArrowUp': + case 'w': + case 'W': + e.preventDefault(); + this.snakeGame.changeDirection({ x: 0, y: -1 }); + break; + case 'ArrowDown': + case 's': + case 'S': + e.preventDefault(); + this.snakeGame.changeDirection({ x: 0, y: 1 }); + break; + case 'ArrowLeft': + case 'a': + case 'A': + e.preventDefault(); + this.snakeGame.changeDirection({ x: -1, y: 0 }); + break; + case 'ArrowRight': + case 'd': + case 'D': + e.preventDefault(); + this.snakeGame.changeDirection({ x: 1, y: 0 }); + break; + case ' ': + e.preventDefault(); + if (this.snakeGame.gameOver) { + this.snakeGame.reset(); + } else { + this.snakeGame.paused = !this.snakeGame.paused; + } + break; + } + }; + + // Add event listeners when node is selected + canvas.tabIndex = 0; + canvas.addEventListener('keydown', handleKeyDown); + canvas.addEventListener('click', () => canvas.focus()); + + // Auto-focus when created + setTimeout(() => canvas.focus(), 100); + + return result; + }; + + // Cleanup on removal + const onRemoved = nodeType.prototype.onRemoved; + nodeType.prototype.onRemoved = function() { + if (this.snakeGameAnimationId) { + cancelAnimationFrame(this.snakeGameAnimationId); + } + if (onRemoved) { + onRemoved.apply(this, arguments); + } + }; + } + } +}); \ No newline at end of file diff --git a/nodes/__init__.py b/nodes/__init__.py index 6a17864..829d891 100644 --- a/nodes/__init__.py +++ b/nodes/__init__.py @@ -3,6 +3,7 @@ from .color_stylizer import ColorStylizer from .local_llm import QueryLocalLLM, ExtractCharacterInfo from .sdxl_resolution import SdxlResolution, SdxlResolutionToDimensions from .checkpoint_names import CheckpointNames +from .snake_game import SnakeGame NODE_CLASS_MAPPINGS = { ImageGlitcher.NAME: ImageGlitcher, @@ -12,6 +13,7 @@ NODE_CLASS_MAPPINGS = { SdxlResolutionToDimensions.NAME: SdxlResolutionToDimensions, CheckpointNames.NAME: CheckpointNames, ExtractCharacterInfo.NAME: ExtractCharacterInfo, + SnakeGame.NAME: SnakeGame } # A dictionary that contains the friendly/humanly readable titles for the nodes @@ -22,7 +24,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "SdxlResolution": "SDXL Resolution", "SdxlResolutionToDimensions": "SDXL Resolution To Dimensions", "CheckPointNames": "Checkpoint Names", - "ExtractCharacterInfo": "Extract Character Info" + "ExtractCharacterInfo": "Extract Character Info", + "SnakeGame": "Snake Game 🐍" } __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/nodes/snake_game.py b/nodes/snake_game.py new file mode 100644 index 0000000..f035f79 --- /dev/null +++ b/nodes/snake_game.py @@ -0,0 +1,17 @@ +class SnakeGame: + NAME = "SnakeGame" + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {} + + RETURN_TYPES = () + FUNCTION = "play_snake" + OUTPUT_NODE = True + CATEGORY = "CrasH Utils/Games" + + def play_snake(self): + return () \ No newline at end of file