Phaser AE

Pin-pulling puzzles

Free packv3.0.0-12DPhysics

The rules of a pin-pulling physics puzzle and its hero-rescue variant, on real rigid-body physics. Pull a pin and what it held falls, rolls and piles; the rules decide whether the cup fills, the hero survives or the level is lost.

This is a Free pack. Every phaser.io account can use it in their games.

Released October 19, 2026

Pin-pulling puzzles: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

The pack

pullPin

function pullPin(): Pack<{ pullPin: PullPinApi }>

The pull-pin pack: game.pullPin, Pull the Pin's and Hero Rescue's rules on a physics-2d world per level.

Functions

createPullPin

function createPullPin(level: PullPinLevel, options: PullPinOptions = {}): PullPinGame

Make a game of a level. Throws, listing every problem, on a level validatePullPinLevel refuses.

fillBalls

function fillBalls(fill: PullPinFill): PullPinBall[]

The balls a fill holds: a close-packed grid inside its box, top row first.

validatePullPinLevel

function validatePullPinLevel(level: PullPinLevel, kinds: ReadonlyArray<string> = []): string[]

Check a level and say what is wrong with it, as plain sentences; empty when it can be played.

Constants

pullPinDefaults

const pullPinDefaults: { readonly gravity: 900; readonly pinThickness: 14; readonly pinSpeed: 1400; readonly ballRadius: 11; readonly rockRadius: 22; readonly cupHeight: 90; readonly heroWidth: 36; readonly heroHeight: 56; readonly heroSpeed: 110; readonly enemyWidth: 40; readonly enemyHeight: 52; readonly enemySpeed: 90; readonly crush: 140; readonly settle: 1.5; readonly wallThickness: 16; }

The defaults the level falls back on.

pullPinLevels

const pullPinLevels: { readonly classic: readonly PullPinLevel[]; readonly hero: readonly PullPinLevel[] }

The built-in levels, by box.

Types

PullPinApi

interface PullPinApi

What the pull-pin pack adds to a game: game.pullPin.

  • create: typeof createPullPin — Make a game of a level.
  • validate: typeof validatePullPinLevel — Check a level; empty when it can be played.
  • fill: typeof fillBalls — The balls a fill holds.
  • levels: typeof pullPinLevels — The built-in levels: the classic box and the hero box.
  • defaults: typeof pullPinDefaults — The defaults every element falls back on.

PullPinBall

interface PullPinBall

A ball.

  • kind?: PullPinStuff — What it is made of. Default 'ball'.
  • x: number — Where it starts, across.
  • y: number — Where it starts, down.
  • radius?: number — Its radius. Default 11 (rocks 22).
  • colour?: string | null — A ball's colour; null (the default) is grey, which takes the colour of a coloured ball it touches.

PullPinContext

interface PullPinContext

What a registered element can reach.

  • readonly world: World — The physics world.
  • readonly level: Readonly<PullPinLevel> — The level.
  • readonly state: Readonly<PullPinState> — The game's state.
  • readonly balls: ReadonlyMap<number, { body: number; kind: PullPinStuff; colour: string | null; radius: number }> — The balls still in play, by id: their body in the world, stuff and colour.
  • emit(name: string, detail?: unknown): void — Tell the presenter something happened.
  • lose(): void — Lose the game.

PullPinCup

interface PullPinCup

The cup at the bottom: balls that fall into it are counted.

  • x: number — Its middle, across.
  • y: number — Its top edge.
  • width: number — Its width.
  • height?: number — Its height. Default 90.
  • need: number — How many balls fill it: the level is won when this many coloured balls are in.

PullPinElement

interface PullPinElement

An element of a kind the game registered: kind names it; the rest is its data.

  • kind: string — The registered kind's name.
  • [key: string]: unknown — The kind's own data.

PullPinElementKind

interface PullPinElementKind<S = unknown>

