Add CCW rotation and refine HUD

- add rotate_cw/rotate_ccw bindings and display updates in options/controls

- refine HUD layout/labels, borders, spacing, and correct lines_cleared output

- support customizable tetromino/background colors and background images in live UI
This commit is contained in:
Erik W.
2026-01-02 22:29:44 -05:00
parent bf1e338d74
commit 28436a0cbc
3 changed files with 505 additions and 43 deletions
+9 -1
View File
@@ -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`
+352 -20
View File
@@ -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);
+144 -22
View File
@@ -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),)