diff --git a/README.md b/README.md index cc45a17..12aba48 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Main gameplay node. **Inputs** - `tetrinode_options` (TETRINODE_OPTIONS): Custom keybindings for moving left/right, rotating the piece, dropping the piece, play/pause, and reset - `seed` (INT): Seed used for piece sequence +- `background_image` (IMAGE, optional): Background image for the game board (scaled to cover, then center-cropped) **Outputs** - `image` (IMAGE): current board @@ -40,6 +41,12 @@ Optional node that provides keybindings. ![TetriNode Options node UI](docs/images/options.png) +**Inputs** + +- `move_left`, `move_right`, `rotate_cw`, `rotate_ccw`, `drop`, `reset`, `pause`: Key bindings +- `color_i`, `color_j`, `color_l`, `color_o`, `color_s`, `color_t`, `color_z`: Hex colors for each tetromino (e.g. `#55D6FF`) +- `background_color`: Hex color for the board background (ignored when a background image is connected) + **Outputs** - `tetrinode_options` (TETRINODE_OPTIONS): Custom keybindings for moving left/right, rotating the piece, dropping the piece, play/pause, and reset @@ -50,7 +57,8 @@ Displayed inside the node UI and reflected in the game input handler. - Move Left: `A` - Move Right: `D` -- Rotate: `W` +- Rotate CW: `W` +- Rotate CCW: `Q` - Drop: `S` - Reset: `R` - Pause: `P` diff --git a/js/tetris_live.js b/js/tetris_live.js index 4a4bb99..315c832 100644 --- a/js/tetris_live.js +++ b/js/tetris_live.js @@ -187,7 +187,6 @@ function stepDown(state) { state.board = result.board; if (result.cleared > 0) { state.lines += result.cleared; - state.score += result.cleared; } spawnNext(state); } @@ -340,6 +339,173 @@ function drawBlockSized(ctx, x, y, size, color) { ctx.fillRect(x, y, size - 1, size - 1); } +function getInputDataByName(node, name) { + const idx = node?.inputs?.findIndex((inp) => inp?.name === name); + if (idx == null || idx < 0) return null; + if (typeof node.getInputData !== "function") return null; + return node.getInputData(idx); +} + +function coerceImageSource(value) { + if (!value) return null; + if ( + value instanceof HTMLImageElement || + value instanceof HTMLCanvasElement || + value instanceof ImageBitmap || + value instanceof OffscreenCanvas || + value instanceof HTMLVideoElement + ) { + return value; + } + if (value.image) { + const img = value.image; + if ( + img instanceof HTMLImageElement || + img instanceof HTMLCanvasElement || + img instanceof ImageBitmap || + img instanceof OffscreenCanvas + ) { + return img; + } + } + return null; +} + +function toByteArray(data) { + if (!data) return null; + if (data instanceof Uint8ClampedArray) return data; + let array = data; + if (data instanceof Uint8Array) { + return new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength); + } + if (data instanceof Float32Array || data instanceof Float64Array) { + array = Array.from(data); + } + if (Array.isArray(array)) { + let max = 0; + for (let i = 0; i < array.length; i += 1) { + const v = array[i]; + if (Number.isFinite(v) && v > max) max = v; + } + const scale = max <= 1 ? 255 : 1; + const out = new Uint8ClampedArray(array.length); + for (let i = 0; i < array.length; i += 1) { + const v = array[i]; + const value = Number.isFinite(v) ? v * scale : 0; + out[i] = Math.max(0, Math.min(255, Math.round(value))); + } + return out; + } + return null; +} + +function buildCanvasFromData(value) { + if (!value || !value.data || !value.width || !value.height) return null; + const width = Math.max(1, Math.floor(value.width)); + const height = Math.max(1, Math.floor(value.height)); + const bytes = toByteArray(value.data); + if (!bytes) return null; + const expected3 = width * height * 3; + const expected4 = width * height * 4; + let pixels = bytes; + if (bytes.length === expected3) { + pixels = new Uint8ClampedArray(expected4); + for (let i = 0; i < width * height; i += 1) { + const src = i * 3; + const dst = i * 4; + pixels[dst] = bytes[src]; + pixels[dst + 1] = bytes[src + 1]; + pixels[dst + 2] = bytes[src + 2]; + pixels[dst + 3] = 255; + } + } else if (bytes.length !== expected4) { + return null; + } + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + if (!ctx) return null; + const imgData = new ImageData(pixels, width, height); + ctx.putImageData(imgData, 0, 0); + return canvas; +} + +function getLinkedImageSource(node, name) { + const resolved = getInputLink(node, name); + if (!resolved) return null; + const { origin, link } = resolved; + const slot = link?.origin_slot ?? 0; + if (!origin) return null; + if (Array.isArray(origin.imgs)) { + const candidate = origin.imgs[slot] || origin.imgs[0]; + const source = coerceImageSource(candidate); + if (source) return source; + } + if (Array.isArray(origin.images)) { + const candidate = origin.images[slot] || origin.images[0]; + const source = coerceImageSource(candidate); + if (source) return source; + } + const direct = coerceImageSource(origin.image || origin.img || origin._img || origin._image); + if (direct) return direct; + return null; +} + +function getBackgroundSource(node) { + const raw = getInputDataByName(node, "background_image"); + let value = Array.isArray(raw) ? raw[0] : raw; + let source = null; + if (!value) { + source = getLinkedImageSource(node, "background_image"); + value = source; + } + if (!value && !source) return null; + if (!node.__tetrisBg) node.__tetrisBg = {}; + if (node.__tetrisBg.value === value && node.__tetrisBg.source) { + return node.__tetrisBg.source; + } + if (!source) { + source = coerceImageSource(value); + } + if (!source) { + source = buildCanvasFromData(value); + } + node.__tetrisBg = { value, source }; + return source || null; +} + +function drawBoardBackground(ctx, source, boardX, boardY, boardW, boardH, fallbackColor) { + if (!source) { + ctx.fillStyle = fallbackColor || COLORS.X; + ctx.fillRect(boardX, boardY, boardW, boardH); + return; + } + const srcW = + source.videoWidth || + source.naturalWidth || + source.width || + source.displayWidth || + 0; + const srcH = + source.videoHeight || + source.naturalHeight || + source.height || + source.displayHeight || + 0; + if (!srcW || !srcH) { + ctx.fillStyle = fallbackColor || COLORS.X; + ctx.fillRect(boardX, boardY, boardW, boardH); + return; + } + const scale = Math.max(boardW / srcW, boardH / srcH); + const cropW = boardW / scale; + const cropH = boardH / scale; + const sx = Math.max(0, (srcW - cropW) / 2); + const sy = Math.max(0, (srcH - cropH) / 2); + ctx.drawImage(source, sx, sy, cropW, cropH, boardX, boardY, boardW, boardH); +} + function drawNode(node, ctx) { const live = node.__tetrisLive; if (!live) return; @@ -349,15 +515,19 @@ function drawNode(node, ctx) { syncSeed(live.state, node); const { state } = live; const { boardX, boardY, boardW, boardH, sideX, sideY, blockSize } = getLayout(node); + const palette = getColorPalette(node); - ctx.fillStyle = COLORS.X; - ctx.fillRect(boardX, boardY, boardW, boardH); + const bgSource = getBackgroundSource(node); + drawBoardBackground(ctx, bgSource, boardX, boardY, boardW, boardH, palette.X); + ctx.strokeStyle = "rgba(150,150,150,0.8)"; + ctx.lineWidth = 1; + ctx.strokeRect(boardX + 0.5, boardY + 0.5, boardW - 1, boardH - 1); for (let y = 0; y < GRID_H; y += 1) { for (let x = 0; x < GRID_W; x += 1) { const cell = state.board[y][x]; if (cell) { - drawBlockSized(ctx, boardX + x * blockSize, boardY + y * blockSize, blockSize, COLORS[cell]); + drawBlockSized(ctx, boardX + x * blockSize, boardY + y * blockSize, blockSize, palette[cell]); } } } @@ -369,16 +539,27 @@ function drawNode(node, ctx) { boardX + x * blockSize, boardY + y * blockSize, blockSize, - COLORS[state.piece.shape], + palette[state.piece.shape], ); } } const scoreFontSize = Math.max(10, Math.floor(blockSize * 0.55)); ctx.fillStyle = COLORS.Text; + const lineGap = Math.floor(scoreFontSize * 0.6); + const linesLabelY = sideY + scoreFontSize + 1; + const linesValueY = linesLabelY + scoreFontSize + 2; + const scoreLabelY = linesValueY + lineGap + scoreFontSize; + const scoreValueY = scoreLabelY + scoreFontSize + 2; + ctx.font = `bold ${scoreFontSize}px sans-serif`; + ctx.fillText("Lines Cleared:", sideX, linesLabelY); + ctx.fillText("Score:", sideX, scoreLabelY); ctx.font = `${scoreFontSize}px sans-serif`; - ctx.fillText(`Score: ${state.score}`, sideX, sideY + scoreFontSize + 1); - ctx.fillText("Next:", sideX, sideY + scoreFontSize * 2 + 5); + ctx.fillText(`${state.lines}`, sideX, linesValueY); + ctx.fillText(`${state.score}`, sideX, scoreValueY); + const nextLabelY = scoreValueY + scoreFontSize + lineGap * 2; + ctx.font = `bold ${scoreFontSize}px sans-serif`; + ctx.fillText("Next Piece:", sideX, nextLabelY); const preview = SHAPES[state.nextShape][0]; let minX = 99; @@ -402,29 +583,41 @@ function drawNode(node, ctx) { drawBlockSized( ctx, sideX + gx * blockSize, - sideY + scoreFontSize * 2 + 13 + gy * blockSize, + nextLabelY + 10 + gy * blockSize, blockSize, - COLORS[state.nextShape], + palette[state.nextShape], ); } } - const infoY = sideY + scoreFontSize * 2 + 13 + PREVIEW_GRID * blockSize + PADDING; + const previewY = nextLabelY + 10; + ctx.strokeStyle = "rgba(150,150,150,0.8)"; + ctx.lineWidth = 1; + ctx.strokeRect(sideX + 0.5, previewY + 0.5, PREVIEW_GRID * blockSize - 1, PREVIEW_GRID * blockSize - 1); + const bindings = getControlBindings(node); const infoLines = [ "Controls:", - `Move Left: ${bindings.moveLeft}`, - `Move Right: ${bindings.moveRight}`, - `Rotate: ${bindings.rotate}`, - `Drop: ${bindings.drop}`, - `Reset: ${bindings.reset}`, - `Pause: ${bindings.pause}`, + `Move Left - ${bindings.moveLeft.toUpperCase()}`, + `Move Right - ${bindings.moveRight.toUpperCase()}`, + `Rotate CW - ${bindings.rotateCw.toUpperCase()}`, + `Rotate CCW - ${bindings.rotateCcw.toUpperCase()}`, + `Drop - ${bindings.drop.toUpperCase()}`, + `Reset - ${bindings.reset.toUpperCase()}`, + `Pause - ${bindings.pause.toUpperCase()}`, ]; const fontSize = Math.max(10, Math.floor(blockSize * 0.55)); const lineHeight = fontSize + 3; + const infoBlockHeight = infoLines.length * lineHeight; + const infoY = Math.max( + previewY + PREVIEW_GRID * blockSize + PADDING * 1.4, + boardY + boardH - infoBlockHeight + ); ctx.fillStyle = COLORS.Text; + ctx.font = `bold ${fontSize}px sans-serif`; + ctx.fillText(infoLines[0], sideX, infoY); ctx.font = `${fontSize}px sans-serif`; - for (let i = 0; i < infoLines.length; i += 1) { + for (let i = 1; i < infoLines.length; i += 1) { ctx.fillText(infoLines[i], sideX, infoY + i * lineHeight); } @@ -450,6 +643,18 @@ function drawNode(node, ctx) { } } +function ensureBackgroundUpdater(node) { + const live = node.__tetrisLive; + if (!live || live.bgTimer) return; + live.bgTimer = setInterval(() => { + const bg = getBackgroundSource(node); + if (bg && live.bgSource !== bg) { + live.bgSource = bg; + node.setDirtyCanvas(true, true); + } + }, 250); +} + function syncSeed(state, node) { const nextSeed = getSeedValue(node, { allowRandomize: false }); if (Number.isInteger(nextSeed)) { @@ -516,6 +721,10 @@ function stopTimer(node) { clearInterval(live.state.timer); live.state.timer = null; } + if (live?.bgTimer) { + clearInterval(live.bgTimer); + live.bgTimer = null; + } } function getSelectedLiveNode(allowFallback = false) { @@ -558,9 +767,12 @@ function handleKey(event) { case bindings.moveRight: move(state, 1, 0); break; - case bindings.rotate: + case bindings.rotateCw: rotate(state, 1); break; + case bindings.rotateCcw: + rotate(state, -1); + break; case bindings.drop: hardDrop(state); break; @@ -604,6 +816,31 @@ function applyWidgetHiding(node) { } } +function ensureOptionsDivider(node) { + if (!node?.widgets) return; + if (node.__tetrisDividerAdded) return; + const pauseIndex = node.widgets.findIndex((w) => w.name === "pause"); + if (pauseIndex < 0) return; + const divider = { + type: "tetrinode_divider", + name: "divider", + draw(ctx, _, width, y, height) { + ctx.strokeStyle = "rgba(235,235,235,0.25)"; + ctx.beginPath(); + const lineY = y + 4; + ctx.moveTo(10, lineY); + ctx.lineTo(width - 10, lineY); + ctx.stroke(); + }, + computeSize(width) { + return [width, 12]; + }, + }; + node.widgets.splice(pauseIndex + 1, 0, divider); + node.__tetrisDividerAdded = true; + node.setDirtyCanvas(true, true); +} + function isValidLinkId(link) { return Number.isInteger(link) && link >= 0; } @@ -728,7 +965,8 @@ function getControlBindings(node) { const defaultBindings = { moveLeft: "a", moveRight: "d", - rotate: "w", + rotateCw: "w", + rotateCcw: "q", drop: "s", reset: "r", pause: "p", @@ -755,13 +993,102 @@ function getControlBindings(node) { return { moveLeft: lookup("move_left", defaultBindings.moveLeft), moveRight: lookup("move_right", defaultBindings.moveRight), - rotate: lookup("rotate", defaultBindings.rotate), + rotateCw: lookup("rotate_cw", defaultBindings.rotateCw), + rotateCcw: lookup("rotate_ccw", defaultBindings.rotateCcw), drop: lookup("drop", defaultBindings.drop), reset: lookup("reset", defaultBindings.reset), pause: lookup("pause", defaultBindings.pause), }; } +function parseHexColor(value) { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + if (!trimmed) return null; + const hex = trimmed.startsWith("#") ? trimmed.slice(1) : trimmed; + if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null; + const r = Number.parseInt(hex.slice(0, 2), 16); + const g = Number.parseInt(hex.slice(2, 4), 16); + const b = Number.parseInt(hex.slice(4, 6), 16); + return `rgb(${r},${g},${b})`; +} + +function coerceStringValue(value) { + if (value == null) return null; + if (typeof value === "string") return value; + if (typeof value === "number" && Number.isFinite(value)) return String(value); + if (typeof value === "object" && "value" in value) { + const inner = value.value; + if (typeof inner === "string") return inner; + if (typeof inner === "number" && Number.isFinite(inner)) return String(inner); + } + return null; +} + +function getLinkedStringValue(node, name) { + const resolved = getInputLink(node, name); + if (!resolved) return null; + const { link, origin } = resolved; + const output = origin.outputs?.[link.origin_slot]; + if (output && output.value !== undefined) { + return coerceStringValue(output.value); + } + const outputName = output?.name; + if (outputName && origin.widgets) { + const widgetIndex = origin.widgets.findIndex((w) => w.name === outputName); + if (widgetIndex >= 0) { + const widgetValue = origin.widgets[widgetIndex]?.value; + const coerced = coerceStringValue(widgetValue); + if (coerced) return coerced; + if (origin.widgets_values && origin.widgets_values.length > widgetIndex) { + const stored = coerceStringValue(origin.widgets_values[widgetIndex]); + if (stored) return stored; + } + } + } + if (origin.widgets_values && origin.widgets_values.length) { + const coerced = coerceStringValue(origin.widgets_values[0]); + if (coerced) return coerced; + } + if (origin.widgets && origin.widgets.length) { + const coerced = coerceStringValue(origin.widgets[0]?.value); + if (coerced) return coerced; + } + return null; +} + +function getColorPalette(node) { + const palette = { ...COLORS }; + const linked = getLinkedOptionsNode(node); + if (!linked?.widgets) return palette; + const lookup = (name) => { + const linkedValue = getLinkedStringValue(linked, name); + const coercedInput = coerceStringValue(linkedValue); + if (coercedInput) { + const parsed = parseHexColor(coercedInput); + if (parsed) return parsed; + } + const widget = linked.widgets.find((w) => w.name === name); + const widgetValue = coerceStringValue(widget?.value); + return parseHexColor(widgetValue); + }; + const mapping = { + color_i: "I", + color_j: "J", + color_l: "L", + color_o: "O", + color_s: "S", + color_t: "T", + color_z: "Z", + background_color: "X", + }; + for (const [key, shape] of Object.entries(mapping)) { + const parsed = lookup(key); + if (parsed) palette[shape] = parsed; + } + return palette; +} + function getLinkedOptionsNode(node) { if (!node?.inputs) return null; const input = node.inputs.find((inp) => inp?.name === "tetrinode_options"); @@ -830,6 +1157,10 @@ function applySeedAfterGenerate(node) { app.registerExtension({ name: EXT_NAME, async nodeCreated(node) { + if (node?.comfyClass === "TetriNodeOptions") { + ensureOptionsDivider(node); + return; + } if (node?.comfyClass !== NODE_CLASS) return; applyWidgetHiding(node); ensureSeedControlWidget(node); @@ -871,6 +1202,7 @@ app.registerExtension({ node.__tetrisLive.state.running = false; updateBackendState(node); ensureTimer(node); + ensureBackgroundUpdater(node); }, async setup() { window.addEventListener("keydown", handleKey, true); diff --git a/tetris_node.py b/tetris_node.py index bd3890e..3cc3312 100644 --- a/tetris_node.py +++ b/tetris_node.py @@ -118,24 +118,98 @@ def _clear_lines(board): return remaining, cleared -def _render(board, piece, block_size): +def _prepare_background(background_image, width, height): + if background_image is None: + return None + try: + img = background_image[0].detach().cpu().numpy() + except Exception: + return None + if img.ndim != 3 or img.shape[-1] < 3: + return None + img = np.clip(img[..., :3] * 255.0, 0, 255).astype(np.uint8) + pil = Image.fromarray(img, "RGB") + src_w, src_h = pil.size + if src_w <= 0 or src_h <= 0: + return None + scale = max(width / src_w, height / src_h) + new_w = max(1, int(round(src_w * scale))) + new_h = max(1, int(round(src_h * scale))) + pil = pil.resize((new_w, new_h), Image.BICUBIC) + left = max(0, (new_w - width) // 2) + top = max(0, (new_h - height) // 2) + return pil.crop((left, top, left + width, top + height)) + + +def _parse_hex_color(value): + if not isinstance(value, str): + return None + candidate = value.strip() + if candidate.startswith("#"): + candidate = candidate[1:] + if len(candidate) != 6: + return None + try: + r = int(candidate[0:2], 16) + g = int(candidate[2:4], 16) + b = int(candidate[4:6], 16) + except ValueError: + return None + return (r, g, b) + + +def _resolve_colors(options_payload): + colors = dict(COLORS) + if not options_payload: + return colors + payload = options_payload + if isinstance(options_payload, str): + try: + payload = json.loads(options_payload) + except json.JSONDecodeError: + return colors + if not isinstance(payload, dict): + return colors + mapping = { + "color_i": "I", + "color_j": "J", + "color_l": "L", + "color_o": "O", + "color_s": "S", + "color_t": "T", + "color_z": "Z", + "background_color": "X", + } + for key, shape in mapping.items(): + parsed = _parse_hex_color(payload.get(key)) + if parsed: + colors[shape] = parsed + return colors + + +def _render(board, piece, block_size, background_image=None, colors=None): width = BOARD_WIDTH * block_size height = BOARD_HEIGHT * block_size - img = Image.new("RGB", (width, height), COLORS["X"]) + palette = colors or COLORS + bg = _prepare_background(background_image, width, height) + if bg is not None: + img = bg + else: + img = Image.new("RGB", (width, height), palette["X"]) draw = ImageDraw.Draw(img) for y in range(BOARD_HEIGHT): for x in range(BOARD_WIDTH): cell = board[y][x] if cell: - color = COLORS[cell] + color = palette[cell] x0 = x * block_size y0 = y * block_size draw.rectangle([x0, y0, x0 + block_size - 2, y0 + block_size - 2], fill=color) for x, y in _piece_cells(piece): if 0 <= y < BOARD_HEIGHT and 0 <= x < BOARD_WIDTH: - color = COLORS[piece["shape"]] + color = palette[piece["shape"]] x0 = x * block_size y0 = y * block_size draw.rectangle([x0, y0, x0 + block_size - 2, y0 + block_size - 2], fill=color) @@ -144,10 +218,11 @@ def _render(board, piece, block_size): return torch.from_numpy(arr)[None, ...] -def _render_next_piece(shape, block_size): +def _render_next_piece(shape, block_size, colors=None): + palette = colors or COLORS grid = PREVIEW_GRID size = grid * block_size - img = Image.new("RGB", (size, size), COLORS["X"]) + img = Image.new("RGB", (size, size), palette["X"]) draw = ImageDraw.Draw(img) cells = SHAPES[shape][0] @@ -166,7 +241,7 @@ def _render_next_piece(shape, block_size): if 0 <= gx < grid and 0 <= gy < grid: x0 = gx * block_size y0 = gy * block_size - draw.rectangle([x0, y0, x0 + block_size - 2, y0 + block_size - 2], fill=COLORS[shape]) + draw.rectangle([x0, y0, x0 + block_size - 2, y0 + block_size - 2], fill=palette[shape]) arr = np.array(img).astype(np.float32) / 255.0 return torch.from_numpy(arr)[None, ...] @@ -290,6 +365,7 @@ class TetriNode: }, "optional": { "tetrinode_options": ("TETRINODE_OPTIONS",), + "background_image": ("IMAGE",), }, } @@ -298,7 +374,8 @@ class TetriNode: FUNCTION = "step" CATEGORY = "games" - def step(self, action, state, seed, block_size, tetrinode_options=""): + def step(self, action, state, seed, block_size, tetrinode_options="", background_image=None): + palette = _resolve_colors(tetrinode_options) if action == "new": state_obj = _default_state(seed) else: @@ -307,14 +384,26 @@ class TetriNode: if action == "sync": state_obj["seed"] = seed - image = _render(state_obj["board"], state_obj["piece"], block_size) - preview = _render_next_piece(state_obj["next_piece_shape"], block_size) - return (image, json.dumps(state_obj), 0, state_obj["score"], preview) + image = _render(state_obj["board"], state_obj["piece"], block_size, background_image, palette) + preview = _render_next_piece(state_obj["next_piece_shape"], block_size, palette) + return ( + image, + json.dumps(state_obj), + state_obj["lines_cleared_total"], + state_obj["score"], + preview, + ) if state_obj.get("game_over"): - image = _render(state_obj["board"], state_obj["piece"], block_size) - preview = _render_next_piece(state_obj["next_piece_shape"], block_size) - return (image, json.dumps(state_obj), 0, state_obj["score"], preview) + image = _render(state_obj["board"], state_obj["piece"], block_size, background_image, palette) + preview = _render_next_piece(state_obj["next_piece_shape"], block_size, palette) + return ( + image, + json.dumps(state_obj), + state_obj["lines_cleared_total"], + state_obj["score"], + preview, + ) board = state_obj["board"] piece = state_obj["piece"] @@ -355,7 +444,6 @@ class TetriNode: _lock_piece(board, piece) board, cleared = _clear_lines(board) lines_cleared = cleared - state_obj["score"] += cleared state_obj["lines_cleared_total"] += cleared piece = _spawn_piece(next_shape) next_shape = _pop_shape(state_obj) @@ -367,7 +455,6 @@ class TetriNode: _lock_piece(board, piece) board, cleared = _clear_lines(board) lines_cleared = cleared - state_obj["score"] += cleared state_obj["lines_cleared_total"] += cleared piece = _spawn_piece(next_shape) next_shape = _pop_shape(state_obj) @@ -378,9 +465,9 @@ class TetriNode: state_obj["piece"] = piece state_obj["next_piece_shape"] = next_shape - image = _render(board, piece, block_size) - preview = _render_next_piece(next_shape, block_size) - return (image, json.dumps(state_obj), lines_cleared, state_obj["score"], preview) + image = _render(board, piece, block_size, background_image, palette) + preview = _render_next_piece(next_shape, block_size, palette) + return (image, json.dumps(state_obj), state_obj["lines_cleared_total"], state_obj["score"], preview) class TetriNodeOptions: @@ -390,10 +477,19 @@ class TetriNodeOptions: "required": { "move_left": ("STRING", {"default": "A"}), "move_right": ("STRING", {"default": "D"}), - "rotate": ("STRING", {"default": "W"}), + "rotate_cw": ("STRING", {"default": "W"}), + "rotate_ccw": ("STRING", {"default": "Q"}), "drop": ("STRING", {"default": "S"}), "reset": ("STRING", {"default": "R"}), "pause": ("STRING", {"default": "P"}), + "color_i": ("STRING", {"default": "#55D6FF"}), + "color_j": ("STRING", {"default": "#5669FF"}), + "color_l": ("STRING", {"default": "#FFA74F"}), + "color_o": ("STRING", {"default": "#FFE757"}), + "color_s": ("STRING", {"default": "#7AEB84"}), + "color_t": ("STRING", {"default": "#BB80FF"}), + "color_z": ("STRING", {"default": "#FF7676"}), + "background_color": ("STRING", {"default": "#32343E"}), } } @@ -402,13 +498,39 @@ class TetriNodeOptions: FUNCTION = "build" CATEGORY = "games" - def build(self, move_left, move_right, rotate, drop, reset, pause): + def build( + self, + move_left, + move_right, + rotate_cw, + rotate_ccw, + drop, + reset, + pause, + color_i, + color_j, + color_l, + color_o, + color_s, + color_t, + color_z, + background_color, + ): payload = { "move_left": move_left, "move_right": move_right, - "rotate": rotate, + "rotate_cw": rotate_cw, + "rotate_ccw": rotate_ccw, "drop": drop, "reset": reset, "pause": pause, + "color_i": color_i, + "color_j": color_j, + "color_l": color_l, + "color_o": color_o, + "color_s": color_s, + "color_t": color_t, + "color_z": color_z, + "background_color": background_color, } return (json.dumps(payload),)