A kind of element a game adds. Its state must be plain JSON.

  • create(entry: PullPinElement, ctx: PullPinContext): S — Make an instance's state from its level entry.
  • step?(state: S, ctx: PullPinContext, dt: number): void — Run every fixed step, after the physics and the built-in rules.
  • view?(state: S, ctx: PullPinContext): unknown — What a presenter draws for it.

PullPinEnemy

interface PullPinEnemy

An enemy: walks at the hero when it can see it. Lava or a falling rock kills it.

  • x: number — Where it stands, across.
  • y: number — Where it stands, down.
  • width?: number — Its width. Default 40.
  • height?: number — Its height. Default 52.
  • speed?: number — How fast it walks. Default 90.

PullPinEvent

type PullPinEvent = | { kind: 'pull'; pin: string } | { kind: 'out'; pin: string } | { kind: 'colour'; ball: number; colour: string } | { kind: 'harden'; ball: number } | { kind: 'cup'; ball: number; count: number } | { kind: 'crush'; enemy: number } | { kind: 'burn'; enemy: number } | { kind: 'loot'; ball: number } | { kind: 'won' } | { kind: 'lost'; cause: 'grey' | 'bomb' | 'hero' | 'stuck' } | { kind: 'element'; element: number; name: string; detail?: unknown }

What happened, for the presenter.

PullPinFill

interface PullPinFill

A box filled with balls on a close-packed grid: how a level holds a chamber full.

  • kind?: PullPinStuff — What they are made of. Default 'ball'.
  • x: number — The box's left edge.
  • y: number — The box's top edge.
  • width: number — The box's width.
  • height: number — The box's height.
  • radius?: number — Each ball's radius. Default 11.
  • colour?: string | null — The balls' colour, or null for grey. Default null.
  • count?: number — How many at most. Default as many as fit.

PullPinGame

interface PullPinGame

A game of one level.

  • readonly level: Readonly<PullPinLevel> — The level, as played (a copy, fills expanded into balls).
  • readonly state: Readonly<PullPinState> — The state, live.
  • readonly world: World — The physics world, for a registered element or a presenter. Replaced by a restart or a restore.
  • step(dt: number): void — Advance by dt seconds. Once the game is over the physics runs on and the rules stop.
  • pull(pin: string): boolean — Pull a pin by id. False when there is no such pin, it is out already, or the game is over.
  • pinAt(point: { x: number; y: number }, reach?: number): string | null — The pin a tap at this point would pull (its handle, or the bar itself), or null.
  • drain(): PullPinEvent[] — What happened since the last drain, oldest first.
  • view(): PullPinView — Everything to draw.
  • restart(): void — Start the level again.
  • snapshot(): PullPinSave — The game as plain JSON: its moves, replayed on restore.
  • restore(save: PullPinSave): void — Resume a snapshot of this level. Throws on another level's, and leaves the game as it was.

PullPinHero

interface PullPinHero

The hero (Hero Rescue): walks to treasure it can see, and must not touch lava or an enemy.

  • x: number — Where it stands, across (its middle).
  • y: number — Where it stands, down (its middle).
  • width?: number — Its width. Default 36.
  • height?: number — Its height. Default 56.
  • speed?: number — How fast it walks. Default 110.

PullPinLevel

interface PullPinLevel

A level.

  • id?: string — Its id, which saves carry.
  • name?: string — A name to show.
  • width: number — The level's width.
  • height: number — The level's height. A ball that falls below it is gone.
  • gravity?: number — Gravity, units per second squared, down. Default 900.
  • walls: PullPinWall[] — The glass.
  • pins: PullPinPin[] — The pins.
  • balls?: PullPinBall[] — Single balls.
  • fills?: PullPinFill[] — Boxes of balls.
  • cup?: PullPinCup — The cup, for the classic game.
  • hero?: PullPinHero — The hero, for the hero game.
  • enemies?: PullPinEnemy[] — Enemies, for the hero game.
  • rules?: PullPinRules — The rule variations.
  • elements?: PullPinElement[] — Elements of kinds the game registered.

PullPinMove

