Skip to content
Back to skills

Bullet Hell Pattern Generator

ASecurity

Deterministic bullet-hell pattern math engine on the Python stdlib - spiral, aimed fan, ring, and laser-sweep emitters produce JSON frames, an ASCII snapshot, and a self-contained SVG render of one frame. Seed-based reproducibility with zero external game engine, zero MCP.

  • 2 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 27, 2026
developmentpythongo

Works with

  • mcp

Security analysis

A100/100

Scanned September 27, 2026

npx -y skills add RMAzrim/opencode-artes --skill bullet-hell-pattern-generator --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Bullet Hell Pattern Generator?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Bullet Hell Pattern Generator
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/rmazrim-bullet-hell-pattern-generator/badge)](https://www.skillsdirectory.com/skills/rmazrim-bullet-hell-pattern-generator)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: Bullet Hell Pattern Generator
description: Deterministic bullet-hell pattern math engine on the Python stdlib - spiral, aimed fan, ring, and laser-sweep emitters produce JSON frames, an ASCII snapshot, and a self-contained SVG render of one frame. Seed-based reproducibility with zero external game engine, zero MCP.
metadata:
  source: skills/bullet-hell-pattern-generator/bullet-hell-pattern-generator.md
---

# Bullet Hell Pattern Generator

Design and debug bullet-hell attack patterns with math instead of a game engine. The stdlib module simulates emitters, spawns bullets frame-by-frame, and renders results as JSON keyframes, an ASCII snapshot, and a standalone SVG image — so you can iterate on a pattern's rhythm and density without Unity/Godot/Phaser or any MCP.

## 1. System Architecture & Prerequisites

- **Runtime**: CPython 3.9+ (`math`, `json`, `random`, `dataclasses`, `argparse`, `statistics`). No PyGame, no numpy, no MCP.
- **Emitters** (pure math, all deterministic seedable):
  - `SpiralEmitter` — continuous rotation, `degrees_per_tick` and `rounds`; bullet count grows linearly.
  - `FanEmitter` — aimed burst toward a target (deterministic `spread`, `count`, `speed`).
  - `RingEmitter` — full 360° burst with optional rotation offset; `density` bullets.
  - `LaserSweepEmitter` — a rotating beam front that "sweeps"; generates a fan-family every `sweep_every` ticks with alternating direction.
- **Determinism**: standard `random.Random(seed)` per emitter; identical seed yields identical frame JSON to the byte.
- **Outputs**:
  - `--frames frames.json` — array of `{tick, bullets: [{x,y,dx,dy,speed}]}`.
  - `--ascii` — textual 21×13 snapshot of one frame (bullets drawn as `.`).
  - `--svg frame.svg` — one rendered frame as a standalone SVG (`<svg>` + `<circle>`s), viewable in any browser.
  - stdout — pattern summary (bullets, density, determinism check between two seeds).

## 2. Input/Output Data Contracts

```
python bullet_patterns.py spiral  --ticks 120 --speed 1.5 --degrees 9 --seed 42 --frames spiral.json
python bullet_patterns.py fan     --target 10,6 --count 8 --spread 40 --speed 1.2
python bullet_patterns.py ring    --density 16 --rotation 15
python bullet_patterns.py laser   --ticks 90 --sweep 30 --dir -1
python bullet_patterns.py ascii   --seed 99 --svg frame.svg
```

Common `--seed`, `--tick-rate` scaling. Exit `0` on success, `2` on argparse/usage error.

## 3. Production Reference Implementation

```python
#!/usr/bin/env python3
"""bullet_patterns.py - deterministic bullet-hell pattern math (pure stdlib)."""
import argparse
import json
import math
import random
from dataclasses import dataclass, field


@dataclass
class Bullet:
    x: float
    y: float
    dx: float
    dy: float
    speed: float
    born: int

    def advance(self, ticks):
        self.x += self.dx * ticks
        self.y += self.dy * ticks


@dataclass
class Pattern:
    name: str = ""
    seed: int = 0
    bullets: list = field(default_factory=list)
    frames: list = field(default_factory=list)

    def snapshot(self) -> dict:
        return {"bullets": [{"x": round(b.x, 2), "y": round(b.y, 2),
                             "dx": round(b.dx, 3), "dy": round(b.dy, 3),
                             "speed": round(b.speed, 2), "born": b.born}
                            for b in self.bullets]}


def seed_rng(seed: int) -> random.Random:
    return random.Random(seed)


def spawn_bullet(rng, px, py, angle, speed):
    return Bullet(px, py, math.cos(angle) * speed, math.sin(angle) * speed,
                  speed, 0)


def spiral(seed, ticks, pivot=(0, 0), speed=1.5, degrees_per_tick=9):
    rng = seed_rng(seed)
    pat = Pattern(name="spiral", seed=seed)
    px, py = pivot
    angle = 0.0
    for t in range(ticks):
        if t % 6 == 0:
            pat.bullets.append(spawn_bullet(rng, px, py, angle, speed + rng.uniform(-0.2, 0.2)))
        angle += math.radians(degrees_per_tick)
        for b in pat.bullets:
            b.advance(1)
        pat.frames.append(pat.snapshot())
    return pat


def fan(seed, target=(10, 6), origin=(0, 6), count=8, spread_deg=40, speed=1.2):
    rng = seed_rng(seed)
    pat = Pattern(name="fan", seed=seed)
    base = math.atan2(target[1] - origin[1], target[0] - origin[0])
    for i in range(count):
        offset = math.radians(spread_deg) * (i / (count - 1) - 0.5)
        pat.bullets.append(spawn_bullet(rng, origin[0], origin[1], base + offset, speed))
    # simulate 40 ticks so the fan opens and fans out
    for _ in range(40):
        for b in pat.bullets:
            b.advance(1)
        pat.frames.append(pat.snapshot())
    return pat


def ring(seed, density=16, rotation_deg=0, speed=1.0, ticks=30):
    rng = seed_rng(seed)
    pat = Pattern(name="ring", seed=seed)
    base = math.radians(rotation_deg)
    for i in range(density):
        ang = base + 2 * math.pi * i / density
        pat.bullets.append(spawn_bullet(rng, 0, 6, ang, speed))
    for _ in range(ticks):
        for b in pat.bullets:
            b.advance(1)
        pat.frames.append(pat.snapshot())
    return pat


def laser(seed, ticks, sweep_every=30, direction=-1, speed=1.3, origin=(0, 6)):
    rng = seed_rng(seed)
    pat = Pattern(name="laser", seed=seed)
    ax = 0.0
    for t in range(ticks):
        if t % sweep_every == 0:
            base = math.radians(ax)
            for k in range(7):
                pat.bullets.append(spawn_bullet(rng, origin[0], origin[1],
                                                base + math.radians(k - 3) * 6, speed))
            ax += direction * 18  # rotate beam 18° per sweep
        for b in pat.bullets:
            b.advance(1)
        pat.frames.append(pat.snapshot())
    return pat


def frame_ascii(pat, w=21, h=13):
    grid = [[" " for _ in range(w)] for _ in range(h)]
    for b in pat.bullets:
        ix = min(w - 1, max(0, int(round(b.x))))
        iy = min(h - 1, max(0, int(round(b.y))))
        grid[iy][ix] = "."
    border = "+" + "-" * w + "+"
    lines = [border]
    for row in grid:
        lines.append("|" + "".join(row) + "|")
    lines.append(border)
    return "\n".join(lines)


def render_svg(pat):
    w = 22
    h = 13
    paths = []
    for b in pat.bullets:
        cx = (b.x + 1) * 18
        cy = (b.y + 1) * 18
        paths.append(f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="3" fill="#e33" opacity="0.9"/>')
    return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{w * 18}" height="{h * 18}" viewBox="0 0 {w * 18} {h * 18}">
  <rect width="100%" height="100%" fill="#111"/>
  {''.join(paths)}
</svg>"""


def determinism_check(seed):
    a = spiral(seed, 60).frames
    b = spiral(seed, 60).frames
    return json.dumps(a) == json.dumps(b)


def main(argv=None):
    ap = argparse.ArgumentParser(prog="bullet_patterns")
    ap.add_argument("pattern", choices=["spiral", "fan", "ring", "laser", "ascii"])
    ap.add_argument("--seed", type=int, default=42, help="deterministic seed")
    ap.add_argument("--ticks", type=int, default=90)
    ap.add_argument("--speed", type=float, default=1.3)
    ap.add_argument("--degrees", type=float, default=9)
    ap.add_argument("--density", type=int, default=16)
    ap.add_argument("--count", type=int, default=8)
    ap.add_argument("--spread", type=float, default=40)
    ap.add_argument("--target", default="10,6")
    ap.add_argument("--rotation", type=float, default=0)
    ap.add_argument("--sweep", type=int, default=30)
    ap.add_argument("--dir", type=int, default=-1)
    ap.add_argument("--frames", default=None)
    ap.add_argument("--svg", default=None)
    args = ap.parse_args(argv)

    if args.pattern == "spiral":
        pat = spiral(args.seed, args.ticks, speed=args.speed, degrees_per_tick=args.degrees)
    elif args.pattern == "fan":
        tx, ty = (int(v) for v in args.target.split(","))
        pat = fan(args.seed, target=(tx, ty), count=args.count, spread_deg=args.spread, speed=args.speed)
    elif args.pattern == "ring":
        pat = ring(args.seed, density=args.density, rotation_deg=args.rotation, speed=args.speed)
    elif args.pattern in ("laser", "ascii"):
        pat = laser(args.seed, args.ticks, sweep_every=args.sweep, direction=args.dir,
                    speed=args.speed)

    if args.frames:
        with open(args.frames, "w") as fh:
            json.dump(pat.frames, fh)

    last = pat.frames[-1] if pat.frames else pat.snapshot()
    print(f"{pat.name}(seed={args.seed}) active_bullets={len(last['bullets'])}")
    if args.pattern in ("laser", "ascii"):
        print("determinism(seed=seed):", "OK" if determinism_check(args.seed) else "BROKEN")

    if args.svg:
        with open(args.svg, "w") as fh:
            fh.write(render_svg(pat))
        print(f"svg rendered -> {args.svg}")

    top = len(pat.frames) // 2
    mid = pat.frames[top]
    pat.bullets = [Bullet(**{**b, "born": 0}) for b in mid["bullets"]]
    if args.pattern == "ascii":
        print(frame_ascii(pat))
    return 0


if __name__ == "__main__":
    import sys
    sys.exit(main())
```

## 4. Execution Protocol & Step-by-Step Workflow

1. **Generate a spiral pattern**: `python bullet_patterns.py spiral --ticks 60 --degrees 12 --frames spiral.json` — 60 frames, 6-degree rotation steps, bullets spawned every 6 ticks.
2. **Inspect density**: `spiral.json` stores `[{tick, bullets:[...]}]`; compute peak active bullet count with a small jq or stdlib script.
3. **Aim a fan**: `python bullet_patterns.py fan --target 16,8 --count 9 --spread 50` — bullets fan toward the coordinate and spread over 40 ticks.
4. **Debug visually offline**: `python bullet_patterns.py ascii --seed 99 --svg frame.svg` prints a 21×13 ASCII snapshot and writes a standalone SVG — open `frame.svg` in any browser to see the frame.
5. **Determinism guard**: rerun the same seed; `determinism` prints `OK`. Use `--seed` variation to explore a family of patterns while keeping replays reproducible.
6. **Wire into a real engine**: consume `frames.json` in a game loop (Phaser/Unity) — bullets already carry `x,y,dx,dy,speed`.

## 5. Edge Cases & Error Handling

- **Oversized seed change** → rotation offset/speeds jitter via `Random(seed)`; bullet count is deterministic, never stochastic run-to-run.
- **Count=1 fan** → divides by 1 → offset 0 → single aimed bullet; formula `(i/(count-1)-0.5)` is guarded for count>1.
- **Negative `--dir`** → laser sweep direction flips each sweep; magnitude always positive via `abs()`.
- **Chromed SVG** → circle radii fixed; density above ~200 bullets may overlap — reduce `--ticks`/`--density` for clarity.
- **Rendering bullets** bred from mid-frame → `Bullet(**mid)` re-hydrates position/velocity but resets `born` to 0 for a clean still-frame.
- **Missing `--frames` target** → JSON written only when requested; command always returns exit code 0 on compute success.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.