Walldye

Pixel invaders

inspired by Taito, Space Invaders1

A *Space Invaders* formation, where the aliens speed up as fewer remain because the hardware has less to redraw.

Made with Claude Opus 5.5

Technique
pixel art
Inspired by
arcade games
Shape
Any screen
Added
27 September 2026

Export

Shape
cropped from 16:9
Crop
Size
Format

Colors

  • #1C1B1Abackground
  • #DAD8CEforeground
  • #CF6A4Caccent

Notes

Aliens that break formation and dive at the player came a year after Space Invaders, in Namco’s Galaxian.

Sources

  1. Taito, Space Invaders, 1978. ↑
  2. Namco, Galaxian, 1979.

Source code

wallpapers/pixel-invaders/design.py, 134 lines

"""A Space Invaders formation with one crab diving out of it, drawn on a single pixel-art grid."""

import math

import numpy as np
from numpy.typing import NDArray

from walldye import (
    ACCENT,
    ACCENT_2,
    ACCENT_3,
    ACCENT_4,
    ACCENT_5,
    BG_ALT,
    UI,
    Canvas,
    Rng,
    design,
    mix,
)
from walldye.pixel import Pixels

PX = 6  # one sprite pixel; every position below is in these cells
DX, DY = 16, 10  # formation pitch

SQUID = (
    "...##...|..####..|.######.|##.##.##|########|..#..#..|.#.##.#.|#.#..#.#",
    "...##...|..####..|.######.|##.##.##|########|.#.##.#.|#......#|.#....#.",
)
CRAB = (
    "..#.....#..|...#...#...|..#######..|.##.###.##.|###########|#.#######.#|#.#.....#.#|...##.##...",
    "..#.....#..|#..#...#..#|#.#######.#|###.###.###|###########|.#########.|..#.....#..|.#.......#.",
)
OCTO = (
    "....####....|.##########.|############|###..##..###|############|...##..##...|..##.##.##..|##........##",
    "....####....|.##########.|############|###..##..###|############|..###..###..|.##..##..##.|..##....##..",
)
CANNON = "......#......|.....###.....|.....###.....|.###########.|#############|#############|#############|#############"
ROWS = (SQUID, CRAB, CRAB, OCTO, OCTO)

PALETTE = (
    None,
    BG_ALT,
    mix(BG_ALT, UI, 0.5),
    mix(BG_ALT, UI, 0.6),
    UI,
    ACCENT_5,
    ACCENT_4,
    ACCENT_3,
    ACCENT_2,
    ACCENT,
)
FAR, MID, SHIELD, NEAR, DIVER = 1, 2, 3, 4, 9
ROW_TONE = (FAR, MID, MID, NEAR, NEAR)  # the formation fades toward the back
TRAIL = (5, 6, 7, 8)  # the accent ladder along the dive, oldest dot first


def art(a: str) -> list[str]:
    return a.split("|")


def bunker(rng: Rng) -> NDArray[np.bool_]:
    """A 22x16 arch shield mask with 2-4 craters: the first above the arch (the player's
    shots), the rest bitten from the top edge (bombs)."""
    y, x = np.mgrid[0:16, 0:22]
    corner = (y < 4) & ((x < 4 - y) | (x > 17 + y))
    arch = ((y >= 12) & (x >= 6) & (x <= 15)) | ((y >= 11) & (x >= 7) & (x <= 14))
    solid = ~(corner | arch)
    for b in range(rng.randint(2, 4)):
        cx, cy, rad = (
            (rng.randrange(3, 19), rng.choice((0, 1)), rng.uniform(1.6, 2.6))
            if b
            else (rng.randrange(8, 14), 10, 2.2)
        )
        for j in range(16):
            for i in range(22):
                if math.hypot(i - cx, (j - cy) * 1.2) < rad + rng.uniform(-0.6, 0.6):
                    solid[j, i] = False
    return solid


def trail(x: int, y: int, dive: int) -> list[tuple[int, int]]:
    """The peel-out path as 8-way moves ending `dive` steps straight down: 3 cells per axis
    step and 2 per axis on a diagonal, so the dots read evenly spaced."""
    dots = [(x, y)]
    for dx, dy, n in ((1, 0, 5), (1, -1, 3), (1, 0, 9), (1, 1, 3), (0, 1, dive), (-1, 1, 3)):
        step = 2 if dx and dy else 3
        for _ in range(n):
            x, y = x + dx * step, y + dy * step
            dots.append((x, y))
    return dots


@design(aspects="any")
def draw(s: Canvas) -> None:
    px = Pixels(s.w // PX, s.h // PX, PALETTE)
    w, h = px.cols, px.rows

    # 8 columns at 16:9, 11 (the arcade's count) on ultrawide, 5 on a phone; the formation and
    # the diver's sideways excursion together sit a third of the way into the free width
    cols = max(5, min(11, round(w / 40)))
    reach = (cols - 1) * DX + 60
    x0 = round((w - reach) * 0.34)
    y0 = round(h * (1 / 6 if s.landscape else 0.2))
    gap = (1, cols - 1)  # (row, col) the diver left
    slot = (x0 + gap[1] * DX + 5, y0 + gap[0] * DY + 3)

    for j, frames in enumerate(ROWS):
        for c in range(cols):
            if (j, c) != gap:
                a = art(frames[(j + c) % 2])
                px.stamp(a, x0 + c * DX + (12 - len(a[0])) // 2, y0 + j * DY, {"#": ROW_TONE[j]})

    # the player's end sits on the bottom edge; shields repeat out from the cannon while they
    # fit, with the cannon's own slot left open for the shot
    floor, margin = h - 19, 20
    rng = s.rng(4)
    for k in (-3, -2, -1, 1):
        bx = slot[0] + k * 50 - 11
        if margin <= bx and bx + 22 <= w - margin:
            px.grid[floor - 32 : floor - 16, bx : bx + 22][bunker(rng)] = SHIELD
    px.stamp(art(CANNON), slot[0] - 6, floor - 10, {"#": NEAR})
    px.grid[floor - 24 : floor - 21, slot[0]] = NEAR  # the shot, up the diver's empty column
    px.grid[floor, margin : w - margin] = FAR

    # the crab lands a little under halfway down on every screen, so tall ones get a longer dive;
    # its top row ends up 10 cells below the slot plus the dive's 3-cell steps
    dive = max(10, round((h * 0.46 - slot[1] - 10) / 3))
    dots = trail(*slot, dive)
    for k, (x, y) in enumerate(dots):
        px.grid[y, x] = TRAIL[k * len(TRAIL) // len(dots)]
    px.stamp(art(CRAB[0]), dots[-1][0] - 8, dots[-1][1] + 4, {"#": DIVER})

    px.draw(s, PX)

Run it yourself

$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render pixel-invaders --theme fireproof -o pixel-invaders-fireproof-16x9.svg