type PullPinMove = { kind: 'advance'; seconds: number } | { kind: 'pull'; pin: string }

A move: time passing, or a pin pulled.

PullPinOptions

interface PullPinOptions

How a game is made.

  • elements?: Record<string, PullPinElementKind<unknown>> — Element kinds beyond the built-in ones.
  • step?: number — The fixed step the rules and physics run at, seconds. Default 1/120.

PullPinPin

interface PullPinPin

A pin: a bar that holds things up, and slides out along its length when pulled.

  • id?: string — Its id. Default pin-<index>.
  • x: number — Its middle, across.
  • y: number — Its middle, down.
  • length: number — Its length.
  • angle?: number — Its angle, radians: 0 lies across, Math.PI / 2 stands up. Default 0.
  • thickness?: number — Its thickness. Default 14.
  • pull?: 1 | -1 — Which way it slides out: 1 along its angle (the handle at that end), -1 the other way. Default 1.
  • speed?: number — How fast it slides, units per second. Default 1400.

PullPinRules

interface PullPinRules

The rule variations.

  • grey?: 'lose' | 'ignore' — A grey ball in the cup: 'lose' (the default) or 'ignore' (it does not count).
  • bombs?: 'lose' | 'ignore' — A bomb in the cup, or touching the hero: 'lose' (the default) or 'ignore'.
  • crush?: number — How hard a rock must hit an enemy to crush it, units per second. Default 140.
  • settle?: number — Seconds everything must lie still, with no pin left to pull, before a level that is not won is lost. Default 1.5.

PullPinSave

interface PullPinSave

A saved game.

  • format: 'phaser-ae-pull-pin' — Always 'phaser-ae-pull-pin'.
  • version: 1 — The save format's version, 1.
  • level: string — The level's id ('' for a level without one).
  • history: ReturnType<History<unknown, PullPinMove>['snapshot']> — The moves, as the kernel's history saves them.

PullPinState

interface PullPinState

The game's state, to read.

  • phase: 'playing' | 'won' | 'lost' — 'playing', 'won' or 'lost'.
  • cause: 'grey' | 'bomb' | 'hero' | 'stuck' | null — Why a lost game was lost.
  • time: number — Seconds played.
  • cup: number — Balls in the cup.
  • pulled: string[] — The pins pulled, in order.

PullPinStuff

type PullPinStuff = 'ball' | 'bomb' | 'lava' | 'water' | 'stone' | 'rock' | 'gold'

What a ball is made of.

PullPinView

interface PullPinView

Everything a presenter draws, in level units.

  • walls: PullPinWall[] — The walls, as the level gave them.
  • pins: { id: string; x: number; y: number; length: number; angle: number; thickness: number; handle: { x: number; y: number }; sliding: boolean }[] — Each pin still in the glass: where it is now, its handle's end, and whether it is sliding out.
  • balls: { id: number; x: number; y: number; angle: number; radius: number; kind: PullPinStuff; colour: string | null }[] — Each ball still in play: position, spin, radius, stuff and colour.
  • cup: { x: number; y: number; width: number; height: number; count: number; need: number } | null — The cup and how full it is.
  • hero: { x: number; y: number; width: number; height: number; alive: boolean; facing: 1 | -1; looted: boolean } | null — The hero.
  • enemies: { x: number; y: number; width: number; height: number; alive: boolean; facing: 1 | -1 }[] — The enemies.
  • elements: { kind: string; view: unknown }[] — What each registered element's view returned.

PullPinWall

type PullPinWall = | { kind: 'box'; x: number; y: number; width: number; height: number; angle?: number } | { kind: 'circle'; x: number; y: number; radius: number } | { kind: 'polygon'; points: ReadonlyArray<readonly [number, number]> } /** A wall along a polyline, `thickness` thick: the sides of a glass. */ | { kind: 'chain'; points: ReadonlyArray<readonly [number, number]>; thickness?: number; closed?: boolean }

A static wall. Units are the level's, y down.