Tetris
This commit is contained in:
@@ -0,0 +1,455 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
const COLS = 10;
|
||||
const ROWS = 20;
|
||||
const BLOCK_SIZE = 20;
|
||||
const CANVAS_WIDTH = COLS * BLOCK_SIZE + 160; // Extra space for sidebar
|
||||
const CANVAS_HEIGHT = ROWS * BLOCK_SIZE + 40;
|
||||
|
||||
const COLORS = {
|
||||
I: '#00f0f0',
|
||||
O: '#f0f000',
|
||||
T: '#a000f0',
|
||||
S: '#00f000',
|
||||
Z: '#f00000',
|
||||
J: '#0000f0',
|
||||
L: '#f0a000',
|
||||
empty: '#000000',
|
||||
grid: '#333333'
|
||||
};
|
||||
|
||||
const SHAPES = {
|
||||
I: [[1, 1, 1, 1]],
|
||||
O: [[1, 1], [1, 1]],
|
||||
T: [[0, 1, 0], [1, 1, 1]],
|
||||
S: [[0, 1, 1], [1, 1, 0]],
|
||||
Z: [[1, 1, 0], [0, 1, 1]],
|
||||
J: [[1, 0, 0], [1, 1, 1]],
|
||||
L: [[0, 0, 1], [1, 1, 1]]
|
||||
};
|
||||
|
||||
class TetrisGame {
|
||||
constructor(node) {
|
||||
this.node = node;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.board = Array(ROWS).fill(null).map(() => Array(COLS).fill(null));
|
||||
this.score = 0;
|
||||
this.level = 1;
|
||||
this.lines = 0;
|
||||
this.gameOver = false;
|
||||
this.paused = false;
|
||||
this.dropCounter = 0;
|
||||
this.dropInterval = 1000;
|
||||
this.lastTime = 0;
|
||||
|
||||
this.currentPiece = this.createPiece();
|
||||
this.nextPiece = this.createPiece();
|
||||
}
|
||||
|
||||
createPiece() {
|
||||
const types = Object.keys(SHAPES);
|
||||
const type = types[Math.floor(Math.random() * types.length)];
|
||||
return {
|
||||
type: type,
|
||||
shape: SHAPES[type],
|
||||
x: Math.floor(COLS / 2) - Math.floor(SHAPES[type][0].length / 2),
|
||||
y: 0
|
||||
};
|
||||
}
|
||||
|
||||
collide(piece = this.currentPiece) {
|
||||
const shape = piece.shape;
|
||||
for (let y = 0; y < shape.length; y++) {
|
||||
for (let x = 0; x < shape[y].length; x++) {
|
||||
if (shape[y][x]) {
|
||||
const boardX = piece.x + x;
|
||||
const boardY = piece.y + y;
|
||||
|
||||
if (boardX < 0 || boardX >= COLS || boardY >= ROWS) {
|
||||
return true;
|
||||
}
|
||||
if (boardY >= 0 && this.board[boardY][boardX]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
merge() {
|
||||
this.currentPiece.shape.forEach((row, y) => {
|
||||
row.forEach((value, x) => {
|
||||
if (value) {
|
||||
const boardY = this.currentPiece.y + y;
|
||||
const boardX = this.currentPiece.x + x;
|
||||
if (boardY >= 0) {
|
||||
this.board[boardY][boardX] = this.currentPiece.type;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
rotate() {
|
||||
const rotated = this.currentPiece.shape[0].map((_, i) =>
|
||||
this.currentPiece.shape.map(row => row[i]).reverse()
|
||||
);
|
||||
|
||||
const previousShape = this.currentPiece.shape;
|
||||
this.currentPiece.shape = rotated;
|
||||
|
||||
// Wall kick
|
||||
let offset = 0;
|
||||
while (this.collide()) {
|
||||
this.currentPiece.x += offset;
|
||||
offset = -(offset + (offset > 0 ? 1 : -1));
|
||||
if (offset > this.currentPiece.shape[0].length) {
|
||||
this.currentPiece.shape = previousShape;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
move(dir) {
|
||||
this.currentPiece.x += dir;
|
||||
if (this.collide()) {
|
||||
this.currentPiece.x -= dir;
|
||||
}
|
||||
}
|
||||
|
||||
drop() {
|
||||
this.currentPiece.y++;
|
||||
if (this.collide()) {
|
||||
this.currentPiece.y--;
|
||||
this.merge();
|
||||
this.clearLines();
|
||||
this.currentPiece = this.nextPiece;
|
||||
this.nextPiece = this.createPiece();
|
||||
|
||||
if (this.collide()) {
|
||||
this.gameOver = true;
|
||||
}
|
||||
}
|
||||
this.dropCounter = 0;
|
||||
}
|
||||
|
||||
hardDrop() {
|
||||
while (!this.collide()) {
|
||||
this.currentPiece.y++;
|
||||
}
|
||||
this.currentPiece.y--;
|
||||
this.drop();
|
||||
}
|
||||
|
||||
clearLines() {
|
||||
let linesCleared = 0;
|
||||
|
||||
outer: for (let y = ROWS - 1; y >= 0; y--) {
|
||||
for (let x = 0; x < COLS; x++) {
|
||||
if (!this.board[y][x]) {
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
// Line is full
|
||||
this.board.splice(y, 1);
|
||||
this.board.unshift(Array(COLS).fill(null));
|
||||
linesCleared++;
|
||||
y++; // Check this row again
|
||||
}
|
||||
|
||||
if (linesCleared > 0) {
|
||||
this.lines += linesCleared;
|
||||
this.score += [0, 40, 100, 300, 1200][linesCleared] * this.level;
|
||||
this.level = Math.floor(this.lines / 10) + 1;
|
||||
this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
if (this.gameOver || this.paused) return;
|
||||
|
||||
this.dropCounter += deltaTime;
|
||||
if (this.dropCounter > this.dropInterval) {
|
||||
this.drop();
|
||||
}
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
// Clear canvas
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||
|
||||
// Draw title
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 16px monospace';
|
||||
ctx.fillText('TETRIS', 10, 25);
|
||||
|
||||
// Draw board border
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(9, 39, COLS * BLOCK_SIZE + 2, ROWS * BLOCK_SIZE + 2);
|
||||
|
||||
// Draw grid
|
||||
ctx.strokeStyle = COLORS.grid;
|
||||
ctx.lineWidth = 1;
|
||||
for (let y = 0; y <= ROWS; y++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(10, 40 + y * BLOCK_SIZE);
|
||||
ctx.lineTo(10 + COLS * BLOCK_SIZE, 40 + y * BLOCK_SIZE);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let x = 0; x <= COLS; x++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(10 + x * BLOCK_SIZE, 40);
|
||||
ctx.lineTo(10 + x * BLOCK_SIZE, 40 + ROWS * BLOCK_SIZE);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw board
|
||||
this.board.forEach((row, y) => {
|
||||
row.forEach((value, x) => {
|
||||
if (value) {
|
||||
ctx.fillStyle = COLORS[value];
|
||||
ctx.fillRect(10 + x * BLOCK_SIZE, 40 + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(10 + x * BLOCK_SIZE, 40 + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Draw current piece
|
||||
if (this.currentPiece) {
|
||||
ctx.fillStyle = COLORS[this.currentPiece.type];
|
||||
this.currentPiece.shape.forEach((row, y) => {
|
||||
row.forEach((value, x) => {
|
||||
if (value) {
|
||||
const drawX = 10 + (this.currentPiece.x + x) * BLOCK_SIZE;
|
||||
const drawY = 40 + (this.currentPiece.y + y) * BLOCK_SIZE;
|
||||
ctx.fillRect(drawX, drawY, BLOCK_SIZE, BLOCK_SIZE);
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(drawX, drawY, BLOCK_SIZE, BLOCK_SIZE);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Draw sidebar
|
||||
const sidebarX = COLS * BLOCK_SIZE + 30;
|
||||
|
||||
// Score
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = '12px monospace';
|
||||
ctx.fillText('SCORE', sidebarX, 60);
|
||||
ctx.font = 'bold 16px monospace';
|
||||
ctx.fillText(this.score.toString(), sidebarX, 80);
|
||||
|
||||
// Level
|
||||
ctx.font = '12px monospace';
|
||||
ctx.fillText('LEVEL', sidebarX, 110);
|
||||
ctx.font = 'bold 16px monospace';
|
||||
ctx.fillText(this.level.toString(), sidebarX, 130);
|
||||
|
||||
// Lines
|
||||
ctx.font = '12px monospace';
|
||||
ctx.fillText('LINES', sidebarX, 160);
|
||||
ctx.font = 'bold 16px monospace';
|
||||
ctx.fillText(this.lines.toString(), sidebarX, 180);
|
||||
|
||||
// Next piece
|
||||
ctx.font = '12px monospace';
|
||||
ctx.fillText('NEXT', sidebarX, 220);
|
||||
if (this.nextPiece) {
|
||||
const offsetX = sidebarX + 10;
|
||||
const offsetY = 230;
|
||||
ctx.fillStyle = COLORS[this.nextPiece.type];
|
||||
this.nextPiece.shape.forEach((row, y) => {
|
||||
row.forEach((value, x) => {
|
||||
if (value) {
|
||||
ctx.fillRect(offsetX + x * BLOCK_SIZE, offsetY + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(offsetX + x * BLOCK_SIZE, offsetY + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Controls
|
||||
ctx.fillStyle = '#888888';
|
||||
ctx.font = '10px monospace';
|
||||
ctx.fillText('← → Move', sidebarX, 320);
|
||||
ctx.fillText('↑ Rotate', sidebarX, 335);
|
||||
ctx.fillText('↓ Soft Drop', sidebarX, 350);
|
||||
ctx.fillText('Space Hard Drop', sidebarX, 365);
|
||||
ctx.fillText('P Pause', sidebarX, 380);
|
||||
|
||||
// Game over
|
||||
if (this.gameOver) {
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
||||
ctx.fillRect(10, CANVAS_HEIGHT / 2 - 40, COLS * BLOCK_SIZE, 80);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 20px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('GAME OVER', 10 + COLS * BLOCK_SIZE / 2, CANVAS_HEIGHT / 2 - 10);
|
||||
ctx.font = '12px monospace';
|
||||
ctx.fillText('Press SPACE to restart', 10 + COLS * BLOCK_SIZE / 2, CANVAS_HEIGHT / 2 + 15);
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
|
||||
// Paused
|
||||
if (this.paused && !this.gameOver) {
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
||||
ctx.fillRect(10, CANVAS_HEIGHT / 2 - 30, COLS * BLOCK_SIZE, 60);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 20px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('PAUSED', 10 + COLS * BLOCK_SIZE / 2, CANVAS_HEIGHT / 2 + 5);
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.TetrisGame",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "TetrisGame") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||
|
||||
this.properties = this.properties || {};
|
||||
this.serialize_widgets = false;
|
||||
this.widgets = this.widgets || [];
|
||||
|
||||
if (this.tetrisGameInitialized) {
|
||||
return result;
|
||||
}
|
||||
this.tetrisGameInitialized = true;
|
||||
|
||||
// Create canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = CANVAS_WIDTH;
|
||||
canvas.height = CANVAS_HEIGHT;
|
||||
canvas.style.width = `${CANVAS_WIDTH}px`;
|
||||
canvas.style.height = `${CANVAS_HEIGHT}px`;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Initialize game
|
||||
const game = new TetrisGame(this);
|
||||
this.tetrisGame = game;
|
||||
|
||||
// Game loop
|
||||
let lastTime = 0;
|
||||
let animationId = null;
|
||||
const gameLoop = (time = 0) => {
|
||||
const deltaTime = time - lastTime;
|
||||
lastTime = time;
|
||||
|
||||
game.update(deltaTime);
|
||||
game.draw(ctx);
|
||||
|
||||
animationId = requestAnimationFrame(gameLoop);
|
||||
};
|
||||
gameLoop();
|
||||
|
||||
this.tetrisGameAnimationId = animationId;
|
||||
|
||||
// Set node size
|
||||
const nodeWidth = CANVAS_WIDTH + 20;
|
||||
const nodeHeight = CANVAS_HEIGHT + 60;
|
||||
this.size = [nodeWidth, nodeHeight];
|
||||
this.setSize([nodeWidth, nodeHeight]);
|
||||
|
||||
// Add canvas widget
|
||||
const widget = this.addDOMWidget("tetris_canvas", "canvas", canvas, {
|
||||
serialize: false,
|
||||
hideOnZoom: false
|
||||
});
|
||||
|
||||
// Handle keyboard input
|
||||
const handleKeyDown = (e) => {
|
||||
if (!this.tetrisGame) return;
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowLeft':
|
||||
case 'a':
|
||||
case 'A':
|
||||
e.preventDefault();
|
||||
if (!this.tetrisGame.gameOver && !this.tetrisGame.paused) {
|
||||
this.tetrisGame.move(-1);
|
||||
}
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case 'd':
|
||||
case 'D':
|
||||
e.preventDefault();
|
||||
if (!this.tetrisGame.gameOver && !this.tetrisGame.paused) {
|
||||
this.tetrisGame.move(1);
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'w':
|
||||
case 'W':
|
||||
e.preventDefault();
|
||||
if (!this.tetrisGame.gameOver && !this.tetrisGame.paused) {
|
||||
this.tetrisGame.rotate();
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case 's':
|
||||
case 'S':
|
||||
e.preventDefault();
|
||||
if (!this.tetrisGame.gameOver && !this.tetrisGame.paused) {
|
||||
this.tetrisGame.drop();
|
||||
}
|
||||
break;
|
||||
case ' ':
|
||||
e.preventDefault();
|
||||
if (this.tetrisGame.gameOver) {
|
||||
this.tetrisGame.reset();
|
||||
} else if (!this.tetrisGame.paused) {
|
||||
this.tetrisGame.hardDrop();
|
||||
}
|
||||
break;
|
||||
case 'p':
|
||||
case 'P':
|
||||
e.preventDefault();
|
||||
if (!this.tetrisGame.gameOver) {
|
||||
this.tetrisGame.paused = !this.tetrisGame.paused;
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
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.tetrisGameAnimationId) {
|
||||
cancelAnimationFrame(this.tetrisGameAnimationId);
|
||||
}
|
||||
if (onRemoved) {
|
||||
onRemoved.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
+5
-2
@@ -5,6 +5,7 @@ from .sdxl_resolution import SdxlResolution, SdxlResolutionToDimensions
|
||||
from .checkpoint_names import CheckpointNames
|
||||
from .snake_game import SnakeGame
|
||||
from .dino_game import DinoGame
|
||||
from .tetris_game import TetrisGame
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
ImageGlitcher.NAME: ImageGlitcher,
|
||||
@@ -15,7 +16,8 @@ NODE_CLASS_MAPPINGS = {
|
||||
CheckpointNames.NAME: CheckpointNames,
|
||||
ExtractCharacterInfo.NAME: ExtractCharacterInfo,
|
||||
SnakeGame.NAME: SnakeGame,
|
||||
DinoGame.NAME: DinoGame
|
||||
DinoGame.NAME: DinoGame,
|
||||
TetrisGame.NAME: TetrisGame
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -27,7 +29,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"CheckPointNames": "Checkpoint Names",
|
||||
"ExtractCharacterInfo": "Extract Character Info",
|
||||
"SnakeGame": "Snake Game 🐍",
|
||||
"DinoGame": "Dino Game 🦖"
|
||||
"DinoGame": "Dino Game 🦖",
|
||||
"TetrisGame": "Tetris 🟦"
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
@@ -0,0 +1,17 @@
|
||||
class TetrisGame:
|
||||
NAME = "TetrisGame"
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "play_tetris"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "CrasH Utils/Games"
|
||||
|
||||
def play_tetris(self):
|
||||
return ()
|
||||
Reference in New Issue
Block a user